Browse Source

Added initial C++17 implementation

cpp17-init
chodak166 5 months ago
parent
commit
f6b45a6923
  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. 36
      cpp17/app/CMakeLists.txt
  12. 72
      cpp17/app/src/App.cpp
  13. 31
      cpp17/app/src/App.h
  14. 10
      cpp17/app/src/Main.cpp
  15. 49
      cpp17/app/src/SpdLogger.h
  16. 21
      cpp17/app/src/Version.h.in
  17. 33
      cpp17/doc/add-item-sequence.md
  18. 25
      cpp17/doc/request-sequence.md
  19. 13
      cpp17/docker/Dockerfile
  20. 8
      cpp17/docker/docker-compose.yml
  21. 45
      cpp17/lib/CMakeLists.txt
  22. 57
      cpp17/lib/include/autostore/AutoStore.h
  23. 98
      cpp17/lib/include/autostore/ILogger.h
  24. 90
      cpp17/lib/src/AutoStore.cpp
  25. 21
      cpp17/lib/src/application/commands/AddItem.cpp
  26. 27
      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. 20
      cpp17/lib/src/domain/entities/Item.h
  33. 15
      cpp17/lib/src/domain/entities/User.h
  34. 18
      cpp17/lib/src/domain/polices/ItemExpirationPolicy.h
  35. 17
      cpp17/lib/src/infrastructure/adapters/SystemClock.h
  36. 32
      cpp17/lib/src/infrastructure/helpers/Jsend.cpp
  37. 20
      cpp17/lib/src/infrastructure/helpers/Jsend.h
  38. 76
      cpp17/lib/src/infrastructure/helpers/JsonItem.cpp
  39. 22
      cpp17/lib/src/infrastructure/helpers/JsonItem.h
  40. 35
      cpp17/lib/src/infrastructure/http/HttpOrderService.cpp
  41. 20
      cpp17/lib/src/infrastructure/http/HttpOrderService.h
  42. 126
      cpp17/lib/src/infrastructure/http/HttpServer.cpp
  43. 30
      cpp17/lib/src/infrastructure/http/HttpServer.h
  44. 119
      cpp17/lib/src/infrastructure/repositories/FileItemRepository.cpp
  45. 29
      cpp17/lib/src/infrastructure/repositories/FileItemRepository.h
  46. 130
      cpp17/lib/src/infrastructure/repositories/FileUserRepository.cpp
  47. 30
      cpp17/lib/src/infrastructure/repositories/FileUserRepository.h
  48. 53
      cpp17/lib/src/webapi/controllers/StoreController.cpp
  49. 27
      cpp17/lib/src/webapi/controllers/StoreController.h
  50. 31
      cpp17/tests/CMakeLists.txt
  51. 365
      cpp17/tests/integration/FileItemRepository.test.cpp
  52. 312
      cpp17/tests/integration/FileUserRepository.test.cpp
  53. 10
      cpp17/vcpkg.json
  54. 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.

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

72
cpp17/app/src/App.cpp

@ -0,0 +1,72 @@
#include "App.h"
#include "SpdLogger.h"
#include <iostream>
#include <filesystem>
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>(spdLogger, 9);
autoStore = std::make_unique<AutoStore>(
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<std::mutex> 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<std::mutex> lock(mtx);
shouldExit = true;
}
exitCv.notify_one();
}
} // namespace nxl

31
cpp17/app/src/App.h

@ -0,0 +1,31 @@
#pragma once
#include <atomic>
#include <condition_variable>
#include <csignal>
#include <mutex>
#include <thread>
#include <memory>
#include <autostore/AutoStore.h>
#include <autostore/ILogger.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;
autostore::ILoggerPtr logger;
};
} // 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();
}

49
cpp17/app/src/SpdLogger.h

@ -0,0 +1,49 @@
#pragma once
#include <autostore/ILogger.h>
#include <spdlog/spdlog.h>
#include <spdlog/sinks/stdout_color_sinks.h>
class SpdLogger : public nxl::autostore::ILogger
{
public:
explicit SpdLogger(std::shared_ptr<spdlog::logger> 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<spdlog::logger> getLogger() const { return logger; }
void setVLevel(int8_t level) { vlevel = level; }
private:
int8_t vlevel{-1};
std::shared_ptr<spdlog::logger> logger;
};

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

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

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

@ -0,0 +1,57 @@
#pragma once
#include "autostore/ILogger.h"
#include <memory>
#include <string>
#include <string_view>
#include <thread>
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<infrastructure::HttpServer> httpServer;
std::unique_ptr<webapi::StoreController> storeController;
std::unique_ptr<application::IItemRepository> itemRepository;
std::unique_ptr<application::IClock> clock;
std::unique_ptr<application::IOrderService> orderService;
};
} // namespace nxl::autostore

98
cpp17/lib/include/autostore/ILogger.h

@ -0,0 +1,98 @@
#pragma once
#include <memory>
#include <string>
#include <utility>
#include <cstdint>
#include <string_view>
#include <cstdio>
namespace nxl::autostore {
template <typename T> auto to_printf_arg(T&& arg)
{
if constexpr (std::is_same_v<std::decay_t<T>, std::string_view>) {
return std::string(arg).c_str();
} else if constexpr (std::is_same_v<std::decay_t<T>, std::string>) {
return arg.c_str();
} else {
return std::forward<T>(arg);
}
}
#define DEFINE_LOG_METHOD(name, level) \
void name(const char* message) \
{ \
log(LogLevel::level, std::string_view(message)); \
} \
template <typename... Args> void name(const char* format, Args&&... args) \
{ \
_log(LogLevel::level, format, -1, std::forward<Args>(args)...); \
}
#define DEFINE_LOGGER_ALIAS(original, alias) \
template <typename... Args> void alias(const char* format, Args&&... args) \
{ \
original(format, std::forward<Args>(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 <typename... Args>
void verbose(int8_t level, const char* format, Args&&... args)
{
_log(LogLevel::Verbose, format, level, std::forward<Args>(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 <typename... Args>
void v(int8_t level, const char* format, Args&&... args)
{
_log(LogLevel::Verbose, format, level, std::forward<Args>(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 <typename... Args>
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>(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<ILogger>;
} // namespace nxl::autostore

90
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 <iostream>
#include <filesystem>
#include <memory>
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<infrastructure::FileItemRepository>(itemsDbPath);
clock = std::make_unique<infrastructure::SystemClock>();
orderService = std::make_unique<infrastructure::HttpOrderService>(log);
// Initialize HTTP server
httpServer = std::make_unique<infrastructure::HttpServer>(log);
// Initialize store controller
storeController = std::make_unique<webapi::StoreController>(
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

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

@ -0,0 +1,21 @@
#include "AddItem.h"
#include <stdexcept>
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

27
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

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

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(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

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(std::string_view 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(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

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(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

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

@ -0,0 +1,35 @@
#include "HttpOrderService.h"
#include <stdexcept>
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

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

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

@ -0,0 +1,126 @@
#include "infrastructure/http/HttpServer.h"
#include <iostream>
#include <future>
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<bool> startupPromise;
std::future<bool> 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

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

@ -0,0 +1,30 @@
#pragma once
#include "autostore/ILogger.h"
#include <httplib.h>
#include <memory>
#include <string>
#include <thread>
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

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

53
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

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

@ -0,0 +1,27 @@
#pragma once
#include "application/commands/AddItem.h"
#include <httplib.h> // 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

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",
"spdlog",
"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