Compare commits

...

12 Commits

  1. 4
      .gitignore
  2. 101
      README.md
  3. 83
      cpp17/.clang-format
  4. 5
      cpp17/.devcontainer/Dockerfile
  5. 20
      cpp17/.devcontainer/devcontainer.json
  6. 12
      cpp17/.devcontainer/docker-compose.yml
  7. 14
      cpp17/CMakeLists.txt
  8. 20
      cpp17/CMakePresets.json
  9. 111
      cpp17/README.md
  10. 55
      cpp17/TODO.md
  11. 33
      cpp17/app/CMakeLists.txt
  12. 55
      cpp17/app/src/App.cpp
  13. 29
      cpp17/app/src/App.h
  14. 10
      cpp17/app/src/Main.cpp
  15. 21
      cpp17/app/src/Version.h.in
  16. 33
      cpp17/doc/add-item-sequence.md
  17. 25
      cpp17/doc/request-sequence.md
  18. 13
      cpp17/docker/Dockerfile
  19. 8
      cpp17/docker/docker-compose.yml
  20. 48
      cpp17/lib/CMakeLists.txt
  21. 41
      cpp17/lib/include/autostore/AutoStore.h
  22. 97
      cpp17/lib/src/AutoStore.cpp
  23. 45
      cpp17/lib/src/DiContainer.cpp
  24. 102
      cpp17/lib/src/DiContainer.h
  25. 27
      cpp17/lib/src/application/commands/AddItem.cpp
  26. 28
      cpp17/lib/src/application/commands/AddItem.h
  27. 17
      cpp17/lib/src/application/interfaces/IAuthService.h
  28. 14
      cpp17/lib/src/application/interfaces/IClock.h
  29. 22
      cpp17/lib/src/application/interfaces/IItemRepository.h
  30. 14
      cpp17/lib/src/application/interfaces/IOrderService.h
  31. 23
      cpp17/lib/src/application/interfaces/IUserRepository.h
  32. 34
      cpp17/lib/src/application/presenters/GenericPresenters.h
  33. 10
      cpp17/lib/src/application/presenters/StorePresenters.h
  34. 20
      cpp17/lib/src/domain/entities/Item.h
  35. 15
      cpp17/lib/src/domain/entities/User.h
  36. 18
      cpp17/lib/src/domain/polices/ItemExpirationPolicy.h
  37. 17
      cpp17/lib/src/infrastructure/adapters/SystemClock.h
  38. 32
      cpp17/lib/src/infrastructure/helpers/Jsend.cpp
  39. 20
      cpp17/lib/src/infrastructure/helpers/Jsend.h
  40. 76
      cpp17/lib/src/infrastructure/helpers/JsonItem.cpp
  41. 22
      cpp17/lib/src/infrastructure/helpers/JsonItem.h
  42. 36
      cpp17/lib/src/infrastructure/http/HttpOrderService.cpp
  43. 20
      cpp17/lib/src/infrastructure/http/HttpOrderService.h
  44. 99
      cpp17/lib/src/infrastructure/http/HttpServer.cpp
  45. 28
      cpp17/lib/src/infrastructure/http/HttpServer.h
  46. 119
      cpp17/lib/src/infrastructure/repositories/FileItemRepository.cpp
  47. 29
      cpp17/lib/src/infrastructure/repositories/FileItemRepository.h
  48. 130
      cpp17/lib/src/infrastructure/repositories/FileUserRepository.cpp
  49. 30
      cpp17/lib/src/infrastructure/repositories/FileUserRepository.h
  50. 58
      cpp17/lib/src/webapi/controllers/StoreController.cpp
  51. 21
      cpp17/lib/src/webapi/controllers/StoreController.h
  52. 31
      cpp17/tests/CMakeLists.txt
  53. 365
      cpp17/tests/integration/FileItemRepository.test.cpp
  54. 312
      cpp17/tests/integration/FileUserRepository.test.cpp
  55. 10
      cpp17/vcpkg.json
  56. 564
      openapi.yaml

4
.gitignore vendored

@ -1,6 +1,10 @@
# ---> C++
# Prerequisites
*.d
build
build-*
volumes
tmp
# Compiled Object files
*.slo

101
README.md

