54 changed files with 3213 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,36 @@ |
|||||||
|
cmake_minimum_required(VERSION 3.20) |
||||||
|
|
||||||
|
project(AutoStoreApp LANGUAGES CXX VERSION 0.1.0) |
||||||
|
set(TARGET_NAME AutoStore) |
||||||
|
|
||||||
|
set(CMAKE_CXX_STANDARD 17) |
||||||
|
set(CMAKE_CXX_STANDARD_REQUIRED ON) |
||||||
|
|
||||||
|
find_package(spdlog CONFIG REQUIRED) |
||||||
|
|
||||||
|
configure_file(src/Version.h.in ${CMAKE_BINARY_DIR}/Version.h) |
||||||
|
|
||||||
|
set(SOURCES |
||||||
|
src/Main.cpp |
||||||
|
src/App.cpp |
||||||
|
src/App.h |
||||||
|
) |
||||||
|
|
||||||
|
set (LIBRARIES |
||||||
|
AutoStoreLib |
||||||
|
spdlog::spdlog |
||||||
|
) |
||||||
|
|
||||||
|
add_executable(${TARGET_NAME} ${SOURCES}) |
||||||
|
target_include_directories(${TARGET_NAME} |
||||||
|
PRIVATE |
||||||
|
${CMAKE_BINARY_DIR} |
||||||
|
) |
||||||
|
|
||||||
|
# for docker test |
||||||
|
# target_compile_options(${TARGET_NAME} PRIVATE -static-libgcc -static-libstdc++) |
||||||
|
# target_link_options(${TARGET_NAME} PRIVATE -static-libgcc -static-libstdc++) |
||||||
|
|
||||||
|
target_link_libraries(${TARGET_NAME} PRIVATE ${LIBRARIES}) |
||||||
|
|
||||||
|
# add_subdirectory(tests/unit) |
||||||
@ -0,0 +1,72 @@ |
|||||||
|
#include "App.h" |
||||||
|
#include "SpdLogger.h" |
||||||
|
#include <iostream> |
||||||
|
#include <filesystem> |
||||||
|
|
||||||
|
namespace nxl { |
||||||
|
|
||||||
|
using nxl::autostore::AutoStore; |
||||||
|
|
||||||
|
std::condition_variable App::exitCv; |
||||||
|
std::mutex App::mtx; |
||||||
|
bool App::shouldExit = false; |
||||||
|
nxl::autostore::ILoggerPtr log{nullptr}; |
||||||
|
|
||||||
|
App::App(int argc, char** argv) |
||||||
|
{ |
||||||
|
signal(SIGINT, App::handleSignal); |
||||||
|
signal(SIGTERM, App::handleSignal); |
||||||
|
|
||||||
|
std::filesystem::create_directories("data"); |
||||||
|
|
||||||
|
auto spdLogger = spdlog::stdout_color_mt("console"); |
||||||
|
spdLogger->set_pattern("[%Y-%m-%d %H:%M:%S] [%^%l%$] %v"); |
||||||
|
spdLogger->set_level(spdlog::level::debug); |
||||||
|
log = logger = std::make_shared<SpdLogger>(spdLogger, 9); |
||||||
|
autoStore = std::make_unique<AutoStore>( |
||||||
|
AutoStore::Config{ |
||||||
|
.dataPath = "data", |
||||||
|
.host = "0.0.0.0", |
||||||
|
.port = 8080, |
||||||
|
}, |
||||||
|
logger); |
||||||
|
|
||||||
|
if (!autoStore->initialize()) { |
||||||
|
std::cerr << "Failed to initialize AutoStore" << std::endl; |
||||||
|
throw std::runtime_error("Failed to initialize AutoStore"); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
App::~App() = default; |
||||||
|
|
||||||
|
int App::exec() |
||||||
|
{ |
||||||
|
if (!autoStore->start()) { |
||||||
|
std::cerr << "Failed to start AutoStore services" << std::endl; |
||||||
|
return 1; |
||||||
|
} |
||||||
|
|
||||||
|
logger->info("AutoStore is running. Press Ctrl+C to stop."); |
||||||
|
|
||||||
|
std::unique_lock<std::mutex> lock(mtx); |
||||||
|
exitCv.wait(lock, [] { return shouldExit; }); |
||||||
|
|
||||||
|
autoStore->stop(); |
||||||
|
|
||||||
|
return 0; |
||||||
|
} |
||||||
|
|
||||||
|
void App::handleSignal(int signum) |
||||||
|
{ |
||||||
|
if (log) { |
||||||
|
log->info("Caught signal %d. Graceful shutdown.", signum); |
||||||
|
} |
||||||
|
|
||||||
|
{ |
||||||
|
std::lock_guard<std::mutex> lock(mtx); |
||||||
|
shouldExit = true; |
||||||
|
} |
||||||
|
exitCv.notify_one(); |
||||||
|
} |
||||||
|
|
||||||
|
} // namespace nxl
|
||||||
@ -0,0 +1,31 @@ |
|||||||
|
#pragma once |
||||||
|
|
||||||
|
#include <atomic> |
||||||
|
#include <condition_variable> |
||||||
|
#include <csignal> |
||||||
|
#include <mutex> |
||||||
|
#include <thread> |
||||||
|
#include <memory> |
||||||
|
#include <autostore/AutoStore.h> |
||||||
|
#include <autostore/ILogger.h> |
||||||
|
|
||||||
|
namespace nxl { |
||||||
|
|
||||||
|
class App |
||||||
|
{ |
||||||
|
public: |
||||||
|
App(int argc, char** argv); |
||||||
|
~App(); |
||||||
|
int exec(); |
||||||
|
|
||||||
|
private: |
||||||
|
static void handleSignal(int signum); |
||||||
|
static std::condition_variable exitCv; |
||||||
|
static std::mutex mtx; |
||||||
|
static bool shouldExit; |
||||||
|
|
||||||
|
std::unique_ptr<nxl::autostore::AutoStore> autoStore; |
||||||
|
autostore::ILoggerPtr logger; |
||||||
|
}; |
||||||
|
|
||||||
|
} // namespace nxl
|
||||||
@ -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,49 @@ |
|||||||
|
#pragma once |
||||||
|
|
||||||
|
#include <autostore/ILogger.h> |
||||||
|
#include <spdlog/spdlog.h> |
||||||
|
#include <spdlog/sinks/stdout_color_sinks.h> |
||||||
|
|
||||||
|
class SpdLogger : public nxl::autostore::ILogger |
||||||
|
{ |
||||||
|
public: |
||||||
|
explicit SpdLogger(std::shared_ptr<spdlog::logger> logger, int8_t vlevel) |
||||||
|
: logger{std::move(logger)}, vlevel{vlevel} |
||||||
|
{} |
||||||
|
|
||||||
|
protected: |
||||||
|
void log(LogLevel level, std::string_view message) override |
||||||
|
{ |
||||||
|
switch (level) { |
||||||
|
case LogLevel::Info: |
||||||
|
logger->info(message); |
||||||
|
break; |
||||||
|
case LogLevel::Warning: |
||||||
|
logger->warn(message); |
||||||
|
break; |
||||||
|
case LogLevel::Error: |
||||||
|
logger->error(message); |
||||||
|
break; |
||||||
|
case LogLevel::Debug: |
||||||
|
logger->debug(message); |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
void vlog(int8_t level, std::string_view message) override |
||||||
|
{ |
||||||
|
if (level > vlevel) { |
||||||
|
return; |
||||||
|
} |
||||||
|
logger->log(spdlog::level::info, |
||||||
|
"[V:" + std::to_string(level) + "] " + std::string(message)); |
||||||
|
} |
||||||
|
|
||||||
|
std::shared_ptr<spdlog::logger> getLogger() const { return logger; } |
||||||
|
|
||||||
|
void setVLevel(int8_t level) { vlevel = level; } |
||||||
|
|
||||||
|
private: |
||||||
|
int8_t vlevel{-1}; |
||||||
|
std::shared_ptr<spdlog::logger> logger; |
||||||
|
}; |
||||||
@ -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,45 @@ |
|||||||
|
cmake_minimum_required(VERSION 3.20) |
||||||
|
project(AutoStoreLib) |
||||||
|
set(TARGET_NAME AutoStoreLib) |
||||||
|
|
||||||
|
set(CMAKE_CXX_STANDARD 17) |
||||||
|
set(CMAKE_CXX_STANDARD_REQUIRED ON) |
||||||
|
|
||||||
|
# Find dependencies |
||||||
|
find_package(httplib CONFIG REQUIRED) |
||||||
|
find_package(nlohmann_json CONFIG REQUIRED) |
||||||
|
|
||||||
|
add_library(${TARGET_NAME} STATIC |
||||||
|
src/AutoStore.cpp |
||||||
|
src/infrastructure/repositories/FileUserRepository.cpp |
||||||
|
src/infrastructure/repositories/FileItemRepository.cpp |
||||||
|
src/infrastructure/http/HttpServer.cpp |
||||||
|
src/infrastructure/http/HttpOrderService.cpp |
||||||
|
src/infrastructure/helpers/Jsend.cpp |
||||||
|
src/infrastructure/helpers/JsonItem.cpp |
||||||
|
src/webapi/controllers/StoreController.cpp |
||||||
|
src/application/commands/AddItem.cpp |
||||||
|
) |
||||||
|
|
||||||
|
target_include_directories(${TARGET_NAME} |
||||||
|
PUBLIC |
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/include/autostore |
||||||
|
PRIVATE |
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/src |
||||||
|
) |
||||||
|
|
||||||
|
target_sources(${TARGET_NAME} |
||||||
|
PUBLIC |
||||||
|
FILE_SET HEADERS |
||||||
|
BASE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/include |
||||||
|
FILES |
||||||
|
include/autostore/AutoStore.h |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
target_link_libraries(${TARGET_NAME} |
||||||
|
PUBLIC |
||||||
|
httplib::httplib |
||||||
|
nlohmann_json::nlohmann_json |
||||||
|
) |
||||||
@ -0,0 +1,57 @@ |
|||||||
|
#pragma once |
||||||
|
|
||||||
|
#include "autostore/ILogger.h" |
||||||
|
#include <memory> |
||||||
|
#include <string> |
||||||
|
#include <string_view> |
||||||
|
#include <thread> |
||||||
|
|
||||||
|
namespace nxl::autostore { |
||||||
|
|
||||||
|
namespace application { |
||||||
|
class IItemRepository; |
||||||
|
class IClock; |
||||||
|
class IOrderService; |
||||||
|
} // namespace application
|
||||||
|
|
||||||
|
namespace infrastructure { |
||||||
|
class HttpServer; |
||||||
|
} // namespace infrastructure
|
||||||
|
|
||||||
|
namespace webapi { |
||||||
|
class StoreController; |
||||||
|
} // namespace webapi
|
||||||
|
|
||||||
|
namespace application { |
||||||
|
class AddItem; |
||||||
|
} // namespace application
|
||||||
|
|
||||||
|
class AutoStore |
||||||
|
{ |
||||||
|
public: |
||||||
|
struct Config |
||||||
|
{ |
||||||
|
std::string dataPath; |
||||||
|
std::string host{"0.0.0.0"}; |
||||||
|
uint16_t port{8080}; |
||||||
|
}; |
||||||
|
|
||||||
|
AutoStore(Config config, ILoggerPtr logger); |
||||||
|
~AutoStore(); |
||||||
|
|
||||||
|
bool initialize(); |
||||||
|
bool start(); |
||||||
|
void stop(); |
||||||
|
|
||||||
|
private: |
||||||
|
Config config; |
||||||
|
ILoggerPtr log; |
||||||
|
|
||||||
|
std::unique_ptr<infrastructure::HttpServer> httpServer; |
||||||
|
std::unique_ptr<webapi::StoreController> storeController; |
||||||
|
std::unique_ptr<application::IItemRepository> itemRepository; |
||||||
|
std::unique_ptr<application::IClock> clock; |
||||||
|
std::unique_ptr<application::IOrderService> orderService; |
||||||
|
}; |
||||||
|
|
||||||
|
} // namespace nxl::autostore
|
||||||
@ -0,0 +1,98 @@ |
|||||||
|
#pragma once |
||||||
|
#include <memory> |
||||||
|
#include <string> |
||||||
|
#include <utility> |
||||||
|
#include <cstdint> |
||||||
|
#include <string_view> |
||||||
|
#include <cstdio> |
||||||
|
namespace nxl::autostore { |
||||||
|
|
||||||
|
template <typename T> auto to_printf_arg(T&& arg) |
||||||
|
{ |
||||||
|
if constexpr (std::is_same_v<std::decay_t<T>, std::string_view>) { |
||||||
|
return std::string(arg).c_str(); |
||||||
|
} else if constexpr (std::is_same_v<std::decay_t<T>, std::string>) { |
||||||
|
return arg.c_str(); |
||||||
|
} else { |
||||||
|
return std::forward<T>(arg); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#define DEFINE_LOG_METHOD(name, level) \ |
||||||
|
void name(const char* message) \
|
||||||
|
{ \
|
||||||
|
log(LogLevel::level, std::string_view(message)); \
|
||||||
|
} \
|
||||||
|
template <typename... Args> void name(const char* format, Args&&... args) \
|
||||||
|
{ \
|
||||||
|
_log(LogLevel::level, format, -1, std::forward<Args>(args)...); \
|
||||||
|
} |
||||||
|
|
||||||
|
#define DEFINE_LOGGER_ALIAS(original, alias) \ |
||||||
|
template <typename... Args> void alias(const char* format, Args&&... args) \
|
||||||
|
{ \
|
||||||
|
original(format, std::forward<Args>(args)...); \
|
||||||
|
} |
||||||
|
|
||||||
|
class ILogger |
||||||
|
{ |
||||||
|
public: |
||||||
|
virtual ~ILogger() = default; |
||||||
|
DEFINE_LOG_METHOD(info, Info) |
||||||
|
DEFINE_LOG_METHOD(warning, Warning) |
||||||
|
DEFINE_LOG_METHOD(error, Error) |
||||||
|
DEFINE_LOG_METHOD(debug, Debug) |
||||||
|
void verbose(int8_t level, const char* message) { vlog(level, message); } |
||||||
|
template <typename... Args> |
||||||
|
void verbose(int8_t level, const char* format, Args&&... args) |
||||||
|
{ |
||||||
|
_log(LogLevel::Verbose, format, level, std::forward<Args>(args)...); |
||||||
|
} |
||||||
|
// Aliases defined using macro
|
||||||
|
DEFINE_LOGGER_ALIAS(info, i) |
||||||
|
DEFINE_LOGGER_ALIAS(warning, w) |
||||||
|
DEFINE_LOGGER_ALIAS(error, e) |
||||||
|
DEFINE_LOGGER_ALIAS(debug, d) |
||||||
|
void v(int8_t level, const char* message) { vlog(level, message); } |
||||||
|
template <typename... Args> |
||||||
|
void v(int8_t level, const char* format, Args&&... args) |
||||||
|
{ |
||||||
|
_log(LogLevel::Verbose, format, level, std::forward<Args>(args)...); |
||||||
|
} |
||||||
|
|
||||||
|
protected: |
||||||
|
enum class LogLevel { Info, Warning, Error, Debug, Verbose }; |
||||||
|
virtual void log(LogLevel level, std::string_view message) = 0; |
||||||
|
virtual void vlog(int8_t level, std::string_view message) = 0; |
||||||
|
|
||||||
|
private: |
||||||
|
template <typename... Args> |
||||||
|
void _log(LogLevel level, const char* format, int8_t vlevel = -1, |
||||||
|
Args&&... args) |
||||||
|
{ |
||||||
|
// Create a lambda that captures the converted arguments
|
||||||
|
auto format_message = [format](auto&&... args) { |
||||||
|
// Calculate the required size
|
||||||
|
size_t size = std::snprintf(nullptr, 0, format, args...) + 1; |
||||||
|
|
||||||
|
// Format the message
|
||||||
|
std::string msg; |
||||||
|
msg.resize(size); |
||||||
|
std::snprintf(&msg[0], size, format, args...); |
||||||
|
msg.pop_back(); |
||||||
|
|
||||||
|
return msg; |
||||||
|
}; |
||||||
|
|
||||||
|
// Call the lambda with the converted arguments
|
||||||
|
std::string msg = |
||||||
|
format_message(to_printf_arg(std::forward<Args>(args))...); |
||||||
|
|
||||||
|
vlevel == -1 ? log(level, msg) : vlog(vlevel, msg); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
// Undefine the macro to avoid polluting the namespace
|
||||||
|
#undef DEFINE_LOGGER_ALIAS |
||||||
|
using ILoggerPtr = std::shared_ptr<ILogger>; |
||||||
|
} // namespace nxl::autostore
|
||||||
@ -0,0 +1,90 @@ |
|||||||
|
#include "AutoStore.h" |
||||||
|
#include "infrastructure/repositories/FileItemRepository.h" |
||||||
|
#include "infrastructure/adapters/SystemClock.h" |
||||||
|
#include "infrastructure/http/HttpOrderService.h" |
||||||
|
#include "webapi/controllers/StoreController.h" |
||||||
|
#include "infrastructure/http/HttpServer.h" |
||||||
|
#include <iostream> |
||||||
|
#include <filesystem> |
||||||
|
#include <memory> |
||||||
|
|
||||||
|
namespace nxl::autostore { |
||||||
|
|
||||||
|
AutoStore::AutoStore(Config config, ILoggerPtr logger) |
||||||
|
: config{std::move(config)}, log{std::move(logger)} |
||||||
|
{} |
||||||
|
|
||||||
|
AutoStore::~AutoStore() |
||||||
|
{ |
||||||
|
if (httpServer && httpServer->isRunning()) { |
||||||
|
stop(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
bool AutoStore::initialize() |
||||||
|
{ |
||||||
|
try { |
||||||
|
std::filesystem::create_directories(config.dataPath); |
||||||
|
|
||||||
|
// Initialize repositories and services
|
||||||
|
std::string itemsDbPath = |
||||||
|
std::filesystem::path(config.dataPath) / "items.json"; |
||||||
|
itemRepository = |
||||||
|
std::make_unique<infrastructure::FileItemRepository>(itemsDbPath); |
||||||
|
|
||||||
|
clock = std::make_unique<infrastructure::SystemClock>(); |
||||||
|
|
||||||
|
orderService = std::make_unique<infrastructure::HttpOrderService>(log); |
||||||
|
|
||||||
|
// Initialize HTTP server
|
||||||
|
httpServer = std::make_unique<infrastructure::HttpServer>(log); |
||||||
|
|
||||||
|
// Initialize store controller
|
||||||
|
storeController = std::make_unique<webapi::StoreController>( |
||||||
|
webapi::StoreController::Context{ |
||||||
|
application::AddItem{*itemRepository, *clock, *orderService}}); |
||||||
|
|
||||||
|
log->info("AutoStore initialized successfully"); |
||||||
|
return true; |
||||||
|
} catch (const std::exception& e) { |
||||||
|
log->error("Failed to initialize AutoStore: %s", e.what()); |
||||||
|
return false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
bool AutoStore::start() |
||||||
|
{ |
||||||
|
log->info("Starting AutoStore services..."); |
||||||
|
|
||||||
|
try { |
||||||
|
storeController->registerRoutes(httpServer->getServer()); |
||||||
|
|
||||||
|
if (!httpServer->start(config.port, config.host)) { |
||||||
|
log->error("Failed to start HTTP server"); |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
log->info("AutoStore services started successfully"); |
||||||
|
log->info("HTTP server listening on http://%s:%d", config.host, |
||||||
|
config.port); |
||||||
|
log->info("API endpoint: POST http://%s:%d/api/items", config.host, |
||||||
|
config.port); |
||||||
|
|
||||||
|
return true; |
||||||
|
} catch (const std::exception& e) { |
||||||
|
log->error("Failed to start AutoStore services: %s", e.what()); |
||||||
|
return false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
void AutoStore::stop() |
||||||
|
{ |
||||||
|
log->info("Stopping AutoStore services..."); |
||||||
|
|
||||||
|
if (httpServer && httpServer->isRunning()) { |
||||||
|
httpServer->stop(); |
||||||
|
} |
||||||
|
log->info("AutoStore services stopped"); |
||||||
|
} |
||||||
|
|
||||||
|
} // namespace nxl::autostore
|
||||||
@ -0,0 +1,21 @@ |
|||||||
|
#include "AddItem.h" |
||||||
|
#include <stdexcept> |
||||||
|
|
||||||
|
namespace nxl::autostore::application { |
||||||
|
|
||||||
|
AddItem::AddItem(IItemRepository& itemRepository, IClock& clock, |
||||||
|
IOrderService& orderService) |
||||||
|
: itemRepository(itemRepository), clock(clock), orderService(orderService) |
||||||
|
{} |
||||||
|
|
||||||
|
domain::Item::Id_t AddItem::execute(domain::Item&& item) |
||||||
|
{ |
||||||
|
const auto currentTime = clock.getCurrentTime(); |
||||||
|
|
||||||
|
if (expirationPolicy.isExpired(item, currentTime)) { |
||||||
|
orderService.orderItem(item); |
||||||
|
} |
||||||
|
return itemRepository.save(item); |
||||||
|
} |
||||||
|
|
||||||
|
} // namespace nxl::autostore::application
|
||||||
@ -0,0 +1,27 @@ |
|||||||
|
#pragma once |
||||||
|
|
||||||
|
#include "domain/entities/Item.h" |
||||||
|
#include "domain/polices/ItemExpirationPolicy.h" |
||||||
|
#include "application/interfaces/IItemRepository.h" |
||||||
|
#include "application/interfaces/IClock.h" |
||||||
|
#include "application/interfaces/IOrderService.h" |
||||||
|
|
||||||
|
namespace nxl::autostore::application { |
||||||
|
|
||||||
|
class AddItem |
||||||
|
{ |
||||||
|
public: |
||||||
|
virtual ~AddItem() = default; |
||||||
|
|
||||||
|
AddItem(IItemRepository& itemRepository, IClock& clock, |
||||||
|
IOrderService& orderService); |
||||||
|
domain::Item::Id_t execute(domain::Item&& item); |
||||||
|
|
||||||
|
private: |
||||||
|
IItemRepository& itemRepository; |
||||||
|
IClock& clock; |
||||||
|
IOrderService& orderService; |
||||||
|
domain::ItemExpirationPolicy expirationPolicy; |
||||||
|
}; |
||||||
|
|
||||||
|
} // namespace nxl::autostore::application
|
||||||
@ -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,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(std::string_view message, int code, |
||||||
|
const nlohmann::json& data) |
||||||
|
{ |
||||||
|
nlohmann::json response; |
||||||
|
response["status"] = "error"; |
||||||
|
response["message"] = message; |
||||||
|
response["code"] = code; |
||||||
|
|
||||||
|
if (!data.is_null()) { |
||||||
|
response["data"] = data; |
||||||
|
} |
||||||
|
|
||||||
|
return response.dump(); |
||||||
|
} |
||||||
|
|
||||||
|
} // namespace nxl::autostore::infrastructure
|
||||||
@ -0,0 +1,20 @@ |
|||||||
|
#pragma once |
||||||
|
|
||||||
|
#include "nlohmann/json.hpp" |
||||||
|
#include <string> |
||||||
|
|
||||||
|
namespace nxl::autostore::infrastructure { |
||||||
|
|
||||||
|
class Jsend |
||||||
|
{ |
||||||
|
public: |
||||||
|
static std::string success(const nlohmann::json& data = nullptr); |
||||||
|
static std::string error(std::string_view message, int code = 500, |
||||||
|
const nlohmann::json& data = nullptr); |
||||||
|
|
||||||
|
private: |
||||||
|
Jsend() = delete; |
||||||
|
~Jsend() = delete; |
||||||
|
}; |
||||||
|
|
||||||
|
} // namespace nxl::autostore::infrastructure
|
||||||
@ -0,0 +1,76 @@ |
|||||||
|
#include "infrastructure/helpers/JsonItem.h" |
||||||
|
#include <chrono> |
||||||
|
#include <ctime> |
||||||
|
#include <stdexcept> |
||||||
|
#include <type_traits> |
||||||
|
|
||||||
|
namespace nxl::autostore::infrastructure { |
||||||
|
domain::Item JsonItem::fromJson(std::string_view jsonBody) |
||||||
|
{ |
||||||
|
auto json = nlohmann::json::parse(jsonBody); |
||||||
|
return fromJsonObj(json); |
||||||
|
} |
||||||
|
|
||||||
|
domain::Item JsonItem::fromJsonObj(const nlohmann::json& j) |
||||||
|
{ |
||||||
|
domain::Item item; |
||||||
|
item.id = j.value("id", ""); |
||||||
|
item.name = j.value("name", ""); |
||||||
|
item.orderUrl = j.value("orderUrl", ""); |
||||||
|
item.userId = j.value("userId", "default-user"); |
||||||
|
|
||||||
|
if (j["expirationDate"].is_number()) { |
||||||
|
// Handle numeric timestamp
|
||||||
|
time_t timestamp = j["expirationDate"]; |
||||||
|
item.expirationDate = std::chrono::system_clock::from_time_t(timestamp); |
||||||
|
} else if (j["expirationDate"].is_string()) { |
||||||
|
// Handle ISO 8601 string format
|
||||||
|
std::string dateStr = j["expirationDate"]; |
||||||
|
std::tm tm = {}; |
||||||
|
std::istringstream ss(dateStr); |
||||||
|
|
||||||
|
// Parse the ISO 8601 format
|
||||||
|
ss >> std::get_time(&tm, "%Y-%m-%dT%H:%M:%S"); |
||||||
|
if (ss.fail()) { |
||||||
|
throw std::runtime_error( |
||||||
|
"Invalid format for expirationDate string. Expected ISO 8601 format " |
||||||
|
"(YYYY-MM-DDTHH:MM:SS)."); |
||||||
|
} |
||||||
|
|
||||||
|
// Convert to time_t
|
||||||
|
time_t timestamp = std::mktime(&tm); |
||||||
|
if (timestamp == -1) { |
||||||
|
throw std::runtime_error( |
||||||
|
"Failed to convert expirationDate to timestamp."); |
||||||
|
} |
||||||
|
|
||||||
|
item.expirationDate = std::chrono::system_clock::from_time_t(timestamp); |
||||||
|
} else { |
||||||
|
throw std::runtime_error("Invalid type for expirationDate. Expected number " |
||||||
|
"(Unix timestamp) or string (ISO 8601 format)."); |
||||||
|
} |
||||||
|
|
||||||
|
if (item.name.empty()) { |
||||||
|
throw std::runtime_error("Item name is required"); |
||||||
|
} |
||||||
|
|
||||||
|
return item; |
||||||
|
} |
||||||
|
|
||||||
|
std::string JsonItem::toJson(const domain::Item& item) |
||||||
|
{ |
||||||
|
return toJsonObj(item).dump(); |
||||||
|
} |
||||||
|
|
||||||
|
nlohmann::json JsonItem::toJsonObj(const domain::Item& item) |
||||||
|
{ |
||||||
|
nlohmann::json j; |
||||||
|
j["id"] = item.id; |
||||||
|
j["name"] = item.name; |
||||||
|
j["expirationDate"] = |
||||||
|
std::chrono::system_clock::to_time_t(item.expirationDate); |
||||||
|
j["orderUrl"] = item.orderUrl; |
||||||
|
j["userId"] = item.userId; |
||||||
|
return j; |
||||||
|
} |
||||||
|
} // namespace nxl::autostore::infrastructure
|
||||||
@ -0,0 +1,22 @@ |
|||||||
|
#pragma once |
||||||
|
|
||||||
|
#include "domain/entities/Item.h" |
||||||
|
#include "nlohmann/json.hpp" |
||||||
|
#include <string> |
||||||
|
|
||||||
|
namespace nxl::autostore::infrastructure { |
||||||
|
|
||||||
|
class JsonItem |
||||||
|
{ |
||||||
|
public: |
||||||
|
static domain::Item fromJson(std::string_view jsonBody); |
||||||
|
static std::string toJson(const domain::Item& item); |
||||||
|
static nlohmann::json toJsonObj(const domain::Item& item); |
||||||
|
static domain::Item fromJsonObj(const nlohmann::json& j); |
||||||
|
|
||||||
|
private: |
||||||
|
JsonItem() = delete; |
||||||
|
~JsonItem() = delete; |
||||||
|
}; |
||||||
|
|
||||||
|
} // namespace nxl::autostore::infrastructure
|
||||||
@ -0,0 +1,35 @@ |
|||||||
|
#include "HttpOrderService.h" |
||||||
|
#include <stdexcept> |
||||||
|
|
||||||
|
namespace nxl::autostore::infrastructure { |
||||||
|
|
||||||
|
HttpOrderService::HttpOrderService(ILoggerPtr logger) : log{std::move(logger)} |
||||||
|
{} |
||||||
|
|
||||||
|
void HttpOrderService::orderItem(const domain::Item& item) |
||||||
|
{ |
||||||
|
if (item.orderUrl.empty()) { |
||||||
|
throw std::runtime_error("Order URL is empty for item: " + item.name); |
||||||
|
} |
||||||
|
|
||||||
|
std::string payload = |
||||||
|
R"({"itemName": ")" + item.name + R"(", "itemId": ")" + item.id + "\"}"; |
||||||
|
sendPostRequest(item.orderUrl, payload); |
||||||
|
} |
||||||
|
|
||||||
|
void HttpOrderService::sendPostRequest(std::string_view url, |
||||||
|
std::string_view payload) |
||||||
|
{ |
||||||
|
// In a real implementation, this would use an HTTP client library
|
||||||
|
// For now, we'll simulate the HTTP call
|
||||||
|
log->i("POST request to: %s", url); |
||||||
|
log->v(1, "Payload: %s", payload); |
||||||
|
|
||||||
|
// Simulate HTTP error handling
|
||||||
|
if (url.find("error") != std::string::npos) { |
||||||
|
throw std::runtime_error("Failed to send order request to: " |
||||||
|
+ std::string(url)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} // namespace nxl::autostore::infrastructure
|
||||||
@ -0,0 +1,20 @@ |
|||||||
|
#pragma once |
||||||
|
|
||||||
|
#include "application/interfaces/IOrderService.h" |
||||||
|
#include "domain/entities/Item.h" |
||||||
|
#include "autostore/ILogger.h" |
||||||
|
|
||||||
|
namespace nxl::autostore::infrastructure { |
||||||
|
|
||||||
|
class HttpOrderService : public application::IOrderService |
||||||
|
{ |
||||||
|
public: |
||||||
|
explicit HttpOrderService(ILoggerPtr logger); |
||||||
|
void orderItem(const domain::Item& item) override; |
||||||
|
|
||||||
|
private: |
||||||
|
ILoggerPtr log; |
||||||
|
void sendPostRequest(std::string_view url, std::string_view payload); |
||||||
|
}; |
||||||
|
|
||||||
|
} // namespace nxl::autostore::infrastructure
|
||||||
@ -0,0 +1,126 @@ |
|||||||
|
#include "infrastructure/http/HttpServer.h" |
||||||
|
#include <iostream> |
||||||
|
#include <future> |
||||||
|
|
||||||
|
namespace nxl::autostore::infrastructure { |
||||||
|
|
||||||
|
namespace { |
||||||
|
constexpr std::chrono::seconds defaultStartupTimeout{5}; |
||||||
|
} |
||||||
|
|
||||||
|
HttpServer::HttpServer(ILoggerPtr logger) : log{std::move(logger)} {} |
||||||
|
|
||||||
|
HttpServer::~HttpServer() |
||||||
|
{ |
||||||
|
if (running) { |
||||||
|
stop(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
bool HttpServer::start(int port, std::string_view host) |
||||||
|
{ |
||||||
|
if (running) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
// std::cout << "Starting HTTP server on " << host << ":" << port <<
|
||||||
|
// std::endl;
|
||||||
|
log->info("Starting HTTP server on %s:%d...", host.data(), port); |
||||||
|
|
||||||
|
std::promise<bool> startupPromise; |
||||||
|
std::future<bool> startupFuture = startupPromise.get_future(); |
||||||
|
|
||||||
|
serverThread = std::thread([host, port, this, &startupPromise]() { |
||||||
|
// std::cout << "Server thread started, binding to " << host << ":" << port
|
||||||
|
// << std::endl;
|
||||||
|
log->v(1, "Server thread started, binding to %s:%d...", host, port); |
||||||
|
|
||||||
|
try { |
||||||
|
// Try to bind to the port
|
||||||
|
if (!server.bind_to_port(host.data(), port)) { |
||||||
|
startupPromise.set_value(false); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
// Signal that the server has bound to the port
|
||||||
|
startupPromise.set_value(true); |
||||||
|
|
||||||
|
// std::cout << "Server thread listening on " << host << ":" << port
|
||||||
|
// << std::endl;
|
||||||
|
log->info("Server thread listening on %s:%d", host, port); |
||||||
|
|
||||||
|
// Start listening for connections
|
||||||
|
bool listenResult = server.listen_after_bind(); |
||||||
|
// std::cout << "Server stopped, listen result: " << listenResult
|
||||||
|
// << std::endl;
|
||||||
|
log->info("Server stopped, listen result: %d", listenResult); |
||||||
|
} catch (const std::exception& e) { |
||||||
|
// std::cerr << "Server thread exception: " << e.what() << std::endl;
|
||||||
|
log->error("Server thread exception: %s", e.what()); |
||||||
|
startupPromise.set_exception(std::current_exception()); |
||||||
|
} catch (...) { |
||||||
|
// std::cerr << "Server thread unknown exception" << std::endl;
|
||||||
|
log->error("Server thread unknown exception"); |
||||||
|
startupPromise.set_exception(std::current_exception()); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
// Wait for the server to start (with a timeout)
|
||||||
|
if (startupFuture.wait_for(defaultStartupTimeout) |
||||||
|
== std::future_status::timeout) { |
||||||
|
// std::cerr << "Failed to start HTTP server - timeout" << std::endl;
|
||||||
|
log->error("Failed to start HTTP server - timeout"); |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
try { |
||||||
|
// Check if the server bound to the port successfully
|
||||||
|
if (!startupFuture.get()) { |
||||||
|
// std::cerr << "Failed to start HTTP server - could not bind to port"
|
||||||
|
// << std::endl;
|
||||||
|
log->error("Failed to start HTTP server - could not bind to port"); |
||||||
|
return false; |
||||||
|
} |
||||||
|
} catch (const std::exception& e) { |
||||||
|
// std::cerr << "Failed to start HTTP server - " << e.what() << std::endl;
|
||||||
|
log->error("Failed to start HTTP server - %s", e.what()); |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
running = true; |
||||||
|
// std::cout << "HTTP server is running" << std::endl;
|
||||||
|
log->info("HTTP server is running"); |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
void HttpServer::stop() |
||||||
|
{ |
||||||
|
if (!running) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
// std::cout << "Stopping HTTP server..." << std::endl;
|
||||||
|
log->info("Stopping HTTP server..."); |
||||||
|
server.stop(); |
||||||
|
if (serverThread.joinable()) { |
||||||
|
// std::cout << "Waiting for server thread to join..." << std::endl;
|
||||||
|
log->info("Waiting for server thread to join..."); |
||||||
|
serverThread.join(); |
||||||
|
} |
||||||
|
|
||||||
|
running = false; |
||||||
|
// std::cout << "HTTP server stopped" << std::endl;
|
||||||
|
log->info("HTTP server stopped"); |
||||||
|
} |
||||||
|
|
||||||
|
bool HttpServer::isRunning() const |
||||||
|
{ |
||||||
|
return running; |
||||||
|
} |
||||||
|
|
||||||
|
httplib::Server& HttpServer::getServer() |
||||||
|
{ |
||||||
|
return server; |
||||||
|
} |
||||||
|
|
||||||
|
} // namespace nxl::autostore::infrastructure
|
||||||
@ -0,0 +1,30 @@ |
|||||||
|
#pragma once |
||||||
|
|
||||||
|
#include "autostore/ILogger.h" |
||||||
|
#include <httplib.h> |
||||||
|
#include <memory> |
||||||
|
#include <string> |
||||||
|
#include <thread> |
||||||
|
|
||||||
|
namespace nxl::autostore::infrastructure { |
||||||
|
|
||||||
|
class HttpServer |
||||||
|
{ |
||||||
|
public: |
||||||
|
explicit HttpServer(ILoggerPtr logger); |
||||||
|
~HttpServer(); |
||||||
|
|
||||||
|
bool start(int port = 8080, std::string_view host = "0.0.0.0"); |
||||||
|
void stop(); |
||||||
|
bool isRunning() const; |
||||||
|
|
||||||
|
httplib::Server& getServer(); |
||||||
|
|
||||||
|
private: |
||||||
|
ILoggerPtr log; |
||||||
|
bool running{false}; |
||||||
|
httplib::Server server; |
||||||
|
std::thread serverThread; |
||||||
|
}; |
||||||
|
|
||||||
|
} // namespace nxl::autostore::infrastructure
|
||||||
@ -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,53 @@ |
|||||||
|
#include "webapi/controllers/StoreController.h" |
||||||
|
#include "infrastructure/helpers/JsonItem.h" |
||||||
|
#include "infrastructure/helpers/Jsend.h" |
||||||
|
#include "application/commands/AddItem.h" |
||||||
|
|
||||||
|
namespace nxl::autostore::webapi { |
||||||
|
|
||||||
|
using infrastructure::Jsend; |
||||||
|
using infrastructure::JsonItem; |
||||||
|
|
||||||
|
StoreController::StoreController(Context&& context) |
||||||
|
: context{std::move(context)} |
||||||
|
{} |
||||||
|
|
||||||
|
void StoreController::registerRoutes(httplib::Server& server) |
||||||
|
{ |
||||||
|
server.Post("/api/items", |
||||||
|
[this](const httplib::Request& req, httplib::Response& res) { |
||||||
|
this->addItem(req, res); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
void StoreController::addItem(const httplib::Request& req, |
||||||
|
httplib::Response& res) |
||||||
|
{ |
||||||
|
try { |
||||||
|
if (req.body.empty()) { |
||||||
|
res.status = 400; |
||||||
|
res.set_content(Jsend::error("Request body is empty", 400), |
||||||
|
"application/json"); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
auto item = JsonItem::fromJson(req.body); |
||||||
|
try { |
||||||
|
nlohmann::json responseData = nlohmann::json::object(); |
||||||
|
responseData["id"] = context.addItemUc.execute(std::move(item)); |
||||||
|
res.status = 201; |
||||||
|
res.set_content(Jsend::success(responseData), "application/json"); |
||||||
|
} catch (const std::exception& e) { |
||||||
|
res.status = 500; |
||||||
|
res.set_content( |
||||||
|
Jsend::error("Failed to add item: " + std::string(e.what()), |
||||||
|
res.status), |
||||||
|
"application/json"); |
||||||
|
} |
||||||
|
} catch (const std::exception& e) { |
||||||
|
res.status = 400; |
||||||
|
res.set_content(Jsend::error(e.what(), res.status), "application/json"); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} // namespace nxl::autostore::webapi
|
||||||
@ -0,0 +1,27 @@ |
|||||||
|
#pragma once |
||||||
|
|
||||||
|
#include "application/commands/AddItem.h" |
||||||
|
#include <httplib.h> // TODO: forward declaration |
||||||
|
|
||||||
|
namespace nxl::autostore::webapi { |
||||||
|
|
||||||
|
class StoreController |
||||||
|
{ |
||||||
|
public: |
||||||
|
struct Context |
||||||
|
{ |
||||||
|
application::AddItem addItemUc; |
||||||
|
}; |
||||||
|
|
||||||
|
StoreController(Context&& context); |
||||||
|
|
||||||
|
void registerRoutes(httplib::Server& server); |
||||||
|
|
||||||
|
private: |
||||||
|
void addItem(const httplib::Request& req, httplib::Response& res); |
||||||
|
|
||||||
|
private: |
||||||
|
Context context; |
||||||
|
}; |
||||||
|
|
||||||
|
} // namespace nxl::autostore::webapi
|
||||||
@ -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", |
||||||
|
"spdlog", |
||||||
|
"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