From f6b45a6923269cb1e3fdc8f12938e346382e34c8 Mon Sep 17 00:00:00 2001 From: chodak166 Date: Sat, 2 Aug 2025 19:32:20 +0200 Subject: [PATCH] Added initial C++17 implementation --- .gitignore | 4 + README.md | 101 ++-- cpp17/.clang-format | 83 +++ cpp17/.devcontainer/Dockerfile | 5 + cpp17/.devcontainer/devcontainer.json | 20 + cpp17/.devcontainer/docker-compose.yml | 12 + cpp17/CMakeLists.txt | 14 + cpp17/CMakePresets.json | 20 + cpp17/README.md | 111 ++++ cpp17/TODO.md | 55 ++ cpp17/app/CMakeLists.txt | 36 ++ cpp17/app/src/App.cpp | 72 +++ cpp17/app/src/App.h | 31 + cpp17/app/src/Main.cpp | 10 + cpp17/app/src/SpdLogger.h | 49 ++ cpp17/app/src/Version.h.in | 21 + cpp17/doc/add-item-sequence.md | 33 + cpp17/doc/request-sequence.md | 25 + cpp17/docker/Dockerfile | 13 + cpp17/docker/docker-compose.yml | 8 + cpp17/lib/CMakeLists.txt | 45 ++ cpp17/lib/include/autostore/AutoStore.h | 57 ++ cpp17/lib/include/autostore/ILogger.h | 98 +++ cpp17/lib/src/AutoStore.cpp | 90 +++ .../lib/src/application/commands/AddItem.cpp | 21 + cpp17/lib/src/application/commands/AddItem.h | 27 + .../src/application/interfaces/IAuthService.h | 17 + cpp17/lib/src/application/interfaces/IClock.h | 14 + .../application/interfaces/IItemRepository.h | 22 + .../application/interfaces/IOrderService.h | 14 + .../application/interfaces/IUserRepository.h | 23 + cpp17/lib/src/domain/entities/Item.h | 20 + cpp17/lib/src/domain/entities/User.h | 15 + .../src/domain/polices/ItemExpirationPolicy.h | 18 + .../src/infrastructure/adapters/SystemClock.h | 17 + .../lib/src/infrastructure/helpers/Jsend.cpp | 32 + cpp17/lib/src/infrastructure/helpers/Jsend.h | 20 + .../src/infrastructure/helpers/JsonItem.cpp | 76 +++ .../lib/src/infrastructure/helpers/JsonItem.h | 22 + .../infrastructure/http/HttpOrderService.cpp | 35 ++ .../infrastructure/http/HttpOrderService.h | 20 + .../src/infrastructure/http/HttpServer.cpp | 126 ++++ .../lib/src/infrastructure/http/HttpServer.h | 30 + .../repositories/FileItemRepository.cpp | 119 ++++ .../repositories/FileItemRepository.h | 29 + .../repositories/FileUserRepository.cpp | 130 ++++ .../repositories/FileUserRepository.h | 30 + .../webapi/controllers/StoreController.cpp | 53 ++ .../src/webapi/controllers/StoreController.h | 27 + cpp17/tests/CMakeLists.txt | 31 + .../integration/FileItemRepository.test.cpp | 365 ++++++++++++ .../integration/FileUserRepository.test.cpp | 312 ++++++++++ cpp17/vcpkg.json | 10 + openapi.yaml | 564 ++++++++++++++++++ 54 files changed, 3213 insertions(+), 39 deletions(-) create mode 100644 cpp17/.clang-format create mode 100644 cpp17/.devcontainer/Dockerfile create mode 100644 cpp17/.devcontainer/devcontainer.json create mode 100644 cpp17/.devcontainer/docker-compose.yml create mode 100644 cpp17/CMakeLists.txt create mode 100644 cpp17/CMakePresets.json create mode 100644 cpp17/README.md create mode 100644 cpp17/TODO.md create mode 100644 cpp17/app/CMakeLists.txt create mode 100644 cpp17/app/src/App.cpp create mode 100644 cpp17/app/src/App.h create mode 100644 cpp17/app/src/Main.cpp create mode 100644 cpp17/app/src/SpdLogger.h create mode 100644 cpp17/app/src/Version.h.in create mode 100644 cpp17/doc/add-item-sequence.md create mode 100644 cpp17/doc/request-sequence.md create mode 100644 cpp17/docker/Dockerfile create mode 100644 cpp17/docker/docker-compose.yml create mode 100644 cpp17/lib/CMakeLists.txt create mode 100644 cpp17/lib/include/autostore/AutoStore.h create mode 100644 cpp17/lib/include/autostore/ILogger.h create mode 100644 cpp17/lib/src/AutoStore.cpp create mode 100644 cpp17/lib/src/application/commands/AddItem.cpp create mode 100644 cpp17/lib/src/application/commands/AddItem.h create mode 100644 cpp17/lib/src/application/interfaces/IAuthService.h create mode 100644 cpp17/lib/src/application/interfaces/IClock.h create mode 100644 cpp17/lib/src/application/interfaces/IItemRepository.h create mode 100644 cpp17/lib/src/application/interfaces/IOrderService.h create mode 100644 cpp17/lib/src/application/interfaces/IUserRepository.h create mode 100644 cpp17/lib/src/domain/entities/Item.h create mode 100644 cpp17/lib/src/domain/entities/User.h create mode 100644 cpp17/lib/src/domain/polices/ItemExpirationPolicy.h create mode 100644 cpp17/lib/src/infrastructure/adapters/SystemClock.h create mode 100644 cpp17/lib/src/infrastructure/helpers/Jsend.cpp create mode 100644 cpp17/lib/src/infrastructure/helpers/Jsend.h create mode 100644 cpp17/lib/src/infrastructure/helpers/JsonItem.cpp create mode 100644 cpp17/lib/src/infrastructure/helpers/JsonItem.h create mode 100644 cpp17/lib/src/infrastructure/http/HttpOrderService.cpp create mode 100644 cpp17/lib/src/infrastructure/http/HttpOrderService.h create mode 100644 cpp17/lib/src/infrastructure/http/HttpServer.cpp create mode 100644 cpp17/lib/src/infrastructure/http/HttpServer.h create mode 100644 cpp17/lib/src/infrastructure/repositories/FileItemRepository.cpp create mode 100644 cpp17/lib/src/infrastructure/repositories/FileItemRepository.h create mode 100644 cpp17/lib/src/infrastructure/repositories/FileUserRepository.cpp create mode 100644 cpp17/lib/src/infrastructure/repositories/FileUserRepository.h create mode 100644 cpp17/lib/src/webapi/controllers/StoreController.cpp create mode 100644 cpp17/lib/src/webapi/controllers/StoreController.h create mode 100644 cpp17/tests/CMakeLists.txt create mode 100644 cpp17/tests/integration/FileItemRepository.test.cpp create mode 100644 cpp17/tests/integration/FileUserRepository.test.cpp create mode 100644 cpp17/vcpkg.json create mode 100644 openapi.yaml diff --git a/.gitignore b/.gitignore index b481d9c..604d834 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,10 @@ # ---> C++ # Prerequisites *.d +build +build-* +volumes +tmp # Compiled Object files *.slo diff --git a/README.md b/README.md index ac53306..18aebdb 100644 --- a/README.md +++ b/README.md @@ -55,45 +55,48 @@ AutoStore/ ├── Extern │ ├── │ └── <...downloaded libraries and git submodules> -└── Src - ├── Domain/ - │ ├── Entities/ - │ │ ├── User - │ │ └── Item - │ └── Services/ - │ └── ExpirationPolicy - ├── Application/ - │ ├── UseCases/ - │ │ ├── RegisterUser - │ │ ├── LoginUser - │ │ ├── AddItem - │ │ ├── GetItem - │ │ ├── DeleteItem - │ │ └── HandleExpiredItems - │ ├── Interfaces/ - │ │ ├── IUserRepository - │ │ ├── IItemRepository - │ │ ├── IAuthService - │ │ └── IClock - │ ├── Dto/ - │ └── Services/ - ├── Infrastructure/ - │ ├── Repositories/ - │ │ ├── FileUserRepository - │ │ └── FileItemRepository - │ ├── Adapters/ - │ │ ├── JwtAuthAdapter - │ │ ├── OrderUrlHttpClient - │ │ ├── SystemClockImpl - │ │ └── <... some extern lib adapters> - │ └── Helpers/ - │ └── <... DRY helpers> - └── WebApi/ - ├── Controllers/ - │ ├── StoreController - │ └── UserController - └── Auth/ - └── JwtMiddleware +├── Src +│ ├── Domain/ +│ │ ├── Entities/ +│ │ │ ├── User +│ │ │ └── Item +│ │ └── Services/ +│ │ └── ExpirationPolicy +│ ├── Application/ +│ │ ├── UseCases/ +│ │ │ ├── RegisterUser +│ │ │ ├── LoginUser +│ │ │ ├── AddItem +│ │ │ ├── GetItem +│ │ │ ├── DeleteItem +│ │ │ └── HandleExpiredItems +│ │ ├── Interfaces/ +│ │ │ ├── IUserRepository +│ │ │ ├── IItemRepository +│ │ │ ├── IAuthService +│ │ │ └── IClock +│ │ ├── Dto/ +│ │ └── Services/ +│ ├── Infrastructure/ +│ │ ├── Repositories/ +│ │ │ ├── FileUserRepository +│ │ │ └── FileItemRepository +│ │ ├── Adapters/ +│ │ │ ├── JwtAuthAdapter +│ │ │ ├── OrderUrlHttpClient +│ │ │ ├── SystemClockImpl +│ │ │ └── <... some extern lib adapters> +│ │ └── Helpers/ +│ │ └── <... DRY helpers> +│ └── WebApi/ +│ ├── Controllers/ +│ │ ├── StoreController +│ │ └── UserController +│ └── Auth/ +│ └── JwtMiddleware +└── Tests + ├── Unit/ + └── Integration/ ``` ## Build and Run @@ -106,3 +109,23 @@ docker compose up to build and run the application. Otherwise, please provide a `/README.md` file with setup and running instructions. + +## API Endpoints + +See `openapi.yaml` file for suggested API (test it with Tavern, Postman etc.). +Here's a summary of example API endpoints: + +| Endpoint | Method | Description | +|-------------------------|--------|--------------------------------------| +| `/register` | POST | Register a new user account | +| `/login` | POST | Authenticate user and get JWT token | +| `/users` | GET | Get list of all users | +| `/users/{id}` | GET | Get user by ID | +| `/users/{id}` | POST | Create new user (admin) | +| `/users/{id}` | PUT | Update user details | +| `/users/{id}` | DELETE | Delete user account | +| `/items` | GET | Get user's items | +| `/items` | POST | Create new item | +| `/items/{id}` | GET | Get item by ID | +| `/items/{id}` | PUT | Update item details | +| `/items/{id}` | DELETE | Delete item | diff --git a/cpp17/.clang-format b/cpp17/.clang-format new file mode 100644 index 0000000..1fce9b2 --- /dev/null +++ b/cpp17/.clang-format @@ -0,0 +1,83 @@ +--- +Language: Cpp +AccessModifierOffset: -2 +AlignAfterOpenBracket: Align +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignEscapedNewlines: Right +AlignOperands: true +AlignTrailingComments: true +AllowAllArgumentsOnNextLine: true +AllowAllConstructorInitializersOnNextLine: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: Inline +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: MultiLine +BinPackArguments: true +BinPackParameters: true +BreakBeforeBinaryOperators: NonAssignment +BreakBeforeBraces: Custom +BraceWrapping: + AfterClass: true + AfterControlStatement: false + AfterEnum: false + AfterFunction: true + AfterNamespace: false + AfterStruct: true + AfterUnion: false + AfterExternBlock: false + BeforeCatch: false + BeforeElse: false + IndentBraces: false + SplitEmptyFunction: false + SplitEmptyRecord: false + SplitEmptyNamespace: false +BreakBeforeInheritanceComma: false +BreakInheritanceList: BeforeColon +ColumnLimit: 80 +CompactNamespaces: false +ConstructorInitializerIndentWidth: 2 +ContinuationIndentWidth: 2 +Cpp11BracedListStyle: true +DerivePointerAlignment: false +DisableFormat: false +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: true +IndentCaseLabels: true +IndentPPDirectives: None +IndentWidth: 2 +IndentWrappedFunctionNames: false +KeepEmptyLinesAtTheStartOfBlocks: false +# LambdaBodyIndentation: Signature +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +PointerAlignment: Left +ReflowComments: true +SortIncludes: false +SortUsingDeclarations: false +SpaceAfterCStyleCast: false +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: true +# SpaceAroundPointerQualifiers: Default +SpaceBeforeAssignmentOperators: true +SpaceBeforeCpp11BracedList: false +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatements +SpaceBeforeRangeBasedForLoopColon: true +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: false +SpacesInCStyleCastParentheses: false +SpacesInConditionalStatement: false +SpacesInContainerLiterals: true +SpacesInParentheses: false +SpacesInSquareBrackets: false +TabWidth: 8 +UseTab: Never diff --git a/cpp17/.devcontainer/Dockerfile b/cpp17/.devcontainer/Dockerfile new file mode 100644 index 0000000..9e9897d --- /dev/null +++ b/cpp17/.devcontainer/Dockerfile @@ -0,0 +1,5 @@ +FROM kuyoh/vcpkg:2025.06.13-ubuntu24.04 +RUN apt update -y && apt install -y gdb +RUN chown -R 1000:1000 /opt/vcpkg +WORKDIR /workspace +CMD ["bash"] diff --git a/cpp17/.devcontainer/devcontainer.json b/cpp17/.devcontainer/devcontainer.json new file mode 100644 index 0000000..11a9434 --- /dev/null +++ b/cpp17/.devcontainer/devcontainer.json @@ -0,0 +1,20 @@ +{ + "name": "AutoStore dev container", + "dockerComposeFile": "./docker-compose.yml", + "service": "app", + "workspaceFolder": "/workspace", + "customizations": { + "vscode": { + "settings": { + "terminal.integrated.defaultProfile.linux": "bash", + "cmake.useCMakePresets": "always" + }, + "extensions": [ + "ms-vscode.cmake-tools", + "fredericbonnet.cmake-test-adapter", + "twxs.cmake", + "ms-vscode.cpptools-extension-pack" + ] + } + } +} \ No newline at end of file diff --git a/cpp17/.devcontainer/docker-compose.yml b/cpp17/.devcontainer/docker-compose.yml new file mode 100644 index 0000000..a572fc5 --- /dev/null +++ b/cpp17/.devcontainer/docker-compose.yml @@ -0,0 +1,12 @@ +version: "3.9" +services: + app: + image: dev-cpp-vcpkg-img + build: + context: .. + dockerfile: .devcontainer/Dockerfile + volumes: + - ../:/workspace:cached + - ./volumes/vscode-server:/home/ubuntu/.vscode-server + command: ["sleep", "infinity"] + user: "1000:1000" diff --git a/cpp17/CMakeLists.txt b/cpp17/CMakeLists.txt new file mode 100644 index 0000000..63a317d --- /dev/null +++ b/cpp17/CMakeLists.txt @@ -0,0 +1,14 @@ +cmake_minimum_required(VERSION 3.20) +project(AutoStore VERSION 1.0.0 LANGUAGES CXX) + +set(PROJECT_ROOT ${PROJECT_SOURCE_DIR}) + +set(CTEST_OUTPUT_ON_FAILURE ON) +enable_testing(true) + +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/bin) +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/lib) + +add_subdirectory(lib) +add_subdirectory(app) +add_subdirectory(tests) diff --git a/cpp17/CMakePresets.json b/cpp17/CMakePresets.json new file mode 100644 index 0000000..d1fe37f --- /dev/null +++ b/cpp17/CMakePresets.json @@ -0,0 +1,20 @@ +{ + "version": 3, + "configurePresets": [ + { + "name": "default", + "toolchainFile": "${env:VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_EXPORT_COMPILE_COMMANDS": "TRUE" + } + } + ], + "buildPresets": [ + { + "name": "default", + "configurePreset": "default", + "jobs": 8 + } + ] +} \ No newline at end of file diff --git a/cpp17/README.md b/cpp17/README.md new file mode 100644 index 0000000..0e27a23 --- /dev/null +++ b/cpp17/README.md @@ -0,0 +1,111 @@ +# About this Repository + +This repository hosts multiple implementations of the same back-end application. The aim is to provide quick, side-by-side comparisons of different technologies (languages, frameworks, libraries) while preserving consistent business logic across all implementations. + +Following principles such as **SOLID** and maintainable architectural patterns (**Clean, Hexagonal, Onion, or even DDD**) is recommended to clearly showcase the strengths and idioms of each technology. + +Some over-engineering is acceptable to demonstrate architectural features, but please keep implementations readable and avoid excessive complexity (e.g., skip event sourcing or strict CQRS unless intentionally focusing on those patterns for comparison). + +--- + +### Project Idea: AutoStore + +A system to store items with expiration dates. When items expire, new ones are automatically ordered by making a POST request to the configured order URL. + +#### Business Rules (Domain) + +1. **Each item has a name and an expiration date.** +2. **Expired items are automatically removed from the store.** +3. **When an item expires, a new item of the same type is automatically ordered.** +4. **Expired items can be added to the store, triggering immediate ordering.** +5. **Every item belongs to a user.** + +#### Application Requirements + +1. **Users can register and log in to obtain a JWT.** +2. **Authenticated users manage their personal collection of items via an HTTP API.** +3. **Each item has an associated "order URL".** +4. **When an item expires, the system must notify the "order URL" with an HTTP POST request.** +5. **This call should occur immediately when the item's expiration date is reached, or when an expired item is added.** +6. **Upon startup, the system must verify expiration dates for all items.** +7. **Persistent storage must be used (file, database, etc.).** + +--- + +## Layer Boundaries + +| Layer | Responsibility | Internal Dependencies | External Dependencies | +|------------------|--------------------------------------------------------------- |----------------------|-----------------------| +| **Domain** | Entities, value objects, domain services (pure business logic) | None | None (language only) | +| **Application** | Use cases, orchestration, DTOs, infrastructure interfaces | Domain | None or minimal | +| **Infrastructure**| Implementations (repositories, HTTP, auth), background jobs | Application | Any (framework/lib) | +| **Presentation** | API controllers, DTOs, auth middleware | Application | UI/web/CLI/others | +| **Assembly** | Main app, DI, startup logic, job scheduling | Any layer | DI container, config, framework, etc.| + +--- + +### Possible directory layout (will vary from tech to tech) + +```plaintext +AutoStore/ +├── App +│ ├── Main +│ ├── AppConfig +│ └── ... +├── Extern +│ ├── +│ └── <...downloaded libraries and git submodules> +├── Src +│ ├── Domain/ +│ │ ├── Entities/ +│ │ │ ├── User +│ │ │ └── Item +│ │ └── Services/ +│ │ └── ExpirationPolicy +│ ├── Application/ +│ │ ├── UseCases/ +│ │ │ ├── RegisterUser +│ │ │ ├── LoginUser +│ │ │ ├── AddItem +│ │ │ ├── GetItem +│ │ │ ├── DeleteItem +│ │ │ └── HandleExpiredItems +│ │ ├── Interfaces/ +│ │ │ ├── IUserRepository +│ │ │ ├── IItemRepository +│ │ │ ├── IAuthService +│ │ │ └── IClock +│ │ ├── Dto/ +│ │ └── Services/ +│ ├── Infrastructure/ +│ │ ├── Repositories/ +│ │ │ ├── FileUserRepository +│ │ │ └── FileItemRepository +│ │ ├── Adapters/ +│ │ │ ├── JwtAuthAdapter +│ │ │ ├── OrderUrlHttpClient +│ │ │ ├── SystemClockImpl +│ │ │ └── <... some extern lib adapters> +│ │ └── Helpers/ +│ │ └── <... DRY helpers> +│ └── WebApi/ +│ ├── Controllers/ +│ │ ├── StoreController +│ │ └── UserController +│ └── Auth/ +│ └── JwtMiddleware +└── Tests + ├── Unit/ + └── Integration/ +``` + +## Build and Run + +Ideally, each implementation should include a `/docker/docker-compose.yml` file so that you can simply run: + +```bash +docker compose up +``` +to build and run the application. + +Otherwise, please provide a `/README.md` file with setup and running instructions. diff --git a/cpp17/TODO.md b/cpp17/TODO.md new file mode 100644 index 0000000..c0dc4d8 --- /dev/null +++ b/cpp17/TODO.md @@ -0,0 +1,55 @@ +# C++17 AutoStore Implementation Plan + +This document outlines the steps to implement the C++17 version of the AutoStore application. Implemented classes should use `nxl::` namespace prefix. + +## Phase 1: Project Scaffolding & Build System + +- [x] Initialize a CMake project structure. +- [x] Set up the root `CMakeLists.txt` to manage the `app` and `lib` subdirectories. +- [x] Create the `lib` directory for the static library. +- [x] Create the `app` directory for the executable. +- [x] Configure `vcpkg` for dependency management and integrate it with CMake. +- [x] Add a dependency for an HTTP library (e.g., `cpp-httplib`) via `vcpkg`. +- [x] Add a dependency for a testing framework (e.g., `catch2`) via `vcpkg`. + +## Phase 2: Library (`lib`) - Dummy Implementation + +- [x] Create the directory structure for the library: `lib/src`, `lib/include`. +- [x] Create `lib/CMakeLists.txt` to build a static library. +- [x] In `lib/include/autostore`, define the public interface for the `App` to use. +- [x] Create a dummy `AutoStore` class in `lib/include/autostore/AutoStore.h` and a source file in `lib/src/AutoStore.cpp`. +- [ ] Define initial classes for core domain and application logic inside the library (e.g., `ItemRepository`, `UserService`, etc.) to establish the architecture. These will be mostly private to the library initially and implemented later. +- [ ] Ensure the project compiles and links successfully with the dummy implementations. + +## Phase 3: Application (`app`) - Dummy Implementation + +- [ ] Create the directory structure for the application: `app/src`. +- [x] Create `app/CMakeLists.txt` to build the executable. +- [ ] Link the `app` against the `lib` static library. +- [ ] Implement the main `App` class in `app/src/App.h` and `app/src/App.cpp`. +- [ ] The `App` class will have a constructor `App(int argc, char** argv)` and an `exec()` method. +- [ ] Implement signal handling (for `SIGINT`, `SIGTERM`) in the `App` class for graceful shutdown. +- [x] In `app/src/Main.cpp`, instantiate and run the `App` class. +- [ ] Ensure the project compiles and links successfully with the dummy implementations. + +## Phase 4: Core Logic Implementation + +- [ ] Implement the Domain layer in `lib/src/domain`. +- [ ] Implement the Application layer in `lib/src/application`. +- [ ] Implement the Infrastructure layer in `lib/src/infrastructure` (e.g., file-based persistence, HTTP client for ordering). +- [ ] Implement the Presentation layer (HTTP API) using the chosen HTTP library. +- [ ] Implement the startup logic to check for expired items. +- [ ] Implement a background mechanism (e.g., a thread) to periodically check for expired items. + +## Phase 5: Testing + +- [ ] Set up a `tests` directory. +- [ ] Create `tests/CMakeLists.txt` to build the test runner. +- [ ] Write unit tests for the Domain layer. +- [ ] Write unit tests for the Application layer, using mocks for infrastructure interfaces. +- [ ] Write integration tests for the Infrastructure layer. + +## Phase 6: Containerization + +- [x] Create a `Dockerfile` to build the C++ application in a container. +- [x] Create a `docker-compose.yml` file to easily build and run the application. \ No newline at end of file diff --git a/cpp17/app/CMakeLists.txt b/cpp17/app/CMakeLists.txt new file mode 100644 index 0000000..069d100 --- /dev/null +++ b/cpp17/app/CMakeLists.txt @@ -0,0 +1,36 @@ +cmake_minimum_required(VERSION 3.20) + +project(AutoStoreApp LANGUAGES CXX VERSION 0.1.0) +set(TARGET_NAME AutoStore) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +find_package(spdlog CONFIG REQUIRED) + +configure_file(src/Version.h.in ${CMAKE_BINARY_DIR}/Version.h) + +set(SOURCES + src/Main.cpp + src/App.cpp + src/App.h + ) + + set (LIBRARIES + AutoStoreLib + spdlog::spdlog + ) + +add_executable(${TARGET_NAME} ${SOURCES}) +target_include_directories(${TARGET_NAME} + PRIVATE + ${CMAKE_BINARY_DIR} + ) + + # for docker test +# target_compile_options(${TARGET_NAME} PRIVATE -static-libgcc -static-libstdc++) +# target_link_options(${TARGET_NAME} PRIVATE -static-libgcc -static-libstdc++) + +target_link_libraries(${TARGET_NAME} PRIVATE ${LIBRARIES}) + +# add_subdirectory(tests/unit) diff --git a/cpp17/app/src/App.cpp b/cpp17/app/src/App.cpp new file mode 100644 index 0000000..8765ae9 --- /dev/null +++ b/cpp17/app/src/App.cpp @@ -0,0 +1,72 @@ +#include "App.h" +#include "SpdLogger.h" +#include +#include + +namespace nxl { + +using nxl::autostore::AutoStore; + +std::condition_variable App::exitCv; +std::mutex App::mtx; +bool App::shouldExit = false; +nxl::autostore::ILoggerPtr log{nullptr}; + +App::App(int argc, char** argv) +{ + signal(SIGINT, App::handleSignal); + signal(SIGTERM, App::handleSignal); + + std::filesystem::create_directories("data"); + + auto spdLogger = spdlog::stdout_color_mt("console"); + spdLogger->set_pattern("[%Y-%m-%d %H:%M:%S] [%^%l%$] %v"); + spdLogger->set_level(spdlog::level::debug); + log = logger = std::make_shared(spdLogger, 9); + autoStore = std::make_unique( + AutoStore::Config{ + .dataPath = "data", + .host = "0.0.0.0", + .port = 8080, + }, + logger); + + if (!autoStore->initialize()) { + std::cerr << "Failed to initialize AutoStore" << std::endl; + throw std::runtime_error("Failed to initialize AutoStore"); + } +} + +App::~App() = default; + +int App::exec() +{ + if (!autoStore->start()) { + std::cerr << "Failed to start AutoStore services" << std::endl; + return 1; + } + + logger->info("AutoStore is running. Press Ctrl+C to stop."); + + std::unique_lock lock(mtx); + exitCv.wait(lock, [] { return shouldExit; }); + + autoStore->stop(); + + return 0; +} + +void App::handleSignal(int signum) +{ + if (log) { + log->info("Caught signal %d. Graceful shutdown.", signum); + } + + { + std::lock_guard lock(mtx); + shouldExit = true; + } + exitCv.notify_one(); +} + +} // namespace nxl \ No newline at end of file diff --git a/cpp17/app/src/App.h b/cpp17/app/src/App.h new file mode 100644 index 0000000..850e475 --- /dev/null +++ b/cpp17/app/src/App.h @@ -0,0 +1,31 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace nxl { + +class App +{ +public: + App(int argc, char** argv); + ~App(); + int exec(); + +private: + static void handleSignal(int signum); + static std::condition_variable exitCv; + static std::mutex mtx; + static bool shouldExit; + + std::unique_ptr autoStore; + autostore::ILoggerPtr logger; +}; + +} // namespace nxl \ No newline at end of file diff --git a/cpp17/app/src/Main.cpp b/cpp17/app/src/Main.cpp new file mode 100644 index 0000000..a9baa97 --- /dev/null +++ b/cpp17/app/src/Main.cpp @@ -0,0 +1,10 @@ +#include "App.h" +#include "Version.h" +#include + +int main(int argc, char** argv) +{ + std::cout << "AutoStore v" << nxl::getVersionString() << std::endl; + nxl::App app(argc, argv); + return app.exec(); +} diff --git a/cpp17/app/src/SpdLogger.h b/cpp17/app/src/SpdLogger.h new file mode 100644 index 0000000..81f2b76 --- /dev/null +++ b/cpp17/app/src/SpdLogger.h @@ -0,0 +1,49 @@ +#pragma once + +#include +#include +#include + +class SpdLogger : public nxl::autostore::ILogger +{ +public: + explicit SpdLogger(std::shared_ptr logger, int8_t vlevel) + : logger{std::move(logger)}, vlevel{vlevel} + {} + +protected: + void log(LogLevel level, std::string_view message) override + { + switch (level) { + case LogLevel::Info: + logger->info(message); + break; + case LogLevel::Warning: + logger->warn(message); + break; + case LogLevel::Error: + logger->error(message); + break; + case LogLevel::Debug: + logger->debug(message); + break; + } + } + + void vlog(int8_t level, std::string_view message) override + { + if (level > vlevel) { + return; + } + logger->log(spdlog::level::info, + "[V:" + std::to_string(level) + "] " + std::string(message)); + } + + std::shared_ptr getLogger() const { return logger; } + + void setVLevel(int8_t level) { vlevel = level; } + +private: + int8_t vlevel{-1}; + std::shared_ptr logger; +}; diff --git a/cpp17/app/src/Version.h.in b/cpp17/app/src/Version.h.in new file mode 100644 index 0000000..b92fe98 --- /dev/null +++ b/cpp17/app/src/Version.h.in @@ -0,0 +1,21 @@ +#ifndef VERSION_H_IN +#define VERSION_H_IN + +#include + +namespace nxl { + +static constexpr int VERSION_MAJOR = ${PROJECT_VERSION_MAJOR}; +static constexpr int VERSION_MINOR = ${PROJECT_VERSION_MINOR}; +static constexpr int VERSION_PATCH = ${PROJECT_VERSION_PATCH}; +static constexpr char VERSION_SUFFIX[] = "${PROJECT_VERSION_SUFFIX}"; + +inline std::string getVersionString() +{ + return std::to_string(VERSION_MAJOR) + "." + std::to_string(VERSION_MINOR) + + "." + std::to_string(VERSION_PATCH) + VERSION_SUFFIX; +} + +} // namespace nxl + +#endif // VERSION_H_IN diff --git a/cpp17/doc/add-item-sequence.md b/cpp17/doc/add-item-sequence.md new file mode 100644 index 0000000..1fb5e71 --- /dev/null +++ b/cpp17/doc/add-item-sequence.md @@ -0,0 +1,33 @@ +# Add item sequence + +```mermaid +sequenceDiagram + participant Controller as StoreController + participant UseCase as AddItem Use Case + participant Clock as IClock + participant Policy as ExpirationPolicy + participant OrderService as OrderingService + participant HttpClient as HttpClient + participant Repo as IItemRepository + + Controller->>UseCase: execute(item) + + UseCase->>Clock: getCurrentTime() + Clock-->>UseCase: DateTime + + UseCase->>Policy: IsExpired(item, currentTime) + Policy-->>UseCase: boolean + + alt Item is expired + UseCase->>OrderService: orderItem(item) + OrderService->>HttpClient: POST to order URL + HttpClient-->>OrderService: Response + OrderService-->>UseCase: OrderResult + end + + UseCase->>Repo: save(item) + Repo->>Repo: Persist to storage + Repo-->>UseCase: Saved Item ID + + UseCase-->>Controller: Result (success/error) +``` diff --git a/cpp17/doc/request-sequence.md b/cpp17/doc/request-sequence.md new file mode 100644 index 0000000..a38295f --- /dev/null +++ b/cpp17/doc/request-sequence.md @@ -0,0 +1,25 @@ +# General request sequence with authentication + +```mermaid +sequenceDiagram + participant Client as HTTP Client + participant Router as Request Router + participant Auth as JwtMiddleware + participant Controller as Controller + participant UseCase as Use Case + + Client->>Router: POST /api/items (with JWT) + Router->>Auth: Forward request + + alt Authentication successful + Auth->>Auth: Validate JWT + Auth->>Controller: Forward authenticated request + Controller->>Controller: Parse request body to DTO + Controller->>UseCase: execute() + UseCase-->>Controller: Result (success/error) + Controller->>Controller: Convert result to HTTP response + Controller-->>Client: HTTP Response (2xx) + else Authentication fails + Auth-->>Client: 401 Unauthorized + end +``` diff --git a/cpp17/docker/Dockerfile b/cpp17/docker/Dockerfile new file mode 100644 index 0000000..c70024b --- /dev/null +++ b/cpp17/docker/Dockerfile @@ -0,0 +1,13 @@ +FROM kuyoh/vcpkg:2025.06.13-ubuntu24.04 AS base + +WORKDIR /workspace + +COPY .. . + +# generate and build +RUN cmake -DCMAKE_TOOLCHAIN_FILE:STRING=${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake \ + -DCMAKE_EXPORT_COMPILE_COMMANDS:BOOL=TRUE -DCMAKE_BUILD_TYPE:STRING=Release \ + -H/workspace -B/workspace/build -G Ninja +RUN cmake --build /workspace/build --config Release --target all -j 6 -- + +CMD ["/workspace/build/bin/AutoStore"] diff --git a/cpp17/docker/docker-compose.yml b/cpp17/docker/docker-compose.yml new file mode 100644 index 0000000..e6a35ec --- /dev/null +++ b/cpp17/docker/docker-compose.yml @@ -0,0 +1,8 @@ +version: "3.9" +services: + app: + build: + context: .. + dockerfile: Docker/Dockerfile + image: autostore-build-cpp-vcpkg-img + container_name: autostore-build-cpp-vcpkg diff --git a/cpp17/lib/CMakeLists.txt b/cpp17/lib/CMakeLists.txt new file mode 100644 index 0000000..fac829b --- /dev/null +++ b/cpp17/lib/CMakeLists.txt @@ -0,0 +1,45 @@ +cmake_minimum_required(VERSION 3.20) +project(AutoStoreLib) +set(TARGET_NAME AutoStoreLib) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Find dependencies +find_package(httplib CONFIG REQUIRED) +find_package(nlohmann_json CONFIG REQUIRED) + +add_library(${TARGET_NAME} STATIC + src/AutoStore.cpp + src/infrastructure/repositories/FileUserRepository.cpp + src/infrastructure/repositories/FileItemRepository.cpp + src/infrastructure/http/HttpServer.cpp + src/infrastructure/http/HttpOrderService.cpp + src/infrastructure/helpers/Jsend.cpp + src/infrastructure/helpers/JsonItem.cpp + src/webapi/controllers/StoreController.cpp + src/application/commands/AddItem.cpp +) + +target_include_directories(${TARGET_NAME} + PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/include/autostore + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/src +) + +target_sources(${TARGET_NAME} + PUBLIC + FILE_SET HEADERS + BASE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/include + FILES + include/autostore/AutoStore.h +) + + + +target_link_libraries(${TARGET_NAME} + PUBLIC + httplib::httplib + nlohmann_json::nlohmann_json +) \ No newline at end of file diff --git a/cpp17/lib/include/autostore/AutoStore.h b/cpp17/lib/include/autostore/AutoStore.h new file mode 100644 index 0000000..624c694 --- /dev/null +++ b/cpp17/lib/include/autostore/AutoStore.h @@ -0,0 +1,57 @@ +#pragma once + +#include "autostore/ILogger.h" +#include +#include +#include +#include + +namespace nxl::autostore { + +namespace application { +class IItemRepository; +class IClock; +class IOrderService; +} // namespace application + +namespace infrastructure { +class HttpServer; +} // namespace infrastructure + +namespace webapi { +class StoreController; +} // namespace webapi + +namespace application { +class AddItem; +} // namespace application + +class AutoStore +{ +public: + struct Config + { + std::string dataPath; + std::string host{"0.0.0.0"}; + uint16_t port{8080}; + }; + + AutoStore(Config config, ILoggerPtr logger); + ~AutoStore(); + + bool initialize(); + bool start(); + void stop(); + +private: + Config config; + ILoggerPtr log; + + std::unique_ptr httpServer; + std::unique_ptr storeController; + std::unique_ptr itemRepository; + std::unique_ptr clock; + std::unique_ptr orderService; +}; + +} // namespace nxl::autostore \ No newline at end of file diff --git a/cpp17/lib/include/autostore/ILogger.h b/cpp17/lib/include/autostore/ILogger.h new file mode 100644 index 0000000..2aac268 --- /dev/null +++ b/cpp17/lib/include/autostore/ILogger.h @@ -0,0 +1,98 @@ +#pragma once +#include +#include +#include +#include +#include +#include +namespace nxl::autostore { + +template auto to_printf_arg(T&& arg) +{ + if constexpr (std::is_same_v, std::string_view>) { + return std::string(arg).c_str(); + } else if constexpr (std::is_same_v, std::string>) { + return arg.c_str(); + } else { + return std::forward(arg); + } +} + +#define DEFINE_LOG_METHOD(name, level) \ + void name(const char* message) \ + { \ + log(LogLevel::level, std::string_view(message)); \ + } \ + template void name(const char* format, Args&&... args) \ + { \ + _log(LogLevel::level, format, -1, std::forward(args)...); \ + } + +#define DEFINE_LOGGER_ALIAS(original, alias) \ + template void alias(const char* format, Args&&... args) \ + { \ + original(format, std::forward(args)...); \ + } + +class ILogger +{ +public: + virtual ~ILogger() = default; + DEFINE_LOG_METHOD(info, Info) + DEFINE_LOG_METHOD(warning, Warning) + DEFINE_LOG_METHOD(error, Error) + DEFINE_LOG_METHOD(debug, Debug) + void verbose(int8_t level, const char* message) { vlog(level, message); } + template + void verbose(int8_t level, const char* format, Args&&... args) + { + _log(LogLevel::Verbose, format, level, std::forward(args)...); + } + // Aliases defined using macro + DEFINE_LOGGER_ALIAS(info, i) + DEFINE_LOGGER_ALIAS(warning, w) + DEFINE_LOGGER_ALIAS(error, e) + DEFINE_LOGGER_ALIAS(debug, d) + void v(int8_t level, const char* message) { vlog(level, message); } + template + void v(int8_t level, const char* format, Args&&... args) + { + _log(LogLevel::Verbose, format, level, std::forward(args)...); + } + +protected: + enum class LogLevel { Info, Warning, Error, Debug, Verbose }; + virtual void log(LogLevel level, std::string_view message) = 0; + virtual void vlog(int8_t level, std::string_view message) = 0; + +private: + template + void _log(LogLevel level, const char* format, int8_t vlevel = -1, + Args&&... args) + { + // Create a lambda that captures the converted arguments + auto format_message = [format](auto&&... args) { + // Calculate the required size + size_t size = std::snprintf(nullptr, 0, format, args...) + 1; + + // Format the message + std::string msg; + msg.resize(size); + std::snprintf(&msg[0], size, format, args...); + msg.pop_back(); + + return msg; + }; + + // Call the lambda with the converted arguments + std::string msg = + format_message(to_printf_arg(std::forward(args))...); + + vlevel == -1 ? log(level, msg) : vlog(vlevel, msg); + } +}; + +// Undefine the macro to avoid polluting the namespace +#undef DEFINE_LOGGER_ALIAS +using ILoggerPtr = std::shared_ptr; +} // namespace nxl::autostore \ No newline at end of file diff --git a/cpp17/lib/src/AutoStore.cpp b/cpp17/lib/src/AutoStore.cpp new file mode 100644 index 0000000..9e72c31 --- /dev/null +++ b/cpp17/lib/src/AutoStore.cpp @@ -0,0 +1,90 @@ +#include "AutoStore.h" +#include "infrastructure/repositories/FileItemRepository.h" +#include "infrastructure/adapters/SystemClock.h" +#include "infrastructure/http/HttpOrderService.h" +#include "webapi/controllers/StoreController.h" +#include "infrastructure/http/HttpServer.h" +#include +#include +#include + +namespace nxl::autostore { + +AutoStore::AutoStore(Config config, ILoggerPtr logger) + : config{std::move(config)}, log{std::move(logger)} +{} + +AutoStore::~AutoStore() +{ + if (httpServer && httpServer->isRunning()) { + stop(); + } +} + +bool AutoStore::initialize() +{ + try { + std::filesystem::create_directories(config.dataPath); + + // Initialize repositories and services + std::string itemsDbPath = + std::filesystem::path(config.dataPath) / "items.json"; + itemRepository = + std::make_unique(itemsDbPath); + + clock = std::make_unique(); + + orderService = std::make_unique(log); + + // Initialize HTTP server + httpServer = std::make_unique(log); + + // Initialize store controller + storeController = std::make_unique( + webapi::StoreController::Context{ + application::AddItem{*itemRepository, *clock, *orderService}}); + + log->info("AutoStore initialized successfully"); + return true; + } catch (const std::exception& e) { + log->error("Failed to initialize AutoStore: %s", e.what()); + return false; + } +} + +bool AutoStore::start() +{ + log->info("Starting AutoStore services..."); + + try { + storeController->registerRoutes(httpServer->getServer()); + + if (!httpServer->start(config.port, config.host)) { + log->error("Failed to start HTTP server"); + return false; + } + + log->info("AutoStore services started successfully"); + log->info("HTTP server listening on http://%s:%d", config.host, + config.port); + log->info("API endpoint: POST http://%s:%d/api/items", config.host, + config.port); + + return true; + } catch (const std::exception& e) { + log->error("Failed to start AutoStore services: %s", e.what()); + return false; + } +} + +void AutoStore::stop() +{ + log->info("Stopping AutoStore services..."); + + if (httpServer && httpServer->isRunning()) { + httpServer->stop(); + } + log->info("AutoStore services stopped"); +} + +} // namespace nxl::autostore \ No newline at end of file diff --git a/cpp17/lib/src/application/commands/AddItem.cpp b/cpp17/lib/src/application/commands/AddItem.cpp new file mode 100644 index 0000000..68acc02 --- /dev/null +++ b/cpp17/lib/src/application/commands/AddItem.cpp @@ -0,0 +1,21 @@ +#include "AddItem.h" +#include + +namespace nxl::autostore::application { + +AddItem::AddItem(IItemRepository& itemRepository, IClock& clock, + IOrderService& orderService) + : itemRepository(itemRepository), clock(clock), orderService(orderService) +{} + +domain::Item::Id_t AddItem::execute(domain::Item&& item) +{ + const auto currentTime = clock.getCurrentTime(); + + if (expirationPolicy.isExpired(item, currentTime)) { + orderService.orderItem(item); + } + return itemRepository.save(item); +} + +} // namespace nxl::autostore::application \ No newline at end of file diff --git a/cpp17/lib/src/application/commands/AddItem.h b/cpp17/lib/src/application/commands/AddItem.h new file mode 100644 index 0000000..bbefb54 --- /dev/null +++ b/cpp17/lib/src/application/commands/AddItem.h @@ -0,0 +1,27 @@ +#pragma once + +#include "domain/entities/Item.h" +#include "domain/polices/ItemExpirationPolicy.h" +#include "application/interfaces/IItemRepository.h" +#include "application/interfaces/IClock.h" +#include "application/interfaces/IOrderService.h" + +namespace nxl::autostore::application { + +class AddItem +{ +public: + virtual ~AddItem() = default; + + AddItem(IItemRepository& itemRepository, IClock& clock, + IOrderService& orderService); + domain::Item::Id_t execute(domain::Item&& item); + +private: + IItemRepository& itemRepository; + IClock& clock; + IOrderService& orderService; + domain::ItemExpirationPolicy expirationPolicy; +}; + +} // namespace nxl::autostore::application \ No newline at end of file diff --git a/cpp17/lib/src/application/interfaces/IAuthService.h b/cpp17/lib/src/application/interfaces/IAuthService.h new file mode 100644 index 0000000..3189bd1 --- /dev/null +++ b/cpp17/lib/src/application/interfaces/IAuthService.h @@ -0,0 +1,17 @@ +#pragma once + +#include +#include +#include + +namespace nxl::autostore::application { + +class IAuthService +{ +public: + virtual ~IAuthService() = default; + virtual std::string generateToken(std::string_view userId) = 0; + virtual std::optional validateToken(std::string_view token) = 0; +}; + +} // namespace nxl::autostore::application \ No newline at end of file diff --git a/cpp17/lib/src/application/interfaces/IClock.h b/cpp17/lib/src/application/interfaces/IClock.h new file mode 100644 index 0000000..21b51c1 --- /dev/null +++ b/cpp17/lib/src/application/interfaces/IClock.h @@ -0,0 +1,14 @@ +#pragma once + +#include + +namespace nxl::autostore::application { + +class IClock +{ +public: + virtual ~IClock() = default; + virtual std::chrono::system_clock::time_point getCurrentTime() const = 0; +}; + +} // namespace nxl::autostore::application \ No newline at end of file diff --git a/cpp17/lib/src/application/interfaces/IItemRepository.h b/cpp17/lib/src/application/interfaces/IItemRepository.h new file mode 100644 index 0000000..cbb312b --- /dev/null +++ b/cpp17/lib/src/application/interfaces/IItemRepository.h @@ -0,0 +1,22 @@ +#pragma once + +#include "domain/entities/Item.h" +#include +#include +#include +#include + +namespace nxl::autostore::application { + +class IItemRepository +{ +public: + virtual ~IItemRepository() = default; + virtual domain::Item::Id_t save(const domain::Item& item) = 0; + virtual std::optional findById(std::string_view id) = 0; + virtual std::vector findByUser(std::string_view userId) = 0; + virtual std::vector findAll() = 0; + virtual void remove(std::string_view id) = 0; +}; + +} // namespace nxl::autostore::application \ No newline at end of file diff --git a/cpp17/lib/src/application/interfaces/IOrderService.h b/cpp17/lib/src/application/interfaces/IOrderService.h new file mode 100644 index 0000000..2cc907a --- /dev/null +++ b/cpp17/lib/src/application/interfaces/IOrderService.h @@ -0,0 +1,14 @@ +#pragma once + +#include "domain/entities/Item.h" + +namespace nxl::autostore::application { + +class IOrderService +{ +public: + virtual ~IOrderService() = default; + virtual void orderItem(const domain::Item& item) = 0; +}; + +} // namespace nxl::autostore::application \ No newline at end of file diff --git a/cpp17/lib/src/application/interfaces/IUserRepository.h b/cpp17/lib/src/application/interfaces/IUserRepository.h new file mode 100644 index 0000000..a9cb85c --- /dev/null +++ b/cpp17/lib/src/application/interfaces/IUserRepository.h @@ -0,0 +1,23 @@ +#pragma once + +#include "domain/entities/User.h" +#include +#include +#include +#include + +namespace nxl::autostore::application { + +class IUserRepository +{ +public: + virtual ~IUserRepository() = default; + virtual void save(const domain::User& user) = 0; + virtual std::optional findById(std::string_view id) = 0; + virtual std::optional + findByUsername(std::string_view username) = 0; + virtual std::vector findAll() = 0; + virtual void remove(std::string_view id) = 0; +}; + +} // namespace nxl::autostore::application \ No newline at end of file diff --git a/cpp17/lib/src/domain/entities/Item.h b/cpp17/lib/src/domain/entities/Item.h new file mode 100644 index 0000000..b992962 --- /dev/null +++ b/cpp17/lib/src/domain/entities/Item.h @@ -0,0 +1,20 @@ +#pragma once + +#include "User.h" +#include +#include + +namespace nxl::autostore::domain { + +struct Item +{ + using Id_t = std::string; + inline const static Id_t NULL_ID{""}; + Id_t id; + std::string name; + std::chrono::system_clock::time_point expirationDate; + std::string orderUrl; + User::Id_t userId; +}; + +} // namespace nxl::autostore::domain \ No newline at end of file diff --git a/cpp17/lib/src/domain/entities/User.h b/cpp17/lib/src/domain/entities/User.h new file mode 100644 index 0000000..e515b0a --- /dev/null +++ b/cpp17/lib/src/domain/entities/User.h @@ -0,0 +1,15 @@ +#pragma once + +#include + +namespace nxl::autostore::domain { + +struct User +{ + using Id_t = std::string; + Id_t id; + std::string username; + std::string passwordHash; +}; + +} // namespace nxl::autostore::domain \ No newline at end of file diff --git a/cpp17/lib/src/domain/polices/ItemExpirationPolicy.h b/cpp17/lib/src/domain/polices/ItemExpirationPolicy.h new file mode 100644 index 0000000..5a39570 --- /dev/null +++ b/cpp17/lib/src/domain/polices/ItemExpirationPolicy.h @@ -0,0 +1,18 @@ +#pragma once + +#include "domain/entities/Item.h" +#include + +namespace nxl::autostore::domain { + +class ItemExpirationPolicy +{ +public: + bool isExpired(const Item& item, + const std::chrono::system_clock::time_point& currentTime) const + { + return item.expirationDate <= currentTime; + } +}; + +} // namespace nxl::autostore::domain \ No newline at end of file diff --git a/cpp17/lib/src/infrastructure/adapters/SystemClock.h b/cpp17/lib/src/infrastructure/adapters/SystemClock.h new file mode 100644 index 0000000..22d8cdb --- /dev/null +++ b/cpp17/lib/src/infrastructure/adapters/SystemClock.h @@ -0,0 +1,17 @@ +#pragma once + +#include "application/interfaces/IClock.h" +#include + +namespace nxl::autostore::infrastructure { + +class SystemClock : public application::IClock +{ +public: + std::chrono::system_clock::time_point getCurrentTime() const override + { + return std::chrono::system_clock::now(); + } +}; + +} // namespace nxl::autostore::infrastructure \ No newline at end of file diff --git a/cpp17/lib/src/infrastructure/helpers/Jsend.cpp b/cpp17/lib/src/infrastructure/helpers/Jsend.cpp new file mode 100644 index 0000000..cff1489 --- /dev/null +++ b/cpp17/lib/src/infrastructure/helpers/Jsend.cpp @@ -0,0 +1,32 @@ +#include "infrastructure/helpers/Jsend.h" + +namespace nxl::autostore::infrastructure { + +std::string Jsend::success(const nlohmann::json& data) +{ + nlohmann::json response; + response["status"] = "success"; + + if (!data.is_null()) { + response["data"] = data; + } + + return response.dump(); +} + +std::string Jsend::error(std::string_view message, int code, + const nlohmann::json& data) +{ + nlohmann::json response; + response["status"] = "error"; + response["message"] = message; + response["code"] = code; + + if (!data.is_null()) { + response["data"] = data; + } + + return response.dump(); +} + +} // namespace nxl::autostore::infrastructure \ No newline at end of file diff --git a/cpp17/lib/src/infrastructure/helpers/Jsend.h b/cpp17/lib/src/infrastructure/helpers/Jsend.h new file mode 100644 index 0000000..a93a496 --- /dev/null +++ b/cpp17/lib/src/infrastructure/helpers/Jsend.h @@ -0,0 +1,20 @@ +#pragma once + +#include "nlohmann/json.hpp" +#include + +namespace nxl::autostore::infrastructure { + +class Jsend +{ +public: + static std::string success(const nlohmann::json& data = nullptr); + static std::string error(std::string_view message, int code = 500, + const nlohmann::json& data = nullptr); + +private: + Jsend() = delete; + ~Jsend() = delete; +}; + +} // namespace nxl::autostore::infrastructure \ No newline at end of file diff --git a/cpp17/lib/src/infrastructure/helpers/JsonItem.cpp b/cpp17/lib/src/infrastructure/helpers/JsonItem.cpp new file mode 100644 index 0000000..568c24e --- /dev/null +++ b/cpp17/lib/src/infrastructure/helpers/JsonItem.cpp @@ -0,0 +1,76 @@ +#include "infrastructure/helpers/JsonItem.h" +#include +#include +#include +#include + +namespace nxl::autostore::infrastructure { +domain::Item JsonItem::fromJson(std::string_view jsonBody) +{ + auto json = nlohmann::json::parse(jsonBody); + return fromJsonObj(json); +} + +domain::Item JsonItem::fromJsonObj(const nlohmann::json& j) +{ + domain::Item item; + item.id = j.value("id", ""); + item.name = j.value("name", ""); + item.orderUrl = j.value("orderUrl", ""); + item.userId = j.value("userId", "default-user"); + + if (j["expirationDate"].is_number()) { + // Handle numeric timestamp + time_t timestamp = j["expirationDate"]; + item.expirationDate = std::chrono::system_clock::from_time_t(timestamp); + } else if (j["expirationDate"].is_string()) { + // Handle ISO 8601 string format + std::string dateStr = j["expirationDate"]; + std::tm tm = {}; + std::istringstream ss(dateStr); + + // Parse the ISO 8601 format + ss >> std::get_time(&tm, "%Y-%m-%dT%H:%M:%S"); + if (ss.fail()) { + throw std::runtime_error( + "Invalid format for expirationDate string. Expected ISO 8601 format " + "(YYYY-MM-DDTHH:MM:SS)."); + } + + // Convert to time_t + time_t timestamp = std::mktime(&tm); + if (timestamp == -1) { + throw std::runtime_error( + "Failed to convert expirationDate to timestamp."); + } + + item.expirationDate = std::chrono::system_clock::from_time_t(timestamp); + } else { + throw std::runtime_error("Invalid type for expirationDate. Expected number " + "(Unix timestamp) or string (ISO 8601 format)."); + } + + if (item.name.empty()) { + throw std::runtime_error("Item name is required"); + } + + return item; +} + +std::string JsonItem::toJson(const domain::Item& item) +{ + return toJsonObj(item).dump(); +} + +nlohmann::json JsonItem::toJsonObj(const domain::Item& item) +{ + nlohmann::json j; + j["id"] = item.id; + j["name"] = item.name; + j["expirationDate"] = + std::chrono::system_clock::to_time_t(item.expirationDate); + j["orderUrl"] = item.orderUrl; + j["userId"] = item.userId; + return j; +} +} // namespace nxl::autostore::infrastructure \ No newline at end of file diff --git a/cpp17/lib/src/infrastructure/helpers/JsonItem.h b/cpp17/lib/src/infrastructure/helpers/JsonItem.h new file mode 100644 index 0000000..2e4d623 --- /dev/null +++ b/cpp17/lib/src/infrastructure/helpers/JsonItem.h @@ -0,0 +1,22 @@ +#pragma once + +#include "domain/entities/Item.h" +#include "nlohmann/json.hpp" +#include + +namespace nxl::autostore::infrastructure { + +class JsonItem +{ +public: + static domain::Item fromJson(std::string_view jsonBody); + static std::string toJson(const domain::Item& item); + static nlohmann::json toJsonObj(const domain::Item& item); + static domain::Item fromJsonObj(const nlohmann::json& j); + +private: + JsonItem() = delete; + ~JsonItem() = delete; +}; + +} // namespace nxl::autostore::infrastructure \ No newline at end of file diff --git a/cpp17/lib/src/infrastructure/http/HttpOrderService.cpp b/cpp17/lib/src/infrastructure/http/HttpOrderService.cpp new file mode 100644 index 0000000..29ba304 --- /dev/null +++ b/cpp17/lib/src/infrastructure/http/HttpOrderService.cpp @@ -0,0 +1,35 @@ +#include "HttpOrderService.h" +#include + +namespace nxl::autostore::infrastructure { + +HttpOrderService::HttpOrderService(ILoggerPtr logger) : log{std::move(logger)} +{} + +void HttpOrderService::orderItem(const domain::Item& item) +{ + if (item.orderUrl.empty()) { + throw std::runtime_error("Order URL is empty for item: " + item.name); + } + + std::string payload = + R"({"itemName": ")" + item.name + R"(", "itemId": ")" + item.id + "\"}"; + sendPostRequest(item.orderUrl, payload); +} + +void HttpOrderService::sendPostRequest(std::string_view url, + std::string_view payload) +{ + // In a real implementation, this would use an HTTP client library + // For now, we'll simulate the HTTP call + log->i("POST request to: %s", url); + log->v(1, "Payload: %s", payload); + + // Simulate HTTP error handling + if (url.find("error") != std::string::npos) { + throw std::runtime_error("Failed to send order request to: " + + std::string(url)); + } +} + +} // namespace nxl::autostore::infrastructure \ No newline at end of file diff --git a/cpp17/lib/src/infrastructure/http/HttpOrderService.h b/cpp17/lib/src/infrastructure/http/HttpOrderService.h new file mode 100644 index 0000000..44af9d2 --- /dev/null +++ b/cpp17/lib/src/infrastructure/http/HttpOrderService.h @@ -0,0 +1,20 @@ +#pragma once + +#include "application/interfaces/IOrderService.h" +#include "domain/entities/Item.h" +#include "autostore/ILogger.h" + +namespace nxl::autostore::infrastructure { + +class HttpOrderService : public application::IOrderService +{ +public: + explicit HttpOrderService(ILoggerPtr logger); + void orderItem(const domain::Item& item) override; + +private: + ILoggerPtr log; + void sendPostRequest(std::string_view url, std::string_view payload); +}; + +} // namespace nxl::autostore::infrastructure \ No newline at end of file diff --git a/cpp17/lib/src/infrastructure/http/HttpServer.cpp b/cpp17/lib/src/infrastructure/http/HttpServer.cpp new file mode 100644 index 0000000..23bac48 --- /dev/null +++ b/cpp17/lib/src/infrastructure/http/HttpServer.cpp @@ -0,0 +1,126 @@ +#include "infrastructure/http/HttpServer.h" +#include +#include + +namespace nxl::autostore::infrastructure { + +namespace { +constexpr std::chrono::seconds defaultStartupTimeout{5}; +} + +HttpServer::HttpServer(ILoggerPtr logger) : log{std::move(logger)} {} + +HttpServer::~HttpServer() +{ + if (running) { + stop(); + } +} + +bool HttpServer::start(int port, std::string_view host) +{ + if (running) { + return true; + } + + // std::cout << "Starting HTTP server on " << host << ":" << port << + // std::endl; + log->info("Starting HTTP server on %s:%d...", host.data(), port); + + std::promise startupPromise; + std::future startupFuture = startupPromise.get_future(); + + serverThread = std::thread([host, port, this, &startupPromise]() { + // std::cout << "Server thread started, binding to " << host << ":" << port + // << std::endl; + log->v(1, "Server thread started, binding to %s:%d...", host, port); + + try { + // Try to bind to the port + if (!server.bind_to_port(host.data(), port)) { + startupPromise.set_value(false); + return; + } + + // Signal that the server has bound to the port + startupPromise.set_value(true); + + // std::cout << "Server thread listening on " << host << ":" << port + // << std::endl; + log->info("Server thread listening on %s:%d", host, port); + + // Start listening for connections + bool listenResult = server.listen_after_bind(); + // std::cout << "Server stopped, listen result: " << listenResult + // << std::endl; + log->info("Server stopped, listen result: %d", listenResult); + } catch (const std::exception& e) { + // std::cerr << "Server thread exception: " << e.what() << std::endl; + log->error("Server thread exception: %s", e.what()); + startupPromise.set_exception(std::current_exception()); + } catch (...) { + // std::cerr << "Server thread unknown exception" << std::endl; + log->error("Server thread unknown exception"); + startupPromise.set_exception(std::current_exception()); + } + }); + + // Wait for the server to start (with a timeout) + if (startupFuture.wait_for(defaultStartupTimeout) + == std::future_status::timeout) { + // std::cerr << "Failed to start HTTP server - timeout" << std::endl; + log->error("Failed to start HTTP server - timeout"); + return false; + } + + try { + // Check if the server bound to the port successfully + if (!startupFuture.get()) { + // std::cerr << "Failed to start HTTP server - could not bind to port" + // << std::endl; + log->error("Failed to start HTTP server - could not bind to port"); + return false; + } + } catch (const std::exception& e) { + // std::cerr << "Failed to start HTTP server - " << e.what() << std::endl; + log->error("Failed to start HTTP server - %s", e.what()); + return false; + } + + running = true; + // std::cout << "HTTP server is running" << std::endl; + log->info("HTTP server is running"); + return true; +} + +void HttpServer::stop() +{ + if (!running) { + return; + } + + // std::cout << "Stopping HTTP server..." << std::endl; + log->info("Stopping HTTP server..."); + server.stop(); + if (serverThread.joinable()) { + // std::cout << "Waiting for server thread to join..." << std::endl; + log->info("Waiting for server thread to join..."); + serverThread.join(); + } + + running = false; + // std::cout << "HTTP server stopped" << std::endl; + log->info("HTTP server stopped"); +} + +bool HttpServer::isRunning() const +{ + return running; +} + +httplib::Server& HttpServer::getServer() +{ + return server; +} + +} // namespace nxl::autostore::infrastructure diff --git a/cpp17/lib/src/infrastructure/http/HttpServer.h b/cpp17/lib/src/infrastructure/http/HttpServer.h new file mode 100644 index 0000000..83f1516 --- /dev/null +++ b/cpp17/lib/src/infrastructure/http/HttpServer.h @@ -0,0 +1,30 @@ +#pragma once + +#include "autostore/ILogger.h" +#include +#include +#include +#include + +namespace nxl::autostore::infrastructure { + +class HttpServer +{ +public: + explicit HttpServer(ILoggerPtr logger); + ~HttpServer(); + + bool start(int port = 8080, std::string_view host = "0.0.0.0"); + void stop(); + bool isRunning() const; + + httplib::Server& getServer(); + +private: + ILoggerPtr log; + bool running{false}; + httplib::Server server; + std::thread serverThread; +}; + +} // namespace nxl::autostore::infrastructure \ No newline at end of file diff --git a/cpp17/lib/src/infrastructure/repositories/FileItemRepository.cpp b/cpp17/lib/src/infrastructure/repositories/FileItemRepository.cpp new file mode 100644 index 0000000..b52891f --- /dev/null +++ b/cpp17/lib/src/infrastructure/repositories/FileItemRepository.cpp @@ -0,0 +1,119 @@ +#include "infrastructure/repositories/FileItemRepository.h" +#include "infrastructure/helpers/JsonItem.h" +#include +#include +#include +#include +#include + +namespace nxl::autostore::infrastructure { + +namespace { + +// Helper functions for vector serialization +inline nlohmann::json itemsToJson(const std::vector& items) +{ + nlohmann::json j = nlohmann::json::array(); + for (const auto& item : items) { + j.push_back(infrastructure::JsonItem::toJsonObj(item)); + } + return j; +} + +inline std::vector jsonToItems(const nlohmann::json& j) +{ + std::vector items; + for (const auto& itemJson : j) { + items.push_back(infrastructure::JsonItem::fromJsonObj(itemJson)); + } + return items; +} + +} // namespace + +FileItemRepository::FileItemRepository(std::string_view dbPath) : dbPath(dbPath) +{ + load(); +} + +domain::Item::Id_t FileItemRepository::save(const domain::Item& item) +{ + std::lock_guard lock(mtx); + domain::Item::Id_t id = item.id; + auto it = + std::find_if(items.begin(), items.end(), + [&](const domain::Item& i) { return i.id == item.id; }); + + if (it != items.end()) { + *it = item; + } else { + domain::Item newItem{item}; + newItem.id = "item-" + + std::to_string( + std::chrono::system_clock::now().time_since_epoch().count()); + items.push_back(newItem); + id = newItem.id; + } + persist(); + + return id; +} + +std::optional FileItemRepository::findById(std::string_view id) +{ + std::lock_guard lock(mtx); + auto it = std::find_if(items.begin(), items.end(), + [&](const domain::Item& i) { return i.id == id; }); + + if (it != items.end()) { + return *it; + } + return std::nullopt; +} + +std::vector +FileItemRepository::findByUser(std::string_view userId) +{ + std::lock_guard lock(mtx); + std::vector userItems; + std::copy_if(items.begin(), items.end(), std::back_inserter(userItems), + [&](const domain::Item& i) { return i.userId == userId; }); + return userItems; +} + +std::vector FileItemRepository::findAll() +{ + std::lock_guard lock(mtx); + return items; +} + +void FileItemRepository::remove(std::string_view id) +{ + std::lock_guard lock(mtx); + items.erase(std::remove_if(items.begin(), items.end(), + [&](const domain::Item& i) { return i.id == id; }), + items.end()); + persist(); +} + +void FileItemRepository::load() +{ + std::lock_guard lock(mtx); + std::ifstream file(dbPath); + if (file.is_open()) { + nlohmann::json j; + file >> j; + items = jsonToItems(j); + } +} + +void FileItemRepository::persist() +{ + std::ofstream file(dbPath); + if (file.is_open()) { + nlohmann::json j = itemsToJson(items); + file << j.dump(4); + } +} + +} // namespace nxl::autostore::infrastructure \ No newline at end of file diff --git a/cpp17/lib/src/infrastructure/repositories/FileItemRepository.h b/cpp17/lib/src/infrastructure/repositories/FileItemRepository.h new file mode 100644 index 0000000..9f27aaa --- /dev/null +++ b/cpp17/lib/src/infrastructure/repositories/FileItemRepository.h @@ -0,0 +1,29 @@ +#pragma once + +#include "application/interfaces/IItemRepository.h" +#include +#include +#include + +namespace nxl::autostore::infrastructure { + +class FileItemRepository : public application::IItemRepository +{ +public: + explicit FileItemRepository(std::string_view dbPath); + domain::Item::Id_t save(const domain::Item& item) override; + std::optional findById(std::string_view id) override; + std::vector findByUser(std::string_view userId) override; + std::vector findAll() override; + void remove(std::string_view id) override; + +private: + void load(); + void persist(); + + std::string dbPath; + std::vector items; + std::mutex mtx; +}; + +} // namespace nxl::autostore::infrastructure \ No newline at end of file diff --git a/cpp17/lib/src/infrastructure/repositories/FileUserRepository.cpp b/cpp17/lib/src/infrastructure/repositories/FileUserRepository.cpp new file mode 100644 index 0000000..18c5f8a --- /dev/null +++ b/cpp17/lib/src/infrastructure/repositories/FileUserRepository.cpp @@ -0,0 +1,130 @@ +#include "infrastructure/repositories/FileUserRepository.h" +#include "nlohmann/json.hpp" +#include +#include + +namespace nxl::autostore::infrastructure { + +namespace { + +// Helper functions for JSON serialization +inline void userToJson(nlohmann::json& j, const domain::User& u) +{ + j = nlohmann::json{ + {"id", u.id}, {"username", u.username}, {"passwordHash", u.passwordHash}}; +} + +inline void jsonToUser(const nlohmann::json& j, domain::User& u) +{ + j.at("id").get_to(u.id); + j.at("username").get_to(u.username); + j.at("passwordHash").get_to(u.passwordHash); +} + +// Helper functions for vector serialization +inline nlohmann::json usersToJson(const std::vector& users) +{ + nlohmann::json j = nlohmann::json::array(); + for (const auto& user : users) { + nlohmann::json userJson; + userToJson(userJson, user); + j.push_back(userJson); + } + return j; +} + +inline std::vector jsonToUsers(const nlohmann::json& j) +{ + std::vector users; + for (const auto& userJson : j) { + domain::User user; + jsonToUser(userJson, user); + users.push_back(user); + } + return users; +} + +} // namespace + +FileUserRepository::FileUserRepository(std::string_view dbPath) : dbPath(dbPath) +{ + load(); +} + +void FileUserRepository::save(const domain::User& user) +{ + std::lock_guard lock(mtx); + auto it = + std::find_if(users.begin(), users.end(), + [&](const domain::User& u) { return u.id == user.id; }); + + if (it != users.end()) { + *it = user; + } else { + users.push_back(user); + } + persist(); +} + +std::optional FileUserRepository::findById(std::string_view id) +{ + std::lock_guard lock(mtx); + auto it = std::find_if(users.begin(), users.end(), + [&](const domain::User& u) { return u.id == id; }); + + if (it != users.end()) { + return *it; + } + return std::nullopt; +} + +std::optional +FileUserRepository::findByUsername(std::string_view username) +{ + std::lock_guard lock(mtx); + auto it = + std::find_if(users.begin(), users.end(), + [&](const domain::User& u) { return u.username == username; }); + + if (it != users.end()) { + return *it; + } + return std::nullopt; +} + +std::vector FileUserRepository::findAll() +{ + std::lock_guard lock(mtx); + return users; +} + +void FileUserRepository::remove(std::string_view id) +{ + std::lock_guard lock(mtx); + users.erase(std::remove_if(users.begin(), users.end(), + [&](const domain::User& u) { return u.id == id; }), + users.end()); + persist(); +} + +void FileUserRepository::load() +{ + std::lock_guard lock(mtx); + std::ifstream file(dbPath); + if (file.is_open()) { + nlohmann::json j; + file >> j; + users = jsonToUsers(j); + } +} + +void FileUserRepository::persist() +{ + std::ofstream file(dbPath); + if (file.is_open()) { + nlohmann::json j = usersToJson(users); + file << j.dump(4); + } +} + +} // namespace nxl::autostore::infrastructure \ No newline at end of file diff --git a/cpp17/lib/src/infrastructure/repositories/FileUserRepository.h b/cpp17/lib/src/infrastructure/repositories/FileUserRepository.h new file mode 100644 index 0000000..5edd987 --- /dev/null +++ b/cpp17/lib/src/infrastructure/repositories/FileUserRepository.h @@ -0,0 +1,30 @@ +#pragma once + +#include "application/interfaces/IUserRepository.h" +#include +#include +#include + +namespace nxl::autostore::infrastructure { + +class FileUserRepository : public application::IUserRepository +{ +public: + explicit FileUserRepository(std::string_view dbPath); + void save(const domain::User& user) override; + std::optional findById(std::string_view id) override; + std::optional + findByUsername(std::string_view username) override; + std::vector findAll() override; + void remove(std::string_view id) override; + +private: + void load(); + void persist(); + + std::string dbPath; + std::vector users; + std::mutex mtx; +}; + +} // namespace nxl::autostore::infrastructure \ No newline at end of file diff --git a/cpp17/lib/src/webapi/controllers/StoreController.cpp b/cpp17/lib/src/webapi/controllers/StoreController.cpp new file mode 100644 index 0000000..1605a21 --- /dev/null +++ b/cpp17/lib/src/webapi/controllers/StoreController.cpp @@ -0,0 +1,53 @@ +#include "webapi/controllers/StoreController.h" +#include "infrastructure/helpers/JsonItem.h" +#include "infrastructure/helpers/Jsend.h" +#include "application/commands/AddItem.h" + +namespace nxl::autostore::webapi { + +using infrastructure::Jsend; +using infrastructure::JsonItem; + +StoreController::StoreController(Context&& context) + : context{std::move(context)} +{} + +void StoreController::registerRoutes(httplib::Server& server) +{ + server.Post("/api/items", + [this](const httplib::Request& req, httplib::Response& res) { + this->addItem(req, res); + }); +} + +void StoreController::addItem(const httplib::Request& req, + httplib::Response& res) +{ + try { + if (req.body.empty()) { + res.status = 400; + res.set_content(Jsend::error("Request body is empty", 400), + "application/json"); + return; + } + + auto item = JsonItem::fromJson(req.body); + try { + nlohmann::json responseData = nlohmann::json::object(); + responseData["id"] = context.addItemUc.execute(std::move(item)); + res.status = 201; + res.set_content(Jsend::success(responseData), "application/json"); + } catch (const std::exception& e) { + res.status = 500; + res.set_content( + Jsend::error("Failed to add item: " + std::string(e.what()), + res.status), + "application/json"); + } + } catch (const std::exception& e) { + res.status = 400; + res.set_content(Jsend::error(e.what(), res.status), "application/json"); + } +} + +} // namespace nxl::autostore::webapi \ No newline at end of file diff --git a/cpp17/lib/src/webapi/controllers/StoreController.h b/cpp17/lib/src/webapi/controllers/StoreController.h new file mode 100644 index 0000000..772330f --- /dev/null +++ b/cpp17/lib/src/webapi/controllers/StoreController.h @@ -0,0 +1,27 @@ +#pragma once + +#include "application/commands/AddItem.h" +#include // TODO: forward declaration + +namespace nxl::autostore::webapi { + +class StoreController +{ +public: + struct Context + { + application::AddItem addItemUc; + }; + + StoreController(Context&& context); + + void registerRoutes(httplib::Server& server); + +private: + void addItem(const httplib::Request& req, httplib::Response& res); + +private: + Context context; +}; + +} // namespace nxl::autostore::webapi \ No newline at end of file diff --git a/cpp17/tests/CMakeLists.txt b/cpp17/tests/CMakeLists.txt new file mode 100644 index 0000000..b5c34a9 --- /dev/null +++ b/cpp17/tests/CMakeLists.txt @@ -0,0 +1,31 @@ +cmake_minimum_required(VERSION 3.20) + +enable_testing() + +find_package(Catch2 CONFIG REQUIRED) + +# Macro to create a test executable +function(add_integration_test TEST_NAME SOURCE_FILE) + add_executable(${TEST_NAME} + ${SOURCE_FILE} + ) + + target_link_libraries(${TEST_NAME} + PRIVATE + AutoStoreLib + Catch2::Catch2WithMain + ) + + target_include_directories(${TEST_NAME} + PRIVATE + ${PROJECT_SOURCE_DIR}/lib/include + ${PROJECT_SOURCE_DIR}/lib/src + ) + + # Add test to CTest + add_test(NAME ${TEST_NAME} COMMAND ${TEST_NAME}) +endfunction() + +# Create test executables +add_integration_test(FileUserRepositoryTest integration/FileUserRepository.test.cpp) +add_integration_test(FileItemRepositoryTest integration/FileItemRepository.test.cpp) \ No newline at end of file diff --git a/cpp17/tests/integration/FileItemRepository.test.cpp b/cpp17/tests/integration/FileItemRepository.test.cpp new file mode 100644 index 0000000..3585960 --- /dev/null +++ b/cpp17/tests/integration/FileItemRepository.test.cpp @@ -0,0 +1,365 @@ +#include "infrastructure/repositories/FileItemRepository.h" +#include "domain/entities/Item.h" +#include +#include +#include +#include +#include +#include + +using namespace nxl::autostore; +using Catch::Matchers::Equals; + +namespace Test { +constexpr const char* TEST_ITEM_ID_1 = "item123"; +constexpr const char* TEST_ITEM_ID_2 = "item456"; +constexpr const char* TEST_ITEM_NAME_1 = "testitem"; +constexpr const char* TEST_ITEM_NAME_2 = "anotheritem"; +constexpr const char* TEST_ORDER_URL_1 = "https://example.com/order1"; +constexpr const char* TEST_ORDER_URL_2 = "https://example.com/order2"; +constexpr const char* TEST_USER_ID_1 = "user123"; +constexpr const char* TEST_USER_ID_2 = "user456"; +constexpr const char* NON_EXISTENT_ID = "nonexistent"; +constexpr const char* NON_EXISTENT_USER_ID = "nonexistentuser"; +constexpr const char* TEST_DIR_NAME = "autostore_test"; +constexpr const char* TEST_DB_FILE_NAME = "test_items.json"; + +// Helper function to create a test item with default values +domain::Item +createTestItem(const std::string& id = TEST_ITEM_ID_1, + const std::string& name = TEST_ITEM_NAME_1, + const std::string& orderUrl = TEST_ORDER_URL_1, + const std::string& userId = TEST_USER_ID_1, + const std::chrono::system_clock::time_point& expirationDate = + std::chrono::system_clock::now() + std::chrono::hours(24)) +{ + domain::Item item; + item.id = id; + item.name = name; + item.orderUrl = orderUrl; + item.userId = userId; + item.expirationDate = expirationDate; + return item; +} + +// Helper function to create a second test item +domain::Item createSecondTestItem() +{ + return createTestItem( + TEST_ITEM_ID_2, TEST_ITEM_NAME_2, TEST_ORDER_URL_2, TEST_USER_ID_2, + std::chrono::system_clock::now() + std::chrono::hours(48)); +} + +// Helper function to set up test environment +std::string setupTestEnvironment() +{ + std::filesystem::path testDir = + std::filesystem::temp_directory_path() / TEST_DIR_NAME; + std::filesystem::create_directories(testDir); + std::string testDbPath = (testDir / TEST_DB_FILE_NAME).string(); + + // Clean up any existing test file + if (std::filesystem::exists(testDbPath)) { + std::filesystem::remove(testDbPath); + } + + return testDbPath; +} + +// Helper function to clean up test environment +void cleanupTestEnvironment() +{ + std::filesystem::path testDir = + std::filesystem::temp_directory_path() / TEST_DIR_NAME; + if (std::filesystem::exists(testDir)) { + std::filesystem::remove_all(testDir); + } +} + +// Helper function to verify item properties match expected values +void verifyItemProperties(const domain::Item& item, + const std::string& expectedId, + const std::string& expectedName, + const std::string& expectedOrderUrl, + const std::string& expectedUserId) +{ + REQUIRE(item.id == expectedId); + REQUIRE(item.name == expectedName); + REQUIRE(item.orderUrl == expectedOrderUrl); + REQUIRE(item.userId == expectedUserId); +} + +// Helper function to verify item properties match default test item values +void verifyDefaultTestItem(const domain::Item& item) +{ + verifyItemProperties(item, TEST_ITEM_ID_1, TEST_ITEM_NAME_1, TEST_ORDER_URL_1, + TEST_USER_ID_1); +} + +// Helper function to verify item properties match second test item values +void verifySecondTestItem(const domain::Item& item) +{ + verifyItemProperties(item, TEST_ITEM_ID_2, TEST_ITEM_NAME_2, TEST_ORDER_URL_2, + TEST_USER_ID_2); +} +} // namespace Test + +TEST_CASE("FileItemRepository Integration Tests", + "[integration][FileItemRepository]") +{ + // Setup test environment + std::string testDbPath = Test::setupTestEnvironment(); + + SECTION("when a new item is saved then it can be found by id") + { + // Given + infrastructure::FileItemRepository repository(testDbPath); + domain::Item testItem = Test::createTestItem(); + + // When + repository.save(testItem); + + // Then + auto foundItem = repository.findById(Test::TEST_ITEM_ID_1); + REQUIRE(foundItem.has_value()); + Test::verifyDefaultTestItem(*foundItem); + } + + SECTION("when a new item is saved then it can be found by user") + { + // Given + infrastructure::FileItemRepository repository(testDbPath); + domain::Item testItem = Test::createTestItem(); + + // When + repository.save(testItem); + + // Then + auto userItems = repository.findByUser(Test::TEST_USER_ID_1); + REQUIRE(userItems.size() == 1); + Test::verifyDefaultTestItem(userItems[0]); + } + + SECTION("when multiple items are saved then findAll returns all items") + { + // Given + infrastructure::FileItemRepository repository(testDbPath); + domain::Item firstItem = Test::createTestItem(); + domain::Item secondItem = Test::createSecondTestItem(); + + // When + repository.save(firstItem); + repository.save(secondItem); + + // Then + auto allItems = repository.findAll(); + REQUIRE(allItems.size() == 2); + + // Verify both items are present (order doesn't matter) + bool foundFirst = false; + bool foundSecond = false; + + for (const auto& item : allItems) { + if (item.id == Test::TEST_ITEM_ID_1) { + Test::verifyDefaultTestItem(item); + foundFirst = true; + } else if (item.id == Test::TEST_ITEM_ID_2) { + Test::verifySecondTestItem(item); + foundSecond = true; + } + } + + REQUIRE(foundFirst); + REQUIRE(foundSecond); + } + + SECTION("when multiple items for same user are saved then findByUser returns " + "all user items") + { + // Given + infrastructure::FileItemRepository repository(testDbPath); + domain::Item firstItem = Test::createTestItem(); + domain::Item secondItem = + Test::createTestItem("item789", "thirditem", "https://example.com/order3", + Test::TEST_USER_ID_1); + + // When + repository.save(firstItem); + repository.save(secondItem); + + // Then + auto userItems = repository.findByUser(Test::TEST_USER_ID_1); + REQUIRE(userItems.size() == 2); + + // Verify both items are present (order doesn't matter) + bool foundFirst = false; + bool foundSecond = false; + + for (const auto& item : userItems) { + if (item.id == Test::TEST_ITEM_ID_1) { + Test::verifyDefaultTestItem(item); + foundFirst = true; + } else if (item.id == "item789") { + REQUIRE(item.name == "thirditem"); + REQUIRE(item.orderUrl == "https://example.com/order3"); + REQUIRE(item.userId == Test::TEST_USER_ID_1); + foundSecond = true; + } + } + + REQUIRE(foundFirst); + REQUIRE(foundSecond); + } + + SECTION("when an existing item is saved then it is updated") + { + // Given + infrastructure::FileItemRepository repository(testDbPath); + domain::Item testItem = Test::createTestItem(); + repository.save(testItem); + + // When + testItem.name = "updateditemname"; + testItem.orderUrl = "https://updated.example.com/order"; + testItem.userId = Test::TEST_USER_ID_2; + repository.save(testItem); + + // Then + auto foundItem = repository.findById(Test::TEST_ITEM_ID_1); + REQUIRE(foundItem.has_value()); + REQUIRE(foundItem->id == Test::TEST_ITEM_ID_1); + REQUIRE(foundItem->name == "updateditemname"); + REQUIRE(foundItem->orderUrl == "https://updated.example.com/order"); + REQUIRE(foundItem->userId == Test::TEST_USER_ID_2); + } + + SECTION("when an item is removed then it cannot be found by id") + { + // Given + infrastructure::FileItemRepository repository(testDbPath); + domain::Item testItem = Test::createTestItem(); + repository.save(testItem); + + // When + repository.remove(Test::TEST_ITEM_ID_1); + + // Then + auto foundItem = repository.findById(Test::TEST_ITEM_ID_1); + REQUIRE_FALSE(foundItem.has_value()); + } + + SECTION("when an item is removed then it is not in findByUser") + { + // Given + infrastructure::FileItemRepository repository(testDbPath); + domain::Item testItem = Test::createTestItem(); + repository.save(testItem); + + // When + repository.remove(Test::TEST_ITEM_ID_1); + + // Then + auto userItems = repository.findByUser(Test::TEST_USER_ID_1); + REQUIRE(userItems.empty()); + } + + SECTION("when an item is removed then it is not in findAll") + { + // Given + infrastructure::FileItemRepository repository(testDbPath); + domain::Item firstItem = Test::createTestItem(); + domain::Item secondItem = Test::createSecondTestItem(); + repository.save(firstItem); + repository.save(secondItem); + + // When + repository.remove(Test::TEST_ITEM_ID_1); + + // Then + auto allItems = repository.findAll(); + REQUIRE(allItems.size() == 1); + Test::verifySecondTestItem(allItems[0]); + } + + SECTION( + "when findById is called with non-existent id then it returns nullopt") + { + // Given + infrastructure::FileItemRepository repository(testDbPath); + + // When + auto foundItem = repository.findById(Test::NON_EXISTENT_ID); + + // Then + REQUIRE_FALSE(foundItem.has_value()); + } + + SECTION("when findByUser is called with non-existent user id then it returns " + "empty vector") + { + // Given + infrastructure::FileItemRepository repository(testDbPath); + domain::Item testItem = Test::createTestItem(); + repository.save(testItem); + + // When + auto userItems = repository.findByUser(Test::NON_EXISTENT_USER_ID); + + // Then + REQUIRE(userItems.empty()); + } + + SECTION("when remove is called with non-existent id then it does nothing") + { + // Given + infrastructure::FileItemRepository repository(testDbPath); + domain::Item testItem = Test::createTestItem(); + repository.save(testItem); + + // When + repository.remove(Test::NON_EXISTENT_ID); + + // Then + auto allItems = repository.findAll(); + REQUIRE(allItems.size() == 1); + Test::verifyDefaultTestItem(allItems[0]); + } + + SECTION( + "when repository is created with existing data file then it loads the data") + { + // Given + { + infrastructure::FileItemRepository firstRepository(testDbPath); + domain::Item testItem = Test::createTestItem(); + firstRepository.save(testItem); + } + + // When + infrastructure::FileItemRepository secondRepository(testDbPath); + + // Then + auto foundItem = secondRepository.findById(Test::TEST_ITEM_ID_1); + REQUIRE(foundItem.has_value()); + Test::verifyDefaultTestItem(*foundItem); + } + + SECTION("when repository is created with non-existent data file then it " + "starts empty") + { + // Given + std::filesystem::path testDir = + std::filesystem::temp_directory_path() / Test::TEST_DIR_NAME; + std::string nonExistentDbPath = (testDir / "nonexistent.json").string(); + + // When + infrastructure::FileItemRepository repository(nonExistentDbPath); + + // Then + auto allItems = repository.findAll(); + REQUIRE(allItems.empty()); + } + + // Clean up test environment + Test::cleanupTestEnvironment(); +} \ No newline at end of file diff --git a/cpp17/tests/integration/FileUserRepository.test.cpp b/cpp17/tests/integration/FileUserRepository.test.cpp new file mode 100644 index 0000000..b33d2cf --- /dev/null +++ b/cpp17/tests/integration/FileUserRepository.test.cpp @@ -0,0 +1,312 @@ +#include "infrastructure/repositories/FileUserRepository.h" +#include "domain/entities/User.h" +#include +#include +#include +#include +#include + +using namespace nxl::autostore; +using Catch::Matchers::Equals; + +namespace Test { +// Constants for magic strings and numbers +constexpr const char* TEST_USER_ID_1 = "user123"; +constexpr const char* TEST_USER_ID_2 = "user456"; +constexpr const char* TEST_USERNAME_1 = "testuser"; +constexpr const char* TEST_USERNAME_2 = "anotheruser"; +constexpr const char* TEST_PASSWORD_HASH_1 = "hashedpassword123"; +constexpr const char* TEST_PASSWORD_HASH_2 = "hashedpassword456"; +constexpr const char* NON_EXISTENT_ID = "nonexistent"; +constexpr const char* NON_EXISTENT_USERNAME = "nonexistentuser"; +constexpr const char* TEST_DIR_NAME = "autostore_test"; +constexpr const char* TEST_DB_FILE_NAME = "test_users.json"; + +// Helper function to create a test user with default values +domain::User +createTestUser(const std::string& id = TEST_USER_ID_1, + const std::string& username = TEST_USERNAME_1, + const std::string& passwordHash = TEST_PASSWORD_HASH_1) +{ + domain::User user; + user.id = id; + user.username = username; + user.passwordHash = passwordHash; + return user; +} + +// Helper function to create a second test user +domain::User createSecondTestUser() +{ + return createTestUser(TEST_USER_ID_2, TEST_USERNAME_2, TEST_PASSWORD_HASH_2); +} + +// Helper function to set up test environment +std::string setupTestEnvironment() +{ + std::filesystem::path testDir = + std::filesystem::temp_directory_path() / TEST_DIR_NAME; + std::filesystem::create_directories(testDir); + std::string testDbPath = (testDir / TEST_DB_FILE_NAME).string(); + + // Clean up any existing test file + if (std::filesystem::exists(testDbPath)) { + std::filesystem::remove(testDbPath); + } + + return testDbPath; +} + +// Helper function to clean up test environment +void cleanupTestEnvironment() +{ + std::filesystem::path testDir = + std::filesystem::temp_directory_path() / TEST_DIR_NAME; + if (std::filesystem::exists(testDir)) { + std::filesystem::remove_all(testDir); + } +} + +// Helper function to verify user properties match expected values +void verifyUserProperties(const domain::User& user, + const std::string& expectedId, + const std::string& expectedUsername, + const std::string& expectedPasswordHash) +{ + REQUIRE(user.id == expectedId); + REQUIRE(user.username == expectedUsername); + REQUIRE(user.passwordHash == expectedPasswordHash); +} + +// Helper function to verify user properties match default test user values +void verifyDefaultTestUser(const domain::User& user) +{ + verifyUserProperties(user, TEST_USER_ID_1, TEST_USERNAME_1, + TEST_PASSWORD_HASH_1); +} + +// Helper function to verify user properties match second test user values +void verifySecondTestUser(const domain::User& user) +{ + verifyUserProperties(user, TEST_USER_ID_2, TEST_USERNAME_2, + TEST_PASSWORD_HASH_2); +} +} // namespace Test + +TEST_CASE("FileUserRepository Integration Tests", + "[integration][FileUserRepository]") +{ + // Setup test environment + std::string testDbPath = Test::setupTestEnvironment(); + + SECTION("when a new user is saved then it can be found by id") + { + // Given + infrastructure::FileUserRepository repository(testDbPath); + domain::User testUser = Test::createTestUser(); + + // When + repository.save(testUser); + + // Then + auto foundUser = repository.findById(Test::TEST_USER_ID_1); + REQUIRE(foundUser.has_value()); + Test::verifyDefaultTestUser(*foundUser); + } + + SECTION("when a new user is saved then it can be found by username") + { + // Given + infrastructure::FileUserRepository repository(testDbPath); + domain::User testUser = Test::createTestUser(); + + // When + repository.save(testUser); + + // Then + auto foundUser = repository.findByUsername(Test::TEST_USERNAME_1); + REQUIRE(foundUser.has_value()); + Test::verifyDefaultTestUser(*foundUser); + } + + SECTION("when multiple users are saved then findAll returns all users") + { + // Given + infrastructure::FileUserRepository repository(testDbPath); + domain::User firstUser = Test::createTestUser(); + domain::User secondUser = Test::createSecondTestUser(); + + // When + repository.save(firstUser); + repository.save(secondUser); + + // Then + auto allUsers = repository.findAll(); + REQUIRE(allUsers.size() == 2); + + // Verify both users are present (order doesn't matter) + bool foundFirst = false; + bool foundSecond = false; + + for (const auto& user : allUsers) { + if (user.id == Test::TEST_USER_ID_1) { + Test::verifyDefaultTestUser(user); + foundFirst = true; + } else if (user.id == Test::TEST_USER_ID_2) { + Test::verifySecondTestUser(user); + foundSecond = true; + } + } + + REQUIRE(foundFirst); + REQUIRE(foundSecond); + } + + SECTION("when an existing user is saved then it is updated") + { + // Given + infrastructure::FileUserRepository repository(testDbPath); + domain::User testUser = Test::createTestUser(); + repository.save(testUser); + + // When + testUser.username = "updatedusername"; + testUser.passwordHash = "updatedpasswordhash"; + repository.save(testUser); + + // Then + auto foundUser = repository.findById(Test::TEST_USER_ID_1); + REQUIRE(foundUser.has_value()); + REQUIRE(foundUser->id == Test::TEST_USER_ID_1); + REQUIRE(foundUser->username == "updatedusername"); + REQUIRE(foundUser->passwordHash == "updatedpasswordhash"); + } + + SECTION("when a user is removed then it cannot be found by id") + { + // Given + infrastructure::FileUserRepository repository(testDbPath); + domain::User testUser = Test::createTestUser(); + repository.save(testUser); + + // When + repository.remove(Test::TEST_USER_ID_1); + + // Then + auto foundUser = repository.findById(Test::TEST_USER_ID_1); + REQUIRE_FALSE(foundUser.has_value()); + } + + SECTION("when a user is removed then it cannot be found by username") + { + // Given + infrastructure::FileUserRepository repository(testDbPath); + domain::User testUser = Test::createTestUser(); + repository.save(testUser); + + // When + repository.remove(Test::TEST_USER_ID_1); + + // Then + auto foundUser = repository.findByUsername(Test::TEST_USERNAME_1); + REQUIRE_FALSE(foundUser.has_value()); + } + + SECTION("when a user is removed then it is not in findAll") + { + // Given + infrastructure::FileUserRepository repository(testDbPath); + domain::User firstUser = Test::createTestUser(); + domain::User secondUser = Test::createSecondTestUser(); + repository.save(firstUser); + repository.save(secondUser); + + // When + repository.remove(Test::TEST_USER_ID_1); + + // Then + auto allUsers = repository.findAll(); + REQUIRE(allUsers.size() == 1); + Test::verifySecondTestUser(allUsers[0]); + } + + SECTION( + "when findById is called with non-existent id then it returns nullopt") + { + // Given + infrastructure::FileUserRepository repository(testDbPath); + + // When + auto foundUser = repository.findById(Test::NON_EXISTENT_ID); + + // Then + REQUIRE_FALSE(foundUser.has_value()); + } + + SECTION("when findByUsername is called with non-existent username then it " + "returns nullopt") + { + // Given + infrastructure::FileUserRepository repository(testDbPath); + + // When + auto foundUser = repository.findByUsername(Test::NON_EXISTENT_USERNAME); + + // Then + REQUIRE_FALSE(foundUser.has_value()); + } + + SECTION("when remove is called with non-existent id then it does nothing") + { + // Given + infrastructure::FileUserRepository repository(testDbPath); + domain::User testUser = Test::createTestUser(); + repository.save(testUser); + + // When + repository.remove(Test::NON_EXISTENT_ID); + + // Then + auto allUsers = repository.findAll(); + REQUIRE(allUsers.size() == 1); + Test::verifyDefaultTestUser(allUsers[0]); + } + + SECTION( + "when repository is created with existing data file then it loads the data") + { + // Given + { + infrastructure::FileUserRepository firstRepository(testDbPath); + domain::User testUser = Test::createTestUser(); + firstRepository.save(testUser); + } + + // When + infrastructure::FileUserRepository secondRepository(testDbPath); + + // Then + auto foundUser = secondRepository.findById(Test::TEST_USER_ID_1); + REQUIRE(foundUser.has_value()); + Test::verifyDefaultTestUser(*foundUser); + } + + SECTION("when repository is created with non-existent data file then it " + "starts empty") + { + // Given + std::filesystem::path testDir = + std::filesystem::temp_directory_path() / Test::TEST_DIR_NAME; + std::string nonExistentDbPath = (testDir / "nonexistent.json").string(); + + // When + infrastructure::FileUserRepository repository(nonExistentDbPath); + + // Then + auto allUsers = repository.findAll(); + REQUIRE(allUsers.empty()); + } + + // Clean up test environment + Test::cleanupTestEnvironment(); +} \ No newline at end of file diff --git a/cpp17/vcpkg.json b/cpp17/vcpkg.json new file mode 100644 index 0000000..97781ea --- /dev/null +++ b/cpp17/vcpkg.json @@ -0,0 +1,10 @@ +{ + "name": "autostore", + "version-string": "1.0.0", + "dependencies": [ + "cpp-httplib", + "nlohmann-json", + "spdlog", + "catch2" + ] +} diff --git a/openapi.yaml b/openapi.yaml new file mode 100644 index 0000000..a76e96a --- /dev/null +++ b/openapi.yaml @@ -0,0 +1,564 @@ +openapi: 3.0.3 +info: + title: AutoStore API + description: API for the AutoStore system - a system to store items with expiration dates that automatically orders new items when they expire. + version: 1.0.0 +servers: + - url: http://localhost:3000/api/v1 + description: Development server +paths: + /register: + post: + summary: Register a new user + description: Creates a new user account and returns a JWT token + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - username + - password + properties: + username: + type: string + description: User's username or email + password: + type: string + description: User's password + responses: + '201': + description: User successfully registered + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/JsendSuccess' + - type: object + properties: + data: + type: object + properties: + user: + $ref: '#/components/schemas/User' + token: + type: string + description: JWT token for authentication + '400': + description: Invalid input + content: + application/json: + schema: + $ref: '#/components/schemas/JsendError' + '409': + description: Username already exists + content: + application/json: + schema: + $ref: '#/components/schemas/JsendError' + + /login: + post: + summary: User login + description: Authenticates a user and returns a JWT token + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - username + - password + properties: + username: + type: string + description: User's username or email + password: + type: string + description: User's password + responses: + '200': + description: Login successful + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/JsendSuccess' + - type: object + properties: + data: + type: object + properties: + user: + $ref: '#/components/schemas/User' + token: + type: string + description: JWT token for authentication + '401': + description: Invalid credentials + content: + application/json: + schema: + $ref: '#/components/schemas/JsendError' + + /users: + get: + summary: Get list of users + description: Returns a list of all users (requires authentication) + security: + - bearerAuth: [] + responses: + '200': + description: List of users + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/JsendSuccess' + - type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/User' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/JsendError' + + /users/{id}: + get: + summary: Get user by ID + description: Returns a specific user by their ID (requires authentication) + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + description: User ID + schema: + type: string + responses: + '200': + description: User details + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/JsendSuccess' + - type: object + properties: + data: + $ref: '#/components/schemas/User' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/JsendError' + '404': + description: User not found + content: + application/json: + schema: + $ref: '#/components/schemas/JsendError' + post: + summary: Create a new user + description: Creates a new user (admin functionality, requires authentication) + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + description: User ID + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UserInput' + responses: + '201': + description: User created successfully + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/JsendSuccess' + - type: object + properties: + data: + $ref: '#/components/schemas/User' + '400': + description: Invalid input + content: + application/json: + schema: + $ref: '#/components/schemas/JsendError' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/JsendError' + '409': + description: User already exists + content: + application/json: + schema: + $ref: '#/components/schemas/JsendError' + put: + summary: Update a user + description: Updates an existing user (requires authentication) + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + description: User ID + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UserInput' + responses: + '200': + description: User updated successfully + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/JsendSuccess' + - type: object + properties: + data: + $ref: '#/components/schemas/User' + '400': + description: Invalid input + content: + application/json: + schema: + $ref: '#/components/schemas/JsendError' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/JsendError' + '404': + description: User not found + content: + application/json: + schema: + $ref: '#/components/schemas/JsendError' + delete: + summary: Delete a user + description: Deletes an existing user (requires authentication) + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + description: User ID + schema: + type: string + responses: + '204': + description: User deleted successfully + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/JsendError' + '404': + description: User not found + content: + application/json: + schema: + $ref: '#/components/schemas/JsendError' + + /items: + get: + summary: Get list of items + description: Returns a list of all items for the authenticated user + security: + - bearerAuth: [] + responses: + '200': + description: List of items + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/JsendSuccess' + - type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/Item' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/JsendError' + post: + summary: Create a new item + description: Creates a new item for the authenticated user + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ItemInput' + responses: + '201': + description: Item created successfully + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/JsendSuccess' + - type: object + properties: + data: + $ref: '#/components/schemas/Item' + '400': + description: Invalid input + content: + application/json: + schema: + $ref: '#/components/schemas/JsendError' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/JsendError' + + /items/{id}: + get: + summary: Get item by ID + description: Returns a specific item by its ID + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + description: Item ID + schema: + type: string + responses: + '200': + description: Item details + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/JsendSuccess' + - type: object + properties: + data: + $ref: '#/components/schemas/Item' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/JsendError' + '404': + description: Item not found + content: + application/json: + schema: + $ref: '#/components/schemas/JsendError' + put: + summary: Update an item + description: Updates an existing item + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + description: Item ID + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ItemInput' + responses: + '200': + description: Item updated successfully + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/JsendSuccess' + - type: object + properties: + data: + $ref: '#/components/schemas/Item' + '400': + description: Invalid input + content: + application/json: + schema: + $ref: '#/components/schemas/JsendError' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/JsendError' + '404': + description: Item not found + content: + application/json: + schema: + $ref: '#/components/schemas/JsendError' + delete: + summary: Delete an item + description: Deletes an existing item + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + description: Item ID + schema: + type: string + responses: + '204': + description: Item deleted successfully + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/JsendError' + '404': + description: Item not found + content: + application/json: + schema: + $ref: '#/components/schemas/JsendError' + +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + + schemas: + JsendSuccess: + type: object + properties: + status: + type: string + example: success + data: + type: object + description: Response data + + JsendError: + type: object + properties: + status: + type: string + example: error + message: + type: string + description: Error message + code: + type: integer + description: Error code + data: + type: object + description: Additional error data + + User: + type: object + properties: + id: + type: string + description: User ID + username: + type: string + description: User's username or email + + UserInput: + type: object + required: + - username + - password + properties: + username: + type: string + description: User's username or email + password: + type: string + description: User's password + + Item: + type: object + properties: + id: + type: string + description: Item ID + name: + type: string + description: Item name + expirationDate: + type: string + format: date-time + description: Item expiration date + orderUrl: + type: string + format: uri + description: URL to send order request when item expires + userId: + type: string + description: ID of the user who owns this item + + ItemInput: + type: object + required: + - name + - expirationDate + - orderUrl + properties: + name: + type: string + description: Item name + expirationDate: + type: string + format: date-time + description: Item expiration date + orderUrl: + type: string + format: uri + description: URL to send order request when item expires