@ -55,45 +55,48 @@ AutoStore/
├── Extern
│ ├── <jwt-lib, http-client, etc.>
│ └── <...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 `<impl>/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 |

83
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

5
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"]

20
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"
]
}
}
}

12
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"

14
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)

20
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
}
]
}

111
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
│ ├── <jwt-lib, http-client, etc.>
│ └── <...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 `<impl>/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 `<impl>/README.md` file with setup and running instructions.

55
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.

33
cpp17/app/CMakeLists.txt

@ -0,0 +1,33 @@
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)
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
)
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)

55
cpp17/app/src/App.cpp

@ -0,0 +1,55 @@
#include "App.h"
#include <iostream>
#include <filesystem>
namespace nxl {
std::condition_variable App::exitCv;
std::mutex App::mtx;
bool App::shouldExit = false;
App::App(int argc, char** argv)
{
signal(SIGINT, App::handleSignal);
signal(SIGTERM, App::handleSignal);
std::filesystem::create_directories("data");
autoStore = std::make_unique<nxl::autostore::AutoStore>("data");
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;
}
std::cout << "AutoStore is running. Press Ctrl+C to stop." << std::endl;
std::unique_lock<std::mutex> lock(mtx);
exitCv.wait(lock, [] { return shouldExit; });
autoStore->stop();
return 0;
}
void App::handleSignal(int signum)
{
std::cout << "\nCaught signal " << signum << ". Graceful shutdown."
<< std::endl;
{
std::lock_guard<std::mutex> lock(mtx);
shouldExit = true;
}
exitCv.notify_one();
}
} // namespace nxl

29
cpp17/app/src/App.h

@ -0,0 +1,29 @@
#pragma once
#include <atomic>
#include <condition_variable>
#include <csignal>
#include <mutex>
#include <thread>
#include <memory>
#include <autostore/AutoStore.h>
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<nxl::autostore::AutoStore> autoStore;
};
} // namespace nxl

10
cpp17/app/src/Main.cpp

@ -0,0 +1,10 @@
#include "App.h"
#include "Version.h"
#include <iostream>
int main(int argc, char** argv)
{
std::cout << "AutoStore v" << nxl::getVersionString() << std::endl;
nxl::App app(argc, argv);
return app.exec();
}

21
cpp17/app/src/Version.h.in

@ -0,0 +1,21 @@
#ifndef VERSION_H_IN
#define VERSION_H_IN
#include <string>
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

33
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)
```

25
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
```

13
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"]

8
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

48
cpp17/lib/CMakeLists.txt

@ -0,0 +1,48 @@
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)
find_path(DINGO_INCLUDE_DIRS "dingo/allocator.h")
add_library(${TARGET_NAME} STATIC
src/AutoStore.cpp
src/DiContainer.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
${DINGO_INCLUDE_DIRS}
)
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
)

41
cpp17/lib/include/autostore/AutoStore.h

@ -0,0 +1,41 @@
#pragma once
#include <memory>
#include <string>
#include <string_view>
#include <thread>
namespace nxl::autostore {
namespace di {
class DiContainer;
}
namespace webapi {
class StoreController;
}
class AutoStore
{
public:
AutoStore(std::string_view dataPath, int port = 8080,
std::string_view host = "0.0.0.0");
~AutoStore();
bool initialize();
bool start();
void stop();
private:
int port;
std::string host;
std::string dataPath;
bool initialized;
std::thread serverThread;
bool serverRunning;
std::unique_ptr<di::DiContainer> diContainer;
std::unique_ptr<webapi::StoreController> storeController;
};
} // namespace nxl::autostore

97
cpp17/lib/src/AutoStore.cpp

