Compare commits
12 Commits
master
...
cpp17-init
| Author | SHA1 | Date |
|---|---|---|
|
|
fc955dd1bb | 5 months ago |
|
|
a6d835d48f | 5 months ago |
|
|
15da327b15 | 5 months ago |
|
|
efc44bd9fa | 5 months ago |
|
|
3a63b9dfcf | 5 months ago |
|
|
74955aa3f9 | 5 months ago |
|
|
e150513ea9 | 5 months ago |
|
|
d3745718a2 | 5 months ago |
|
|
bf0634ae52 | 5 months ago |
|
|
a7c13a1cca | 5 months ago |
|
|
46cd5958b5 | 5 months ago |
|
|
1f0d185a14 | 5 months ago |
56 changed files with 3207 additions and 39 deletions
@ -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 |
||||
@ -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"] |
||||
@ -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" |
||||
] |
||||
} |
||||
} |
||||
} |
||||
@ -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" |
||||
@ -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) |
||||
@ -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 |
||||
} |
||||
] |
||||
} |
||||
@ -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. |
||||
@ -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. |
||||
@ -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) |
||||
@ -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
|
||||
@ -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
|
||||
@ -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(); |
||||
} |
||||
@ -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
|
||||
@ -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) |
||||
``` |
||||
@ -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 |
||||
``` |
||||
@ -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"] |
||||
@ -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 |
||||
@ -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 |
||||
) |
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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) |
||||
@ -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(); |
||||
} |
||||
@ -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(); |
||||
} |
||||
@ -0,0 +1,10 @@
|
||||
{ |
||||
"name": "autostore", |
||||
"version-string": "1.0.0", |
||||
"dependencies": [ |
||||
"cpp-httplib", |
||||
"nlohmann-json", |
||||
"dingo", |
||||
"catch2" |
||||
] |
||||
} |
||||
@ -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…
Reference in new issue