@ -0,0 +1,97 @@
#include "AutoStore.h"
#include "DiContainer.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 <iostream>
#include <filesystem>
#include <memory>
namespace nxl::autostore {
AutoStore::AutoStore(std::string_view dataPath, int port, std::string_view host)
: dataPath(dataPath), port(port), host(host), initialized(false),
serverRunning(false)
{}
AutoStore::~AutoStore()
{
if (serverRunning) {
stop();
}
}
bool AutoStore::initialize()
{
std::cout << "Initializing AutoStore with data path: " << dataPath
<< ", host: " << host << ", port: " << port << std::endl;
try {
std::filesystem::create_directories(dataPath);
diContainer = std::make_unique<di::DiContainer>();
storeController = std::make_unique<webapi::StoreController>(*diContainer);
initialized = true;
std::cout << "AutoStore initialized successfully" << std::endl;
return true;
} catch (const std::exception& e) {
std::cerr << "Failed to initialize AutoStore: " << e.what() << std::endl;
return false;
}
}
bool AutoStore::start()
{
if (!initialized) {
std::cerr << "AutoStore not initialized. Call initialize() first."
<< std::endl;
return false;
}
std::cout << "Starting AutoStore services..." << std::endl;
try {
auto& httpServer = diContainer->resolveRef<infrastructure::HttpServer>();
storeController->registerRoutes(httpServer.getServer());
if (!httpServer.start(port, host)) {
std::cerr << "Failed to start HTTP server" << std::endl;
return false;
}
serverRunning = true;
std::cout << "AutoStore services started successfully" << std::endl;
std::cout << "HTTP server listening on http://" << host << ":" << port
<< std::endl;
std::cout << "API endpoint: POST http://" << host << ":" << port
<< "/api/items" << std::endl;
return true;
} catch (const std::exception& e) {
std::cerr << "Failed to start AutoStore services: " << e.what()
<< std::endl;
return false;
}
}
void AutoStore::stop()
{
if (!serverRunning) {
return;
}
std::cout << "Stopping AutoStore services..." << std::endl;
if (diContainer) {
auto& httpServer = diContainer->resolveRef<infrastructure::HttpServer>();
httpServer.stop();
}
serverRunning = false;
std::cout << "AutoStore services stopped" << std::endl;
}
} // namespace nxl::autostore

45
cpp17/lib/src/DiContainer.cpp

@ -0,0 +1,45 @@
#include "DiContainer.h"
#include "infrastructure/repositories/FileItemRepository.h"
#include "infrastructure/adapters/SystemClock.h"
#include "infrastructure/http/HttpOrderService.h"
#include "infrastructure/http/HttpServer.h"
#include "application/commands/AddItem.h"
#include "webapi/controllers/StoreController.h"
#include <filesystem>
namespace nxl::autostore::di {
DiContainer::DiContainer()
{
registerDependencies();
}
void DiContainer::registerDependencies()
{
// Register shared references
container.register_type<dingo::scope<dingo::shared>,
dingo::storage<infrastructure::FileItemRepository>,
dingo::interface<application::IItemRepository>>();
container.register_type<dingo::scope<dingo::shared>,
dingo::storage<infrastructure::SystemClock>,
dingo::interface<application::IClock>>();
container.register_type<dingo::scope<dingo::shared>,
dingo::storage<infrastructure::HttpOrderService>,
dingo::interface<application::IOrderService>>();
container.register_type<dingo::scope<dingo::shared>,
dingo::storage<infrastructure::HttpServer>>();
container.register_indexed_type<dingo::scope<dingo::shared>,
dingo::storage<application::AddItem>,
dingo::interface<application::AddItem>>(
std::string("AddItem"));
// test:
auto uc = container.resolve<application::AddItem>(
std::string("AddItem")); // throws on start
}
} // namespace nxl::autostore::di

102
cpp17/lib/src/DiContainer.h

@ -0,0 +1,102 @@
#pragma once
#include <dingo/container.h>
#include <dingo/factory/constructor.h>
#include <dingo/storage/external.h>
#include <dingo/storage/shared.h>
#include <dingo/index/unordered_map.h>
#include <memory>
#include <string>
#include <filesystem>
#include <tuple>
// Forward declarations
namespace nxl::autostore {
class AutoStore;
}
namespace nxl::autostore::infrastructure {
class FileItemRepository;
class SystemClock;
class HttpOrderService;
class HttpServer;
} // namespace nxl::autostore::infrastructure
namespace nxl::autostore::application {
class AddItem;
}
namespace nxl::autostore::webapi {
class StoreController;
}
namespace nxl::autostore::di {
// Declare traits with std::string based index for named resolution
struct container_traits : dingo::dynamic_container_traits
{
using index_definition_type =
std::tuple<std::tuple<std::string, dingo::index_type::unordered_map>>;
};
/**
* @brief Dependency Injection Container for AutoStore application
*
* This class wraps the dingo container and provides a simplified interface
* for registering and resolving dependencies in the AutoStore application.
*/
class DiContainer
{
public:
/**
* @brief Construct a new DiContainer object
*/
DiContainer();
/**
* @brief Destroy the DiContainer object
*/
~DiContainer() = default;
/**
* @brief Register all application dependencies
*
* @param dataPath Path to the data directory
* @param port HTTP server port
* @param host HTTP server host
*/
void registerDependencies();
/**
* @brief Resolve a dependency by type
*
* @tparam T Type to resolve
* @return Instance of the resolved type
*/
template <typename T> T resolve() { return container.resolve<T>(); }
/**
* @brief Resolve a dependency by type as a shared pointer
*
* @tparam T Type to resolve
* @return Shared pointer to the resolved type
*/
template <typename T> std::shared_ptr<T> resolveShared()
{
return container.resolve<std::shared_ptr<T>>();
}
/**
* @brief Resolve a dependency by type as a reference
*
* @tparam T Type to resolve
* @return Reference to the resolved type
*/
template <typename T> T& resolveRef() { return container.resolve<T&>(); }
private:
dingo::container<container_traits> container;
};
} // namespace nxl::autostore::di

27
cpp17/lib/src/application/commands/AddItem.cpp

@ -0,0 +1,27 @@
#include "AddItem.h"
#include <stdexcept>
namespace nxl::autostore::application {
AddItem::AddItem(IItemRepository& itemRepository, IClock& clock,
IOrderService& orderService)
: itemRepository(itemRepository), clock(clock), orderService(orderService)
{}
void AddItem::execute(domain::Item&& item, const ItemPresenter& presenter)
{
try {
const auto currentTime = clock.getCurrentTime();
if (expirationPolicy.isExpired(item, currentTime)) {
orderService.orderItem(item);
}
item.id = itemRepository.save(item);
presenter(item); // Success
} catch (const std::exception& e) {
presenter(item); // Failure
}
}
} // namespace nxl::autostore::application

28
cpp17/lib/src/application/commands/AddItem.h

@ -0,0 +1,28 @@
#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"
#include "application/presenters/StorePresenters.h"
namespace nxl::autostore::application {
class AddItem
{
public:
virtual ~AddItem() = default;
AddItem(IItemRepository& itemRepository, IClock& clock,
IOrderService& orderService);
void execute(domain::Item&& item, const ItemPresenter& presenter);
private:
IItemRepository& itemRepository;
IClock& clock;
IOrderService& orderService;
domain::ItemExpirationPolicy expirationPolicy;
};
} // namespace nxl::autostore::application

17
cpp17/lib/src/application/interfaces/IAuthService.h

@ -0,0 +1,17 @@
#pragma once
#include <string>
#include <string_view>
#include <optional>
namespace nxl::autostore::application {
class IAuthService
{
public:
virtual ~IAuthService() = default;
virtual std::string generateToken(std::string_view userId) = 0;
virtual std::optional<std::string> validateToken(std::string_view token) = 0;
};
} // namespace nxl::autostore::application

14
cpp17/lib/src/application/interfaces/IClock.h

@ -0,0 +1,14 @@
#pragma once
#include <chrono>
namespace nxl::autostore::application {
class IClock
{
public:
virtual ~IClock() = default;
virtual std::chrono::system_clock::time_point getCurrentTime() const = 0;
};
} // namespace nxl::autostore::application

22
cpp17/lib/src/application/interfaces/IItemRepository.h

@ -0,0 +1,22 @@
#pragma once
#include "domain/entities/Item.h"
#include <optional>
#include <string>
#include <string_view>
#include <vector>
namespace nxl::autostore::application {
class IItemRepository
{
public:
virtual ~IItemRepository() = default;
virtual domain::Item::Id_t save(const domain::Item& item) = 0;
virtual std::optional<domain::Item> findById(std::string_view id) = 0;
virtual std::vector<domain::Item> findByUser(std::string_view userId) = 0;
virtual std::vector<domain::Item> findAll() = 0;
virtual void remove(std::string_view id) = 0;
};
} // namespace nxl::autostore::application

14
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

23
cpp17/lib/src/application/interfaces/IUserRepository.h

@ -0,0 +1,23 @@
#pragma once
#include "domain/entities/User.h"
#include <optional>
#include <string>
#include <string_view>
#include <vector>
namespace nxl::autostore::application {
class IUserRepository
{
public:
virtual ~IUserRepository() = default;
virtual void save(const domain::User& user) = 0;
virtual std::optional<domain::User> findById(std::string_view id) = 0;
virtual std::optional<domain::User>
findByUsername(std::string_view username) = 0;
virtual std::vector<domain::User> findAll() = 0;
virtual void remove(std::string_view id) = 0;
};
} // namespace nxl::autostore::application

34
cpp17/lib/src/application/presenters/GenericPresenters.h

@ -0,0 +1,34 @@
#pragma once
#include <functional>
#include <string>
namespace nxl::autostore::application {
struct OpResult
{
bool success;
std::string message;
bool operator==(const OpResult& other) const
{
return success == other.success && message == other.message;
}
};
struct ErrorResult : public OpResult
{
ErrorResult(std::string message) : OpResult({false, message}) {}
};
struct SuccessResult : public OpResult
{
SuccessResult(std::string message) : OpResult({true, message}) {}
};
using BoolPresenter = std::function<void(bool)>;
using IntPresenter = std::function<void(int)>;
using DoublePresenter = std::function<void(double)>;
using StringPresenter = std::function<void(std::string)>;
} // namespace nxl::autostore::application

10
cpp17/lib/src/application/presenters/StorePresenters.h

@ -0,0 +1,10 @@
#pragma once
#include "domain/entities/Item.h"
#include <functional>
namespace nxl::autostore::application {
using ItemPresenter = std::function<void(const domain::Item& item)>;
} // namespace nxl::autostore::application

20
cpp17/lib/src/domain/entities/Item.h

@ -0,0 +1,20 @@
#pragma once
#include "User.h"
#include <string>
#include <chrono>
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

15
cpp17/lib/src/domain/entities/User.h

@ -0,0 +1,15 @@
#pragma once
#include <string>
namespace nxl::autostore::domain {
struct User
{
using Id_t = std::string;
Id_t id;
std::string username;
std::string passwordHash;
};
} // namespace nxl::autostore::domain

18
cpp17/lib/src/domain/polices/ItemExpirationPolicy.h

@ -0,0 +1,18 @@
#pragma once
#include "domain/entities/Item.h"
#include <chrono>
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

17
cpp17/lib/src/infrastructure/adapters/SystemClock.h

@ -0,0 +1,17 @@
#pragma once
#include "application/interfaces/IClock.h"
#include <chrono>
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

32
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(const std::string& 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

20
cpp17/lib/src/infrastructure/helpers/Jsend.h

@ -0,0 +1,20 @@
#pragma once
#include "nlohmann/json.hpp"
#include <string>
namespace nxl::autostore::infrastructure {
class Jsend
{
public:
static std::string success(const nlohmann::json& data = nullptr);
static std::string error(const std::string& message, int code = 500,
const nlohmann::json& data = nullptr);
private:
Jsend() = delete;
~Jsend() = delete;
};
} // namespace nxl::autostore::infrastructure

76
cpp17/lib/src/infrastructure/helpers/JsonItem.cpp

@ -0,0 +1,76 @@
#include "infrastructure/helpers/JsonItem.h"
#include <chrono>
#include <ctime>
#include <stdexcept>
#include <type_traits>
namespace nxl::autostore::infrastructure {
domain::Item JsonItem::fromJson(const std::string& 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

22
cpp17/lib/src/infrastructure/helpers/JsonItem.h

@ -0,0 +1,22 @@
#pragma once
#include "domain/entities/Item.h"
#include "nlohmann/json.hpp"
#include <string>
namespace nxl::autostore::infrastructure {
class JsonItem
{
public:
static domain::Item fromJson(const std::string& 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

36
cpp17/lib/src/infrastructure/http/HttpOrderService.cpp

@ -0,0 +1,36 @@
#include "HttpOrderService.h"
#include <stdexcept>
#include <iostream>
namespace nxl::autostore::infrastructure {
HttpOrderService::HttpOrderService(const std::string& baseUrl)
: baseUrl(baseUrl)
{}
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(const std::string& url,
const std::string& payload)
{
// In a real implementation, this would use an HTTP client library
// For now, we'll simulate the HTTP call
std::cout << "POST request to: " << url << std::endl;
std::cout << "Payload: " << payload << std::endl;
// Simulate HTTP error handling
if (url.find("error") != std::string::npos) {
throw std::runtime_error("Failed to send order request to: " + url);
}
}
} // namespace nxl::autostore::infrastructure

20
cpp17/lib/src/infrastructure/http/HttpOrderService.h

@ -0,0 +1,20 @@
#pragma once
#include "application/interfaces/IOrderService.h"
#include "domain/entities/Item.h"
#include <string>
namespace nxl::autostore::infrastructure {
class HttpOrderService : public application::IOrderService
{
public:
explicit HttpOrderService(const std::string& baseUrl = "");
void orderItem(const domain::Item& item) override;
private:
std::string baseUrl;
void sendPostRequest(const std::string& url, const std::string& payload);
};
} // namespace nxl::autostore::infrastructure

99
cpp17/lib/src/infrastructure/http/HttpServer.cpp

@ -0,0 +1,99 @@
#include "infrastructure/http/HttpServer.h"
#include <iostream>
namespace nxl::autostore::infrastructure {
HttpServer::HttpServer() {}
HttpServer::~HttpServer()
{
if (running) {
stop();
}
}
bool HttpServer::start(int port, const std::string& host)
{
if (running) {
return true;
}
// Set up error handler
server.set_error_handler([](const auto& req, auto& res) {
auto fmt = "<p>Error Status: <span style='color:red;'>%d</span></p>";
char buf[BUFSIZ];
snprintf(buf, sizeof(buf), fmt, res.status);
res.set_content(buf, "text/html");
});
// Set up exception handler
server.set_exception_handler(
[](const auto& req, auto& res, std::exception_ptr ep) {
auto fmt = "<h1>Error 500</h1><p>%s</p>";
char buf[BUFSIZ];
try {
std::rethrow_exception(ep);
} catch (std::exception& e) {
snprintf(buf, sizeof(buf), fmt, e.what());
} catch (...) {
snprintf(buf, sizeof(buf), fmt, "Unknown Exception");
}
res.set_content(buf, "text/html");
res.status = 500;
});
std::cout << "Starting HTTP server on " << host << ":" << port << std::endl;
// Start server in a separate thread
serverThread = std::thread([host, port, this]() {
std::cout << "Server thread started, listening on " << host << ":" << port
<< std::endl;
bool listenResult = server.listen(host.c_str(), port);
std::cout << "Server stopped, listen result: " << listenResult << std::endl;
});
// Give the server a moment to start
std::this_thread::sleep_for(std::chrono::milliseconds(500));
// Check if server is listening by trying to bind to the port
if (!server.is_running()) {
std::cerr << "Failed to start HTTP server - server is not running"
<< std::endl;
if (serverThread.joinable()) {
serverThread.join();
}
return false;
}
running = true;
std::cout << "HTTP server is running" << std::endl;
return true;
}
void HttpServer::stop()
{
if (!running) {
return;
}
std::cout << "Stopping HTTP server..." << std::endl;
server.stop();
// Wait for the server to stop
std::this_thread::sleep_for(std::chrono::milliseconds(500));
running = false;
std::cout << "HTTP server stopped" << std::endl;
}
bool HttpServer::isRunning() const
{
return running;
}
httplib::Server& HttpServer::getServer()
{
return server;
}
} // namespace nxl::autostore::infrastructure

28
cpp17/lib/src/infrastructure/http/HttpServer.h

@ -0,0 +1,28 @@
#pragma once
#include <httplib.h>
#include <memory>
#include <string>
#include <thread>
namespace nxl::autostore::infrastructure {
class HttpServer
{
public:
explicit HttpServer();
~HttpServer();
bool start(int port = 8080, const std::string& host = "0.0.0.0");
void stop();
bool isRunning() const;
httplib::Server& getServer();
private:
bool running{false};
httplib::Server server;
std::thread serverThread;
};
} // namespace nxl::autostore::infrastructure

119
cpp17/lib/src/infrastructure/repositories/FileItemRepository.cpp

@ -0,0 +1,119 @@
#include "infrastructure/repositories/FileItemRepository.h"
#include "infrastructure/helpers/JsonItem.h"
#include <fstream>
#include <algorithm>
#include <chrono>
#include <ctime>
#include <iterator>
namespace nxl::autostore::infrastructure {
namespace {
// Helper functions for vector serialization
inline nlohmann::json itemsToJson(const std::vector<domain::Item>& items)
{
nlohmann::json j = nlohmann::json::array();
for (const auto& item : items) {
j.push_back(infrastructure::JsonItem::toJsonObj(item));
}
return j;
}
inline std::vector<domain::Item> jsonToItems(const nlohmann::json& j)
{
std::vector<domain::Item> 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<std::mutex> 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<domain::Item> FileItemRepository::findById(std::string_view id)
{
std::lock_guard<std::mutex> 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<domain::Item>
FileItemRepository::findByUser(std::string_view userId)
{
std::lock_guard<std::mutex> lock(mtx);
std::vector<domain::Item> userItems;
std::copy_if(items.begin(), items.end(), std::back_inserter(userItems),
[&](const domain::Item& i) { return i.userId == userId; });
return userItems;
}
std::vector<domain::Item> FileItemRepository::findAll()
{
std::lock_guard<std::mutex> lock(mtx);
return items;
}
void FileItemRepository::remove(std::string_view id)
{
std::lock_guard<std::mutex> 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<std::mutex> 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

29
cpp17/lib/src/infrastructure/repositories/FileItemRepository.h

@ -0,0 +1,29 @@
#pragma once
#include "application/interfaces/IItemRepository.h"
#include <string>
#include <vector>
#include <mutex>
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<domain::Item> findById(std::string_view id) override;
std::vector<domain::Item> findByUser(std::string_view userId) override;
std::vector<domain::Item> findAll() override;
void remove(std::string_view id) override;
private:
void load();
void persist();
std::string dbPath;
std::vector<domain::Item> items;
std::mutex mtx;
};
} // namespace nxl::autostore::infrastructure

130
cpp17/lib/src/infrastructure/repositories/FileUserRepository.cpp

@ -0,0 +1,130 @@
#include "infrastructure/repositories/FileUserRepository.h"
#include "nlohmann/json.hpp"
#include <fstream>
#include <algorithm>
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<domain::User>& 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<domain::User> jsonToUsers(const nlohmann::json& j)
{
std::vector<domain::User> 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<std::mutex> 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<domain::User> FileUserRepository::findById(std::string_view id)
{
std::lock_guard<std::mutex> 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<domain::User>
FileUserRepository::findByUsername(std::string_view username)
{
std::lock_guard<std::mutex> 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<domain::User> FileUserRepository::findAll()
{
std::lock_guard<std::mutex> lock(mtx);
return users;
}
void FileUserRepository::remove(std::string_view id)
{
std::lock_guard<std::mutex> 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<std::mutex> 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

30
cpp17/lib/src/infrastructure/repositories/FileUserRepository.h

@ -0,0 +1,30 @@
#pragma once
#include "application/interfaces/IUserRepository.h"
#include <string>
#include <vector>
#include <mutex>
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<domain::User> findById(std::string_view id) override;
std::optional<domain::User>
findByUsername(std::string_view username) override;
std::vector<domain::User> findAll() override;
void remove(std::string_view id) override;
private:
void load();
void persist();
std::string dbPath;
std::vector<domain::User> users;
std::mutex mtx;
};
} // namespace nxl::autostore::infrastructure

58
cpp17/lib/src/webapi/controllers/StoreController.cpp

@ -0,0 +1,58 @@
#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(di::DiContainer& diContainer)
: diContainer(diContainer)
{}
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 {
auto& addItemUseCase = diContainer.resolveRef<application::AddItem>();
addItemUseCase.execute(std::move(item), [&res](auto item) {
res.status = 201;
nlohmann::json responseData = nlohmann::json::object();
responseData["id"] = item.id;
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

21
cpp17/lib/src/webapi/controllers/StoreController.h

@ -0,0 +1,21 @@
#pragma once
#include "DiContainer.h"
#include <httplib.h> // TODO: forward declaration
namespace nxl::autostore::webapi {
class StoreController
{
public:
StoreController(di::DiContainer& diContainer);
void registerRoutes(httplib::Server& server);
private:
void addItem(const httplib::Request& req, httplib::Response& res);
di::DiContainer& diContainer;
};
} // namespace nxl::autostore::webapi

31
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)

365
cpp17/tests/integration/FileItemRepository.test.cpp

@ -0,0 +1,365 @@
#include "infrastructure/repositories/FileItemRepository.h"
#include "domain/entities/Item.h"
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_string.hpp>
#include <filesystem>
#include <fstream>
#include <optional>
#include <chrono>
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();
}

312
cpp17/tests/integration/FileUserRepository.test.cpp

@ -0,0 +1,312 @@
#include "infrastructure/repositories/FileUserRepository.h"
#include "domain/entities/User.h"
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_string.hpp>
#include <filesystem>
#include <fstream>
#include <optional>
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();
}

10
cpp17/vcpkg.json

@ -0,0 +1,10 @@
{
"name": "autostore",
"version-string": "1.0.0",
"dependencies": [
"cpp-httplib",
"nlohmann-json",
"dingo",
"catch2"
]
}

564
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
Loading…
Cancel
Save