Compare commits
16 Commits
cpp17-init
...
master
| Author | SHA1 | Date |
|---|---|---|
|
|
32facc9b6d | 3 months ago |
|
|
31f16d697a | 3 months ago |
|
|
2f93978456 | 3 months ago |
|
|
eedcfe1607 | 3 months ago |
|
|
5088253d00 | 3 months ago |
|
|
800fb66a65 | 3 months ago |
|
|
c374030abf | 3 months ago |
|
|
77eab34b5d | 4 months ago |
|
|
0cae61530f | 4 months ago |
|
|
2604928070 | 4 months ago |
|
|
4a990be146 | 4 months ago |
|
|
21c2c2a364 | 4 months ago |
|
|
1f142849ee | 4 months ago |
|
|
95a5a2ee04 | 4 months ago |
|
|
67edb98056 | 4 months ago |
|
|
f6b45a6923 | 4 months ago |
283 changed files with 37413 additions and 58 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,254 @@
|
||||
--- |
||||
Checks: >- |
||||
-*, |
||||
bugprone-assert-side-effect, |
||||
bugprone-bad-signal-to-kill-thread, |
||||
bugprone-bool-pointer-implicit-conversion, |
||||
bugprone-branch-clone, |
||||
bugprone-copy-constructor-init, |
||||
bugprone-dangling-handle, |
||||
bugprone-dynamic-static-initializers, |
||||
bugprone-exception-escape, |
||||
bugprone-forward-declaration-namespace, |
||||
bugprone-forwarding-reference-overload, |
||||
bugprone-inaccurate-erase, |
||||
bugprone-incorrect-roundings, |
||||
bugprone-infinite-loop, |
||||
bugprone-integer-division, |
||||
bugprone-macro-parentheses, |
||||
bugprone-misplaced-operator-in-strlen-in-alloc, |
||||
bugprone-misplaced-pointer-arithmetic-in-alloc, |
||||
bugprone-misplaced-widening-cast, |
||||
bugprone-move-forwarding-reference, |
||||
bugprone-multiple-statement-macro, |
||||
bugprone-not-null-terminated-result, |
||||
bugprone-parent-virtual-call, |
||||
bugprone-posix-return, |
||||
bugprone-signed-char-misuse, |
||||
bugprone-sizeof-container, |
||||
bugprone-sizeof-expression, |
||||
bugprone-spuriously-wake-up-functions, |
||||
bugprone-string-constructor, |
||||
bugprone-string-integer-assignment, |
||||
bugprone-string-literal-with-embedded-nul, |
||||
bugprone-suspicious-enum-usage, |
||||
bugprone-suspicious-include, |
||||
bugprone-suspicious-memset-usage, |
||||
bugprone-suspicious-missing-comma, |
||||
bugprone-suspicious-semicolon, |
||||
bugprone-suspicious-string-compare, |
||||
bugprone-swapped-arguments, |
||||
bugprone-terminating-continue, |
||||
bugprone-throw-keyword-missing, |
||||
bugprone-too-small-loop-variable, |
||||
bugprone-undefined-memory-manipulation, |
||||
bugprone-undelegated-constructor, |
||||
bugprone-unhandled-self-assignment, |
||||
bugprone-unhandled-self-assignment, |
||||
bugprone-unused-raii, |
||||
bugprone-use-after-move, |
||||
|
||||
cert-dcl21-cpp, |
||||
cert-dcl50-cpp, |
||||
cert-dcl58-cpp, |
||||
cert-env33-c, |
||||
cert-err34-c, |
||||
cert-err52-cpp, |
||||
cert-err58-cpp, |
||||
cert-err60-cpp, |
||||
cert-flp30-c, |
||||
cert-mem57-cpp, |
||||
cert-msc50-cpp, |
||||
cert-msc51-cpp, |
||||
cert-oop57-cpp, |
||||
cert-oop58-cpp, |
||||
|
||||
clang-analyzer-core.CallAndMessage, |
||||
clang-analyzer-core.DivideZero, |
||||
|
||||
clang-analyzer-core.*, |
||||
clang-analyzer-cplusplus.*, |
||||
clang-analyzer-deadcode.*, |
||||
clang-analyzer-nullability.*, |
||||
clang-analyzer-optin.*, |
||||
clang-analyzer-valist.*, |
||||
clang-analyzer-security.*, |
||||
|
||||
cppcoreguidelines-avoid-goto, |
||||
cppcoreguidelines-avoid-non-const-global-variables, |
||||
cppcoreguidelines-init-variables, |
||||
cppcoreguidelines-interfaces-global-init, |
||||
cppcoreguidelines-macro-usage, |
||||
cppcoreguidelines-narrowing-conversions, |
||||
cppcoreguidelines-no-malloc, |
||||
cppcoreguidelines-owning-memory, |
||||
cppcoreguidelines-pro-bounds-array-to-pointer-decay, |
||||
cppcoreguidelines-pro-bounds-constant-array-index, |
||||
cppcoreguidelines-pro-bounds-pointer-arithmetic, |
||||
cppcoreguidelines-pro-type-const-cast, |
||||
cppcoreguidelines-pro-type-cstyle-cast, |
||||
cppcoreguidelines-pro-type-member-init, |
||||
cppcoreguidelines-pro-type-reinterpret-cast, |
||||
cppcoreguidelines-pro-type-static-cast-downcast, |
||||
cppcoreguidelines-pro-type-union-access, |
||||
cppcoreguidelines-pro-type-vararg, |
||||
cppcoreguidelines-slicing, |
||||
cppcoreguidelines-special-member-functions, |
||||
|
||||
google-build-namespaces, |
||||
google-default-arguments, |
||||
google-explicit-constructor, |
||||
google-build-using-namespace, |
||||
google-global-names-in-headers, |
||||
google-readability-casting, |
||||
google-runtime-int, |
||||
google-runtime-operator, |
||||
|
||||
hicpp-exception-baseclass, |
||||
hicpp-multiway-paths-covered, |
||||
hicpp-no-assembler, |
||||
hicpp-signed-bitwise, |
||||
llvm-namespace-comment, |
||||
|
||||
misc-definitions-in-headers, |
||||
misc-misplaced-const, |
||||
misc-new-delete-overloads, |
||||
misc-no-recursion, |
||||
misc-non-copyable-objects, |
||||
misc-non-private-member-variables-in-classes, |
||||
misc-redundant-expression, |
||||
misc-static-assert, |
||||
misc-throw-by-value-catch-by-reference, |
||||
misc-unconventional-assign-operator, |
||||
misc-uniqueptr-reset-release, |
||||
misc-unused-parameters, |
||||
misc-unused-using-decls, |
||||
misc-unused-alias-decls, |
||||
|
||||
modernize-avoid-bind, |
||||
modernize-avoid-c-arrays, |
||||
modernize-concat-nested-namespaces, |
||||
modernize-deprecated-headers, |
||||
modernize-deprecated-ios-base-aliases, |
||||
modernize-loop-convert, |
||||
modernize-make-shared, |
||||
modernize-make-unique, |
||||
modernize-raw-string-literal, |
||||
modernize-redundant-void-arg, |
||||
modernize-replace-auto-ptr, |
||||
modernize-replace-disallow-copy-and-assign-macro, |
||||
modernize-replace-random-shuffle, |
||||
modernize-return-braced-init-list, |
||||
modernize-shrink-to-fit, |
||||
modernize-unary-static-assert, |
||||
modernize-use-auto, |
||||
modernize-use-bool-literals, |
||||
modernize-use-default-member-init, |
||||
modernize-use-emplace, |
||||
modernize-use-equals-default, |
||||
modernize-use-equals-delete, |
||||
modernize-use-nodiscard, |
||||
modernize-use-noexcept, |
||||
modernize-use-nullptr, |
||||
modernize-use-override, |
||||
modernize-use-transparent-functors, |
||||
modernize-use-uncaught-exceptions, |
||||
modernize-use-using, |
||||
|
||||
performance-faster-string-find, |
||||
performance-for-range-copy, |
||||
performance-implicit-conversion-in-loop, |
||||
performance-inefficient-algorithm, |
||||
performance-inefficient-string-concatenation, |
||||
performance-inefficient-vector-operation, |
||||
performance-move-const-arg, |
||||
performance-move-constructor-init, |
||||
performance-no-automatic-move, |
||||
performance-noexcept-move-constructor, |
||||
performance-trivially-destructible, |
||||
performance-type-promotion-in-math-fn, |
||||
performance-unnecessary-copy-initialization, |
||||
performance-unnecessary-value-param, |
||||
|
||||
readability-avoid-const-params-in-decls, |
||||
readability-braces-around-statements, |
||||
readability-const-return-type, |
||||
readability-container-size-empty, |
||||
readability-delete-null-pointer, |
||||
readability-deleted-default, |
||||
readability-else-after-return, |
||||
readability-function-size, |
||||
readability-identifier-naming, |
||||
readability-implicit-bool-conversion, |
||||
readability-inconsistent-declaration-parameter-name, |
||||
readability-isolate-declaration, |
||||
readability-magic-numbers, |
||||
readability-make-member-function-const, |
||||
readability-misleading-indentation, |
||||
readability-misplaced-array-index, |
||||
readability-named-parameter, |
||||
readability-non-const-parameter, |
||||
readability-redundant-control-flow, |
||||
readability-redundant-declaration, |
||||
readability-redundant-function-ptr-dereference, |
||||
readability-redundant-member-init, |
||||
readability-redundant-preprocessor, |
||||
readability-redundant-smartptr-get, |
||||
readability-redundant-string-cstr, |
||||
readability-redundant-string-init, |
||||
readability-simplify-boolean-expr, |
||||
readability-simplify-subscript-expr, |
||||
readability-static-accessed-through-instance, |
||||
readability-static-definition-in-anonymous-namespace, |
||||
readability-string-compare, |
||||
readability-uniqueptr-delete-release, |
||||
readability-uppercase-literal-suffix, |
||||
readability-use-anyofallof |
||||
WarningsAsErrors: '' |
||||
HeaderFilterRegex: '.*' |
||||
AnalyzeTemporaryDtors: false |
||||
FormatStyle: none |
||||
CheckOptions: |
||||
- key: cppcoreguidelines-special-member-functions.AllowSoleDefaultDtor |
||||
value: 1 |
||||
- key: modernize-use-nullptr.NullMacros |
||||
value: 'NULL' |
||||
- key: readability-function-size.LineThreshold |
||||
value: 50 |
||||
- key: readability-function-size.StatementThreshold |
||||
value: 800 |
||||
- key: readability-function-size.BranchThreshold |
||||
value: 10 |
||||
- key: readability-function-size.ParameterThreshold |
||||
value: 6 |
||||
- key: readability-function-size.NestingThreshold |
||||
value: 15 |
||||
- key: readability-function-size.VariableThreshold |
||||
value: 10 |
||||
- key: readability-identifier-naming.ClassCase |
||||
value: CamelCase |
||||
- key: readability-identifier-naming.MemberCase |
||||
value: camelBack |
||||
- key: readability-identifier-naming.ClassMemberCase |
||||
value: camelBack |
||||
- key: readability-identifier-naming.ClassMethodCase |
||||
value: camelBack |
||||
- key: readability-identifier-naming.MethodCase |
||||
value: camelBack |
||||
- key: readability-identifier-naming.ConstantCase |
||||
value: UPPER_CASE |
||||
- key: readability-identifier-naming.LocalConstantCase |
||||
value: camelBack |
||||
- key: readability-identifier-naming.NamespaceCase |
||||
value: lower_case |
||||
- key: readability-identifier-naming.ParameterCase |
||||
value: camelBack |
||||
- key: readability-identifier-naming.EnumCase |
||||
value: CamelCase |
||||
- key: readability-identifier-naming.EnumConstantCase |
||||
value: CamelCase |
||||
- key: readability-identifier-naming.FunctionCase |
||||
value: camelBack |
||||
- key: misc-non-private-member-variables-in-classes.IgnoreClassesWithAllMemberVariablesBeingPublic |
||||
value: 1 |
||||
... |
||||
@ -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,33 @@
|
||||
{ |
||||
"version": 3, |
||||
"configurePresets": [ |
||||
{ |
||||
"name": "debug", |
||||
"toolchainFile": "${env:VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", |
||||
"cacheVariables": { |
||||
"CMAKE_BUILD_TYPE": "Debug", |
||||
"CMAKE_EXPORT_COMPILE_COMMANDS": "TRUE" |
||||
} |
||||
}, |
||||
{ |
||||
"name": "release", |
||||
"toolchainFile": "${env:VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", |
||||
"cacheVariables": { |
||||
"CMAKE_BUILD_TYPE": "Release", |
||||
"CMAKE_EXPORT_COMPILE_COMMANDS": "TRUE" |
||||
} |
||||
} |
||||
], |
||||
"buildPresets": [ |
||||
{ |
||||
"name": "debug", |
||||
"configurePreset": "debug", |
||||
"jobs": 8 |
||||
}, |
||||
{ |
||||
"name": "release", |
||||
"configurePreset": "release", |
||||
"jobs": 8 |
||||
} |
||||
] |
||||
} |
||||
@ -0,0 +1,44 @@
|
||||
# Ovierview |
||||
|
||||
Read top-level `README.md` for more information on this repository. |
||||
|
||||
# Authentication |
||||
|
||||
No external service is used. JWT tokens are created and verified using `jwt-cpp` library. |
||||
Default, pre-defined user databse is a simple json file (`app/defaults/users.json`). |
||||
|
||||
# Build and Run |
||||
|
||||
```bash |
||||
cd docker |
||||
docker compose build |
||||
docker compose up |
||||
``` |
||||
|
||||
Note: do not use this for development. See `.devcontainer` directory for development setup. |
||||
|
||||
For non-container development, see Dockerfile and replicate the steps. Simple `build-and-test.sh` |
||||
script would look like this: |
||||
|
||||
```bash |
||||
#/bin/bash |
||||
|
||||
OUT_DIR=./out |
||||
DEBUG_DIR=$OUT_DIR/build/debug |
||||
|
||||
cmake -DCMAKE_BUILD_TYPE=Debug \ |
||||
-DCMAKE_EXPORT_COMPILE_COMMANDS=TRUE \ |
||||
-DCMAKE_TOOLCHAIN_FILE=/opt/vcpkg/scripts/buildsystems/vcpkg.cmake \ |
||||
-S /workspace -B $DEBUG_DIR |
||||
|
||||
cd "$DEBUG_DIR" |
||||
cmake --build . -- -j8 |
||||
ctest --output-on-failure . |
||||
|
||||
``` |
||||
|
||||
# Testing |
||||
|
||||
Unit tests are added to ctest and executed on docker build. Execute `ctest .` in build dir to run it. |
||||
|
||||
Use top-level testing/tavern scripts to run functional tests. |
||||
@ -0,0 +1,36 @@
|
||||
cmake_minimum_required(VERSION 3.20) |
||||
|
||||
project(AutoStoreApp LANGUAGES CXX) |
||||
set(TARGET_NAME AutoStore) |
||||
|
||||
set(CMAKE_CXX_STANDARD 17) |
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON) |
||||
|
||||
find_package(spdlog CONFIG REQUIRED) |
||||
|
||||
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} |
||||
) |
||||
|
||||
# Create data directory and copy defalut users.json for development |
||||
add_custom_command( |
||||
TARGET ${TARGET_NAME} POST_BUILD |
||||
COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/data" && ${CMAKE_COMMAND} -E copy |
||||
"${CMAKE_CURRENT_LIST_DIR}/defaults/users.json" |
||||
"${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/data/users.json" |
||||
) |
||||
|
||||
target_link_libraries(${TARGET_NAME} PRIVATE ${LIBRARIES}) |
||||
@ -0,0 +1,13 @@
|
||||
[ |
||||
{ |
||||
"username": "admin", |
||||
"password": "8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918", |
||||
"id": "1000" |
||||
}, |
||||
{ |
||||
"username": "user", |
||||
"password": "04f8996da763b7a969b1028ee3007569eaf3a635486ddab211d512c85b9df8fb", |
||||
"id": "1001" |
||||
} |
||||
] |
||||
|
||||
@ -0,0 +1,71 @@
|
||||
#include "App.h" |
||||
#include "SpdLogger.h" |
||||
#include "OsHelpers.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); |
||||
|
||||
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 = os::getApplicationDirectory() + "/data", |
||||
.host = "0.0.0.0", |
||||
.port = 50080, |
||||
}, |
||||
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 "autostore/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,48 @@
|
||||
#pragma once |
||||
|
||||
#include <string> |
||||
|
||||
#ifdef _WIN32 |
||||
|
||||
#include <windows.h> |
||||
|
||||
#ifndef PATH_MAX |
||||
#define PATH_MAX 4096 |
||||
#endif |
||||
|
||||
namespace nxl::os { |
||||
|
||||
std::string getApplicationDirectory() |
||||
{ |
||||
char exePath[PATH_MAX]; |
||||
GetModuleFileNameA(NULL, exePath, PATH_MAX); |
||||
std::string exeDir = std::string(dirname(exePath)); |
||||
return exeDir; |
||||
} |
||||
|
||||
} // namespace nxl::os
|
||||
|
||||
#else |
||||
#include <sys/types.h> |
||||
#include <unistd.h> |
||||
#include <libgen.h> |
||||
|
||||
namespace nxl::os { |
||||
|
||||
std::string getApplicationDirectory() |
||||
{ |
||||
char result[PATH_MAX] = {0}; |
||||
std::string path; |
||||
ssize_t count = readlink("/proc/self/exe", result, PATH_MAX); |
||||
if (count != -1) { |
||||
result[count] = '\0'; |
||||
path = dirname(result); |
||||
} else { |
||||
path = "./"; |
||||
} |
||||
return path; |
||||
} |
||||
|
||||
} // namespace nxl::os
|
||||
|
||||
#endif |
||||
@ -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,58 @@
|
||||
# Add item sequence |
||||
|
||||
```mermaid |
||||
sequenceDiagram |
||||
participant Client as HTTP Client |
||||
participant Middleware as HttpJwtMiddleware |
||||
participant Server as HttpServer |
||||
participant Controller as StoreController |
||||
participant AuthService as IAuthService |
||||
participant UseCase as AddItem |
||||
participant Clock as ITimeProvider |
||||
participant Policy as ItemExpirationPolicy |
||||
participant OrderService as IOrderService |
||||
participant HttpClient as HttpOrderService |
||||
participant Repo as IItemRepository |
||||
|
||||
Client->>Server: POST /items with JWT |
||||
Server->>Middleware: Validate token |
||||
Middleware-->>Server: token valid |
||||
Server->>Controller: Forward request |
||||
|
||||
Controller->>AuthService: Extract user ID from token |
||||
AuthService-->>Controller: User ID |
||||
|
||||
Controller->>Controller: Parse request body to Item |
||||
Controller->>UseCase: execute(item) |
||||
|
||||
UseCase->>Clock: now() |
||||
Clock-->>UseCase: current time |
||||
|
||||
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: void |
||||
end |
||||
|
||||
UseCase->>Repo: save(item) |
||||
Repo->>Repo: Persist to file storage |
||||
Repo-->>UseCase: Item ID |
||||
|
||||
UseCase-->>Controller: Item ID |
||||
Controller->>Controller: Build success response |
||||
Controller-->>Client: 201 Created with Item ID |
||||
|
||||
alt Error occurs |
||||
UseCase-->>Controller: Exception |
||||
Controller->>Controller: Build error response |
||||
Controller-->>Client: 4xx/5xx error |
||||
end |
||||
|
||||
alt Authentication fails |
||||
Middleware-->>Client: 401 Unauthorized |
||||
end |
||||
``` |
||||
@ -0,0 +1,82 @@
|
||||
# AutoStore Architecture Overview |
||||
|
||||
## Layer Boundaries |
||||
|
||||
```mermaid |
||||
graph TB |
||||
subgraph PL[Presentation Layer] |
||||
A[StoreController] |
||||
B[AuthController] |
||||
end |
||||
|
||||
subgraph AL[Application Layer] |
||||
E[AddItem Use Case] |
||||
F[DeleteItem Use Case] |
||||
G[LoginUser Use Case] |
||||
H[GetItem Use Case] |
||||
I[ListItems Use Case] |
||||
J[HandleExpiredItems Use Case] |
||||
K[TaskScheduler] |
||||
L[IItemRepository] |
||||
M[IAuthService] |
||||
N[IOrderService] |
||||
O[ITimeProvider] |
||||
P[IThreadManager] |
||||
end |
||||
|
||||
subgraph DL[Domain Layer] |
||||
Q[Item] |
||||
R[User] |
||||
S[ItemExpirationPolicy] |
||||
end |
||||
|
||||
subgraph IL[Infrastructure Layer] |
||||
C[HttpServer] |
||||
D[HttpJwtMiddleware] |
||||
T[FileItemRepository] |
||||
U[FileJwtAuthService] |
||||
V[HttpOrderService] |
||||
W[SystemTimeProvider] |
||||
X[SystemThreadManager] |
||||
Y[CvBlocker] |
||||
end |
||||
``` |
||||
|
||||
## Component Dependencies |
||||
|
||||
```mermaid |
||||
graph LR |
||||
StoreController --> AddItem |
||||
StoreController --> DeleteItem |
||||
StoreController --> GetItem |
||||
StoreController --> ListItems |
||||
StoreController --> IAuthService |
||||
|
||||
AuthController --> LoginUser |
||||
|
||||
AddItem --> IItemRepository |
||||
AddItem --> ITimeProvider |
||||
AddItem --> IOrderService |
||||
AddItem --> ItemExpirationPolicy |
||||
|
||||
DeleteItem --> IItemRepository |
||||
|
||||
LoginUser --> IAuthService |
||||
|
||||
GetItem --> IItemRepository |
||||
|
||||
ListItems --> IItemRepository |
||||
|
||||
HandleExpiredItems --> IItemRepository |
||||
HandleExpiredItems --> ITimeProvider |
||||
HandleExpiredItems --> IOrderService |
||||
HandleExpiredItems --> ItemExpirationPolicy |
||||
|
||||
TaskScheduler --> ITimeProvider |
||||
TaskScheduler --> IThreadManager |
||||
|
||||
IItemRepository --> Item |
||||
IAuthService --> User |
||||
IOrderService --> Item |
||||
ItemExpirationPolicy --> Item |
||||
ItemExpirationPolicy --> ITimeProvider |
||||
@ -0,0 +1,28 @@
|
||||
FROM kuyoh/vcpkg:2025.06.13-ubuntu24.04 AS builder |
||||
|
||||
WORKDIR /workspace |
||||
|
||||
COPY ../CMakeLists.txt . |
||||
COPY ../vcpkg.json . |
||||
|
||||
RUN vcpkg install |
||||
|
||||
# Cche stays valid if only code changes |
||||
COPY .. . |
||||
|
||||
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 8 -- |
||||
|
||||
# run tests |
||||
RUN cd /workspace/build && ctest --output-on-failure . |
||||
|
||||
FROM ubuntu:24.04 AS runtime |
||||
|
||||
WORKDIR /app |
||||
|
||||
COPY --from=builder /workspace/build/bin/AutoStore ./AutoStore |
||||
COPY --from=builder /workspace/build/bin/data ./data |
||||
|
||||
CMD ["./AutoStore"] |
||||
@ -0,0 +1,10 @@
|
||||
version: "3.9" |
||||
services: |
||||
app: |
||||
build: |
||||
context: .. |
||||
dockerfile: docker/Dockerfile |
||||
image: autostore-build-cpp-vcpkg-img |
||||
container_name: autostore-build-cpp-vcpkg |
||||
ports: |
||||
- 50080:50080 |
||||
@ -0,0 +1,62 @@
|
||||
cmake_minimum_required(VERSION 3.20) |
||||
project(AutoStoreLib LANGUAGES CXX VERSION 0.1.0) |
||||
set(TARGET_NAME AutoStoreLib) |
||||
|
||||
set(CMAKE_CXX_STANDARD 17) |
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON) |
||||
|
||||
# Find dependencies |
||||
find_package(httplib CONFIG REQUIRED) |
||||
find_package(nlohmann_json CONFIG REQUIRED) |
||||
find_package(jwt-cpp CONFIG REQUIRED) |
||||
|
||||
configure_file(src/Version.h.in ${CMAKE_BINARY_DIR}/autostore/Version.h) |
||||
|
||||
add_library(${TARGET_NAME} STATIC |
||||
src/domain/helpers/Specification.cpp |
||||
src/application/queries/GetItem.cpp |
||||
src/application/queries/ListItems.cpp |
||||
src/application/commands/AddItem.cpp |
||||
src/application/commands/HandleExpiredItems.cpp |
||||
src/application/commands/DeleteItem.cpp |
||||
src/application/commands/LoginUser.cpp |
||||
src/infrastructure/repositories/FileItemRepository.cpp |
||||
src/infrastructure/http/HttpServer.cpp |
||||
src/infrastructure/http/HttpJwtMiddleware.cpp |
||||
src/infrastructure/http/HttpOrderService.cpp |
||||
src/infrastructure/helpers/Jsend.cpp |
||||
src/infrastructure/helpers/JsonItem.cpp |
||||
src/infrastructure/auth/FileJwtAuthService.cpp |
||||
src/application/services/TaskScheduler.cpp |
||||
src/infrastructure/adapters/CvBlocker.cpp |
||||
src/infrastructure/adapters/SystemThreadManager.cpp |
||||
src/infrastructure/adapters/SystemTimeProvider.cpp |
||||
src/webapi/controllers/BaseController.cpp |
||||
src/webapi/controllers/StoreController.cpp |
||||
src/webapi/controllers/AuthController.cpp |
||||
src/AutoStore.cpp |
||||
) |
||||
|
||||
target_include_directories(${TARGET_NAME} |
||||
PUBLIC |
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/autostore |
||||
PRIVATE |
||||
${CMAKE_CURRENT_SOURCE_DIR}/src |
||||
${CMAKE_BINARY_DIR} |
||||
) |
||||
|
||||
target_sources(${TARGET_NAME} |
||||
PUBLIC |
||||
FILE_SET HEADERS |
||||
BASE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/include |
||||
FILES |
||||
include/autostore/AutoStore.h |
||||
include/autostore/ILogger.h |
||||
) |
||||
|
||||
target_link_libraries(${TARGET_NAME} |
||||
PUBLIC |
||||
httplib::httplib |
||||
nlohmann_json::nlohmann_json |
||||
jwt-cpp::jwt-cpp |
||||
) |
||||
@ -0,0 +1,67 @@
|
||||
#pragma once |
||||
|
||||
#include "autostore/ILogger.h" |
||||
#include <memory> |
||||
#include <string> |
||||
#include <string_view> |
||||
#include <thread> |
||||
|
||||
namespace nxl::autostore { |
||||
|
||||
namespace application { |
||||
class IItemRepository; |
||||
class ITimeProvider; |
||||
class IOrderService; |
||||
class IAuthService; |
||||
class IThreadManager; |
||||
class TaskScheduler; |
||||
} // namespace application
|
||||
|
||||
namespace infrastructure { |
||||
class HttpServer; |
||||
} // namespace infrastructure
|
||||
|
||||
namespace webapi { |
||||
class StoreController; |
||||
class AuthController; |
||||
} // namespace webapi
|
||||
|
||||
namespace application { |
||||
class AddItem; |
||||
class LoginUser; |
||||
} // namespace application
|
||||
|
||||
class AutoStore |
||||
{ |
||||
public: |
||||
struct Config |
||||
{ |
||||
std::string dataPath; |
||||
std::string host{"0.0.0.0"}; |
||||
uint16_t port{50080}; |
||||
}; |
||||
|
||||
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<application::TaskScheduler> taskScheduler; |
||||
std::unique_ptr<webapi::StoreController> storeController; |
||||
std::unique_ptr<webapi::AuthController> authController; |
||||
std::unique_ptr<application::IItemRepository> itemRepository; |
||||
std::unique_ptr<application::ITimeProvider> clock; |
||||
std::unique_ptr<application::IOrderService> orderService; |
||||
std::unique_ptr<application::IAuthService> authService; |
||||
std::unique_ptr<application::ITimeProvider> timeProvider; |
||||
std::unique_ptr<application::IThreadManager> threadManager; |
||||
}; |
||||
|
||||
} // 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 arg.data(); |
||||
} 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,132 @@
|
||||
#include "AutoStore.h" |
||||
#include "infrastructure/repositories/FileItemRepository.h" |
||||
#include "infrastructure/adapters/SystemTimeProvider.h" |
||||
#include "infrastructure/http/HttpOrderService.h" |
||||
#include "infrastructure/auth/FileJwtAuthService.h" |
||||
#include "webapi/controllers/StoreController.h" |
||||
#include "webapi/controllers/AuthController.h" |
||||
#include "infrastructure/http/HttpServer.h" |
||||
#include "application/services/TaskScheduler.h" |
||||
#include "application/commands/HandleExpiredItems.h" |
||||
#include "infrastructure/adapters/SystemTimeProvider.h" |
||||
#include "infrastructure/adapters/SystemThreadManager.h" |
||||
#include "infrastructure/adapters/CvBlocker.h" |
||||
#include <iostream> |
||||
#include <filesystem> |
||||
#include <memory> |
||||
|
||||
namespace nxl::autostore { |
||||
|
||||
using namespace infrastructure; |
||||
using namespace application; |
||||
|
||||
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<FileItemRepository>(itemsDbPath); |
||||
|
||||
clock = std::make_unique<SystemTimeProvider>(); |
||||
|
||||
orderService = std::make_unique<HttpOrderService>(log); |
||||
|
||||
// Initialize auth service
|
||||
std::string usersDbPath = |
||||
std::filesystem::path(config.dataPath) / "users.json"; |
||||
authService = std::make_unique<FileJwtAuthService>(usersDbPath); |
||||
|
||||
// Initialize dependencies for task scheduler
|
||||
timeProvider = std::make_unique<SystemTimeProvider>(); |
||||
threadManager = std::make_unique<SystemThreadManager>(); |
||||
auto blocker = std::make_unique<CvBlocker>(); |
||||
|
||||
// Initialize task scheduler (for handling expired items)
|
||||
taskScheduler = std::make_unique<TaskScheduler>( |
||||
log, *timeProvider, *threadManager, std::move(blocker)); |
||||
|
||||
// Initialize HTTP server
|
||||
httpServer = std::make_unique<HttpServer>(log, *authService); |
||||
|
||||
// Initialize store controller
|
||||
storeController = std::make_unique<webapi::StoreController>( |
||||
webapi::StoreController::Context{ |
||||
application::AddItem{*itemRepository, *clock, *orderService}, |
||||
application::ListItems{*itemRepository}, |
||||
application::GetItem{*itemRepository}, |
||||
application::DeleteItem{*itemRepository}, *authService}); |
||||
|
||||
// Initialize auth controller
|
||||
authController = std::make_unique<webapi::AuthController>( |
||||
webapi::AuthController::Context{application::LoginUser{*authService}}); |
||||
|
||||
log->info("Data path: %s", config.dataPath); |
||||
log->info("AutoStore initialized successfully, handling expired items..."); |
||||
|
||||
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 { |
||||
taskScheduler->schedule( |
||||
[this]() { |
||||
application::HandleExpiredItems{*itemRepository, *clock, *orderService} |
||||
.execute(); |
||||
}, |
||||
00, 00, 00, // midnight (00:00:00)
|
||||
TaskScheduler::RunMode::Forever | TaskScheduler::RunMode::OnStart); |
||||
taskScheduler->start(); |
||||
|
||||
storeController->registerRoutes(httpServer->getServer()); |
||||
authController->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/v1", 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 @@
|
||||
#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,27 @@
|
||||
#include "AddItem.h" |
||||
#include <stdexcept> |
||||
|
||||
namespace nxl::autostore::application { |
||||
|
||||
AddItem::AddItem(IItemRepository& itemRepository, ITimeProvider& clock, |
||||
IOrderService& orderService) |
||||
: itemRepository(itemRepository), clock(clock), orderService(orderService) |
||||
{} |
||||
|
||||
domain::Item::Id_t AddItem::execute(domain::Item&& item) |
||||
{ |
||||
if (item.userId == domain::User::NULL_ID) { |
||||
throw std::runtime_error("User ID not provided"); |
||||
} |
||||
|
||||
const auto currentTime = clock.now(); |
||||
|
||||
if (expirationPolicy.isExpired(item, currentTime)) { |
||||
item.id = domain::Item::NULL_ID; |
||||
orderService.orderItem(item); |
||||
return item.id; // TODO: test that
|
||||
} |
||||
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/ITimeProvider.h" |
||||
#include "application/interfaces/IOrderService.h" |
||||
|
||||
namespace nxl::autostore::application { |
||||
|
||||
class AddItem |
||||
{ |
||||
public: |
||||
virtual ~AddItem() = default; |
||||
|
||||
AddItem(IItemRepository& itemRepository, ITimeProvider& clock, |
||||
IOrderService& orderService); |
||||
domain::Item::Id_t execute(domain::Item&& item); |
||||
|
||||
private: |
||||
IItemRepository& itemRepository; |
||||
ITimeProvider& clock; |
||||
IOrderService& orderService; |
||||
domain::ItemExpirationPolicy expirationPolicy; |
||||
}; |
||||
|
||||
} // namespace nxl::autostore::application
|
||||
@ -0,0 +1,19 @@
|
||||
#include "DeleteItem.h" |
||||
#include "application/exceptions/AutoStoreExceptions.h" |
||||
|
||||
namespace nxl::autostore::application { |
||||
|
||||
DeleteItem::DeleteItem(IItemRepository& itemRepository) |
||||
: itemRepository(itemRepository) |
||||
{} |
||||
|
||||
void DeleteItem::execute(domain::Item::Id_t id, domain::User::Id_t ownerId) |
||||
{ |
||||
auto item = itemRepository.findById(id); |
||||
if (!item || item->userId != ownerId) { |
||||
throw ItemNotFoundException("Item not found"); |
||||
} |
||||
itemRepository.remove(id); |
||||
} |
||||
|
||||
} // namespace nxl::autostore::application
|
||||
@ -0,0 +1,21 @@
|
||||
#pragma once |
||||
|
||||
#include "domain/entities/Item.h" |
||||
#include "application/interfaces/IItemRepository.h" |
||||
#include <string_view> |
||||
|
||||
namespace nxl::autostore::application { |
||||
|
||||
class DeleteItem |
||||
{ |
||||
public: |
||||
virtual ~DeleteItem() = default; |
||||
|
||||
explicit DeleteItem(IItemRepository& itemRepository); |
||||
void execute(domain::Item::Id_t id, domain::User::Id_t ownerId); |
||||
|
||||
private: |
||||
IItemRepository& itemRepository; |
||||
}; |
||||
|
||||
} // namespace nxl::autostore::application
|
||||
@ -0,0 +1,29 @@
|
||||
#include "HandleExpiredItems.h" |
||||
#include <stdexcept> |
||||
|
||||
namespace nxl::autostore::application { |
||||
|
||||
HandleExpiredItems::HandleExpiredItems(IItemRepository& itemRepository, |
||||
ITimeProvider& clock, |
||||
IOrderService& orderService) |
||||
: itemRepository(itemRepository), clock(clock), orderService(orderService) |
||||
{} |
||||
|
||||
uint16_t HandleExpiredItems::execute() |
||||
{ |
||||
const auto currentTime = clock.now(); |
||||
|
||||
auto items = itemRepository.findWhere([&](const domain::Item& i) { |
||||
return expirationPolicy.isExpired(i, currentTime); |
||||
}); |
||||
|
||||
// remove expired one and order a new one
|
||||
for (auto& item : items) { |
||||
itemRepository.remove(item.id); |
||||
orderService.orderItem(item); |
||||
} |
||||
|
||||
return items.size(); |
||||
} |
||||
|
||||
} // namespace nxl::autostore::application
|
||||
@ -0,0 +1,30 @@
|
||||
#pragma once |
||||
|
||||
#include "domain/entities/Item.h" |
||||
#include "domain/polices/ItemExpirationPolicy.h" |
||||
#include "application/interfaces/IItemRepository.h" |
||||
#include "application/interfaces/ITimeProvider.h" |
||||
#include "application/interfaces/IOrderService.h" |
||||
|
||||
namespace nxl::autostore::application { |
||||
|
||||
class HandleExpiredItems |
||||
{ |
||||
public: |
||||
virtual ~HandleExpiredItems() = default; |
||||
|
||||
HandleExpiredItems(IItemRepository& itemRepository, ITimeProvider& clock, |
||||
IOrderService& orderService); |
||||
/**
|
||||
* @returns number of expired items |
||||
*/ |
||||
uint16_t execute(); |
||||
|
||||
private: |
||||
IItemRepository& itemRepository; |
||||
ITimeProvider& clock; |
||||
IOrderService& orderService; |
||||
domain::ItemExpirationPolicy expirationPolicy; |
||||
}; |
||||
|
||||
} // namespace nxl::autostore::application
|
||||
@ -0,0 +1,20 @@
|
||||
#include "LoginUser.h" |
||||
#include <stdexcept> |
||||
|
||||
namespace nxl::autostore::application { |
||||
|
||||
LoginUser::LoginUser(IAuthService& authService) : authService(authService) {} |
||||
|
||||
std::string LoginUser::execute(std::string_view username, |
||||
std::string_view password) |
||||
{ |
||||
auto userId = authService.authenticateUser(username, password); |
||||
if (!userId) { |
||||
throw std::runtime_error("Invalid username or password"); |
||||
} |
||||
|
||||
// Generate a token for the authenticated user
|
||||
return authService.generateToken(*userId); |
||||
} |
||||
|
||||
} // namespace nxl::autostore::application
|
||||
@ -0,0 +1,20 @@
|
||||
#pragma once |
||||
|
||||
#include "domain/entities/User.h" |
||||
#include "application/interfaces/IAuthService.h" |
||||
|
||||
namespace nxl::autostore::application { |
||||
|
||||
class LoginUser |
||||
{ |
||||
public: |
||||
virtual ~LoginUser() = default; |
||||
|
||||
LoginUser(IAuthService& authService); |
||||
std::string execute(std::string_view username, std::string_view password); |
||||
|
||||
private: |
||||
IAuthService& authService; |
||||
}; |
||||
|
||||
} // namespace nxl::autostore::application
|
||||
@ -0,0 +1,16 @@
|
||||
#pragma once |
||||
|
||||
#include <stdexcept> |
||||
#include <string> |
||||
|
||||
namespace nxl::autostore::application { |
||||
|
||||
class ItemNotFoundException : public std::runtime_error |
||||
{ |
||||
public: |
||||
explicit ItemNotFoundException(const std::string& message) |
||||
: std::runtime_error(message) |
||||
{} |
||||
}; |
||||
|
||||
} // namespace nxl::autostore::application
|
||||
@ -0,0 +1,21 @@
|
||||
#pragma once |
||||
|
||||
#include "domain/entities/User.h" |
||||
#include <string> |
||||
#include <string_view> |
||||
#include <optional> |
||||
|
||||
namespace nxl::autostore::application { |
||||
|
||||
class IAuthService |
||||
{ |
||||
public: |
||||
virtual ~IAuthService() = default; |
||||
virtual std::optional<domain::User::Id_t> |
||||
authenticateUser(std::string_view username, std::string_view password) = 0; |
||||
virtual std::string generateToken(std::string_view userId) = 0; |
||||
virtual std::optional<domain::User::Id_t> |
||||
extractUserId(std::string_view token) = 0; |
||||
}; |
||||
|
||||
} // namespace nxl::autostore::application
|
||||
@ -0,0 +1,20 @@
|
||||
#pragma once |
||||
|
||||
#include <chrono> |
||||
|
||||
namespace nxl::autostore::application { |
||||
|
||||
class IBlocker |
||||
{ |
||||
public: |
||||
using TimePoint = std::chrono::system_clock::time_point; |
||||
virtual ~IBlocker() = default; |
||||
virtual void block() = 0; |
||||
virtual void blockFor(const std::chrono::milliseconds& duration) = 0; |
||||
virtual void blockUntil(const TimePoint& timePoint) = 0; |
||||
virtual void notify() = 0; |
||||
virtual bool isBlocked() = 0; |
||||
virtual bool wasNotified() = 0; |
||||
}; |
||||
|
||||
} // namespace nxl::autostore::application
|
||||
@ -0,0 +1,27 @@
|
||||
#pragma once |
||||
|
||||
#include "domain/entities/Item.h" |
||||
#include "domain/polices/ItemExpirationPolicy.h" |
||||
#include <optional> |
||||
#include <functional> |
||||
#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(domain::Item::Id_t id) = 0; |
||||
virtual std::vector<domain::Item> findByOwner(domain::User::Id_t ownerId) = 0; |
||||
virtual std::vector<domain::Item> |
||||
findWhere(std::function<bool(const domain::Item&)> predicate) = 0; |
||||
virtual std::vector<domain::Item> |
||||
findWhere(const domain::ItemExpirationSpec& spec) = 0; |
||||
virtual void remove(domain::Item::Id_t 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,28 @@
|
||||
#pragma once |
||||
|
||||
#include <functional> |
||||
#include <thread> |
||||
|
||||
namespace nxl::autostore::application { |
||||
|
||||
class IThreadManager |
||||
{ |
||||
public: |
||||
class ThreadHandle |
||||
{ |
||||
public: |
||||
virtual ~ThreadHandle() = default; |
||||
virtual void join() = 0; |
||||
virtual bool joinable() const = 0; |
||||
}; |
||||
|
||||
using ThreadHandlePtr = std::unique_ptr<ThreadHandle>; |
||||
|
||||
virtual ~IThreadManager() = default; |
||||
|
||||
virtual ThreadHandlePtr createThread(std::function<void()> func) = 0; |
||||
virtual std::thread::id getCurrentThreadId() const = 0; |
||||
virtual void sleep(const std::chrono::milliseconds& duration) = 0; |
||||
}; |
||||
|
||||
} // namespace nxl::autostore::application
|
||||
@ -0,0 +1,18 @@
|
||||
#pragma once |
||||
|
||||
#include <chrono> |
||||
|
||||
namespace nxl::autostore::application { |
||||
|
||||
class ITimeProvider |
||||
{ |
||||
public: |
||||
using Clock = std::chrono::system_clock; |
||||
virtual ~ITimeProvider() = default; |
||||
|
||||
virtual Clock::time_point now() const = 0; |
||||
virtual std::tm to_tm(const Clock::time_point& timePoint) const = 0; |
||||
virtual Clock::time_point from_tm(const std::tm& tm) const = 0; |
||||
}; |
||||
|
||||
} // namespace nxl::autostore::application
|
||||
@ -0,0 +1,21 @@
|
||||
#include "GetItem.h" |
||||
#include "../exceptions/AutoStoreExceptions.h" |
||||
|
||||
namespace nxl::autostore::application { |
||||
|
||||
GetItem::GetItem(IItemRepository& itemRepository) |
||||
: itemRepository(itemRepository) |
||||
{} |
||||
|
||||
std::optional<domain::Item> GetItem::execute(domain::Item::Id_t id, |
||||
domain::User::Id_t ownerId) |
||||
{ |
||||
auto item = itemRepository.findById(id); |
||||
// act as not found when ownerId does not match for security
|
||||
if (!item || item->userId != ownerId) { |
||||
throw ItemNotFoundException("Item not found"); |
||||
} |
||||
return item; |
||||
} |
||||
|
||||
} // namespace nxl::autostore::application
|
||||
@ -0,0 +1,23 @@
|
||||
#pragma once |
||||
|
||||
#include "domain/entities/Item.h" |
||||
#include "application/interfaces/IItemRepository.h" |
||||
#include <optional> |
||||
#include <string_view> |
||||
|
||||
namespace nxl::autostore::application { |
||||
|
||||
class GetItem |
||||
{ |
||||
public: |
||||
virtual ~GetItem() = default; |
||||
|
||||
explicit GetItem(IItemRepository& itemRepository); |
||||
std::optional<domain::Item> execute(domain::Item::Id_t id, |
||||
domain::User::Id_t ownerId); |
||||
|
||||
private: |
||||
IItemRepository& itemRepository; |
||||
}; |
||||
|
||||
} // namespace nxl::autostore::application
|
||||
@ -0,0 +1,14 @@
|
||||
#include "ListItems.h" |
||||
|
||||
namespace nxl::autostore::application { |
||||
|
||||
ListItems::ListItems(IItemRepository& itemRepository) |
||||
: itemRepository(itemRepository) |
||||
{} |
||||
|
||||
std::vector<domain::Item> ListItems::execute(domain::User::Id_t ownerId) |
||||
{ |
||||
return itemRepository.findByOwner(ownerId); |
||||
} |
||||
|
||||
} // namespace nxl::autostore::application
|
||||
@ -0,0 +1,21 @@
|
||||
#pragma once |
||||
|
||||
#include "domain/entities/Item.h" |
||||
#include "application/interfaces/IItemRepository.h" |
||||
#include <vector> |
||||
|
||||
namespace nxl::autostore::application { |
||||
|
||||
class ListItems |
||||
{ |
||||
public: |
||||
virtual ~ListItems() = default; |
||||
|
||||
explicit ListItems(IItemRepository& itemRepository); |
||||
std::vector<domain::Item> execute(domain::User::Id_t ownerId); |
||||
|
||||
private: |
||||
IItemRepository& itemRepository; |
||||
}; |
||||
|
||||
} // namespace nxl::autostore::application
|
||||
@ -0,0 +1,229 @@
|
||||
#include "TaskScheduler.h" |
||||
#include <stdexcept> |
||||
|
||||
namespace nxl::autostore::application { |
||||
|
||||
namespace { |
||||
using Clock = std::chrono::system_clock; |
||||
using TimePoint = Clock::time_point; |
||||
using Duration = Clock::duration; |
||||
using Hours = std::chrono::hours; |
||||
using Minutes = std::chrono::minutes; |
||||
using Seconds = std::chrono::seconds; |
||||
using Milliseconds = std::chrono::milliseconds; |
||||
using Days = std::chrono::duration<int, std::ratio<86400>>; |
||||
using RunMode = TaskScheduler::RunMode; |
||||
|
||||
bool isValidTime(int hour, int minute, int second) |
||||
{ |
||||
return (hour >= 0 && hour <= 23) && (minute >= 0 && minute <= 59) |
||||
&& (second >= 0 && second <= 59); |
||||
} |
||||
|
||||
bool areModesMutuallyExclusive(RunMode mode) |
||||
{ |
||||
return (static_cast<int>(mode) & static_cast<int>(RunMode::Forever)) |
||||
&& (static_cast<int>(mode) & static_cast<int>(RunMode::Once)); |
||||
} |
||||
|
||||
TimePoint todayAt(uint8_t hour, uint8_t minute, uint8_t second, |
||||
const ITimeProvider& timeProvider) |
||||
{ |
||||
auto now = timeProvider.now(); |
||||
auto midnight = |
||||
std::chrono::time_point_cast<Duration>(std::chrono::floor<Days>(now)); |
||||
auto offset = Hours{hour} + Minutes{minute} + Seconds{second}; |
||||
return midnight + offset; |
||||
} |
||||
|
||||
bool shouldExecuteOnStart(const TaskScheduler::ScheduledTask& task) |
||||
{ |
||||
return (static_cast<int>(task.mode) & static_cast<int>(RunMode::OnStart)) |
||||
&& !task.executed; |
||||
} |
||||
|
||||
TimePoint calculateNextExecutionTime(const TaskScheduler::ScheduledTask& task, |
||||
const ITimeProvider& timeProvider, |
||||
TimePoint now) |
||||
{ |
||||
auto taskTime = todayAt(task.hour, task.minute, task.second, timeProvider); |
||||
|
||||
if (taskTime <= now) { |
||||
if (static_cast<int>(task.mode) & static_cast<int>(RunMode::Forever)) { |
||||
taskTime += Hours(24); |
||||
} else if ((static_cast<int>(task.mode) & static_cast<int>(RunMode::Once)) |
||||
&& task.executed) { |
||||
return TimePoint{}; |
||||
} |
||||
} |
||||
|
||||
return taskTime; |
||||
} |
||||
|
||||
bool shouldExecuteBasedOnTime(const TaskScheduler::ScheduledTask& task, |
||||
TimePoint now) |
||||
{ |
||||
if (task.nextExecution == TimePoint{}) { |
||||
return false; |
||||
} |
||||
return task.nextExecution <= now; |
||||
} |
||||
|
||||
void executeTask(TaskScheduler::ScheduledTask& task, ILogger& logger, |
||||
const ITimeProvider& timeProvider, TimePoint& nextWakeupTime) |
||||
{ |
||||
try { |
||||
task.function(); |
||||
task.executed = true; |
||||
logger.info("Task executed successfully"); |
||||
|
||||
if (static_cast<int>(task.mode) & static_cast<int>(RunMode::Forever)) { |
||||
auto nextTaskTime = |
||||
todayAt(task.hour, task.minute, task.second, timeProvider); |
||||
nextTaskTime += Hours(24); |
||||
task.nextExecution = nextTaskTime; |
||||
|
||||
if (nextTaskTime < nextWakeupTime) { |
||||
nextWakeupTime = nextTaskTime; |
||||
} |
||||
} |
||||
} catch (const std::exception& e) { |
||||
logger.error("Task execution failed: %s", e.what()); |
||||
} |
||||
} |
||||
|
||||
void processTasks(std::vector<TaskScheduler::ScheduledTask>& tasks, |
||||
ILogger& logger, const ITimeProvider& timeProvider, |
||||
std::atomic<bool>& stopRequested, TimePoint now, |
||||
bool& hasOnStartTask, TimePoint& nextWakeupTime) |
||||
{ |
||||
for (auto& task : tasks) { |
||||
if (stopRequested) { |
||||
break; |
||||
} |
||||
|
||||
bool executeNow = false; |
||||
|
||||
if (shouldExecuteOnStart(task)) { |
||||
executeNow = true; |
||||
hasOnStartTask = true; |
||||
} else if ((static_cast<int>(task.mode) & static_cast<int>(RunMode::Once)) |
||||
|| (static_cast<int>(task.mode) |
||||
& static_cast<int>(RunMode::Forever))) { |
||||
if (task.nextExecution == TimePoint{}) { |
||||
auto taskTime = calculateNextExecutionTime(task, timeProvider, now); |
||||
|
||||
if ((static_cast<int>(task.mode) & static_cast<int>(RunMode::Once)) |
||||
&& taskTime == TimePoint{} && !task.executed) { |
||||
executeNow = true; |
||||
} else if (taskTime != TimePoint{}) { |
||||
task.nextExecution = taskTime; |
||||
} |
||||
} |
||||
|
||||
if (!executeNow && shouldExecuteBasedOnTime(task, now)) { |
||||
executeNow = true; |
||||
} |
||||
|
||||
if (!executeNow && task.nextExecution < nextWakeupTime) { |
||||
nextWakeupTime = task.nextExecution; |
||||
} |
||||
} |
||||
|
||||
if (executeNow) { |
||||
executeTask(task, logger, timeProvider, nextWakeupTime); |
||||
} |
||||
} |
||||
} |
||||
|
||||
void waitForNextTask(IBlocker& blocker, std::atomic<bool>& stopRequested, |
||||
TimePoint now, TimePoint nextWakeupTime) |
||||
{ |
||||
if (!stopRequested && nextWakeupTime > now) { |
||||
auto waitDuration = |
||||
std::chrono::duration_cast<Milliseconds>(nextWakeupTime - now); |
||||
auto maxWait = Minutes(1); |
||||
|
||||
if (waitDuration > maxWait) { |
||||
waitDuration = maxWait; |
||||
} |
||||
|
||||
blocker.blockFor(waitDuration); |
||||
} |
||||
} |
||||
|
||||
} // namespace
|
||||
|
||||
TaskScheduler::TaskScheduler(ILoggerPtr logger, ITimeProvider& timeProvider, |
||||
IThreadManager& threadManager, |
||||
std::unique_ptr<IBlocker> blocker) |
||||
: logger{std::move(logger)}, timeProvider{timeProvider}, |
||||
threadManager{threadManager}, blocker{std::move(blocker)} |
||||
{} |
||||
|
||||
void TaskScheduler::schedule(TaskFunction task, int hour, int minute, |
||||
int second, RunMode mode) |
||||
{ |
||||
if (!isValidTime(hour, minute, second)) { |
||||
throw std::invalid_argument("Invalid time parameters"); |
||||
} |
||||
|
||||
if (areModesMutuallyExclusive(mode)) { |
||||
throw std::invalid_argument( |
||||
"Forever and Once modes are mutually exclusive"); |
||||
} |
||||
|
||||
std::lock_guard<std::mutex> lock(tasksMutex); |
||||
tasks.emplace_back(std::move(task), hour, minute, second, mode); |
||||
} |
||||
|
||||
void TaskScheduler::start() |
||||
{ |
||||
if (running) { |
||||
return; |
||||
} |
||||
|
||||
running = true; |
||||
stopRequested = false; |
||||
|
||||
threadHandle = threadManager.createThread([this]() { |
||||
logger->info("TaskScheduler thread started"); |
||||
|
||||
while (!stopRequested) { |
||||
auto now = timeProvider.now(); |
||||
bool shouldExecuteOnStart = false; |
||||
auto nextWakeupTime = now + Hours(24); |
||||
|
||||
{ |
||||
std::lock_guard<std::mutex> lock(tasksMutex); |
||||
processTasks(tasks, *logger, timeProvider, stopRequested, now, |
||||
shouldExecuteOnStart, nextWakeupTime); |
||||
} |
||||
|
||||
if (shouldExecuteOnStart) { |
||||
continue; |
||||
} |
||||
|
||||
waitForNextTask(*blocker, stopRequested, now, nextWakeupTime); |
||||
} |
||||
|
||||
running = false; |
||||
logger->info("TaskScheduler thread stopped"); |
||||
}); |
||||
} |
||||
|
||||
void TaskScheduler::stop() |
||||
{ |
||||
if (!running) { |
||||
return; |
||||
} |
||||
|
||||
stopRequested = true; |
||||
blocker->notify(); |
||||
|
||||
if (threadHandle && threadHandle->joinable()) { |
||||
threadHandle->join(); |
||||
} |
||||
} |
||||
|
||||
} // namespace nxl::autostore::application
|
||||
@ -0,0 +1,85 @@
|
||||
#pragma once |
||||
|
||||
#include "application/interfaces/ITimeProvider.h" |
||||
#include "application/interfaces/IThreadManager.h" |
||||
#include "application/interfaces/IBlocker.h" |
||||
|
||||
#include <autostore/ILogger.h> |
||||
#include <functional> |
||||
#include <chrono> |
||||
#include <vector> |
||||
#include <atomic> |
||||
#include <memory> |
||||
#include <mutex> |
||||
|
||||
namespace nxl::autostore::application { |
||||
|
||||
class TaskScheduler |
||||
{ |
||||
public: |
||||
enum class RunMode { |
||||
OnStart = 1, |
||||
Forever = 2, |
||||
OnStartThenForever = 3, // OnStart | Forever
|
||||
Once = 4, |
||||
OnStartThenOnce = 5 // OnStart | Once
|
||||
}; |
||||
|
||||
using TaskFunction = std::function<void()>; |
||||
using TimePoint = application::ITimeProvider::Clock::time_point; |
||||
using ThreadHandle = application::IThreadManager::ThreadHandle; |
||||
|
||||
struct ScheduledTask |
||||
{ |
||||
TaskFunction function; |
||||
int hour; |
||||
int minute; |
||||
int second; |
||||
RunMode mode; |
||||
bool executed = false; |
||||
TimePoint nextExecution{}; |
||||
|
||||
ScheduledTask(TaskFunction t, int h, int m, int s, RunMode md) |
||||
: function(std::move(t)), hour(h), minute(m), second(s), mode(md) |
||||
{} |
||||
}; |
||||
|
||||
TaskScheduler(ILoggerPtr logger, ITimeProvider& timeProvider, |
||||
IThreadManager& threadManager, |
||||
std::unique_ptr<IBlocker> blocker); |
||||
|
||||
TaskScheduler(const TaskScheduler&) = delete; |
||||
TaskScheduler& operator=(const TaskScheduler&) = delete; |
||||
virtual ~TaskScheduler() = default; |
||||
|
||||
void schedule(TaskFunction task, int hour, int minute, int second, |
||||
RunMode mode); |
||||
void start(); |
||||
void stop(); |
||||
|
||||
private: |
||||
ILoggerPtr logger; |
||||
ITimeProvider& timeProvider; |
||||
IThreadManager& threadManager; |
||||
std::unique_ptr<IBlocker> blocker; |
||||
|
||||
std::vector<ScheduledTask> tasks; |
||||
std::mutex tasksMutex; |
||||
std::atomic<bool> running{false}; |
||||
std::atomic<bool> stopRequested{false}; |
||||
std::unique_ptr<ThreadHandle> threadHandle; |
||||
}; |
||||
|
||||
constexpr TaskScheduler::RunMode operator|(TaskScheduler::RunMode a, |
||||
TaskScheduler::RunMode b) |
||||
{ |
||||
return static_cast<TaskScheduler::RunMode>(static_cast<uint8_t>(a) |
||||
| static_cast<uint8_t>(b)); |
||||
} |
||||
|
||||
constexpr uint8_t operator&(TaskScheduler::RunMode a, TaskScheduler::RunMode b) |
||||
{ |
||||
return static_cast<uint8_t>(a) & static_cast<uint8_t>(b); |
||||
} |
||||
|
||||
} // 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,17 @@
|
||||
#pragma once |
||||
|
||||
#include <string> |
||||
|
||||
namespace nxl::autostore::domain { |
||||
|
||||
struct User |
||||
{ |
||||
using Id_t = std::string; |
||||
inline static const Id_t NULL_ID{""}; |
||||
Id_t id; |
||||
|
||||
std::string username; |
||||
std::string passwordHash; |
||||
}; |
||||
|
||||
} // namespace nxl::autostore::domain
|
||||
@ -0,0 +1,141 @@
|
||||
#include "Specification.h" |
||||
#include <stdexcept> |
||||
|
||||
namespace nxl::helpers { |
||||
|
||||
// Condition render implementation
|
||||
std::string Condition::render(const Renderer& renderer) const |
||||
{ |
||||
std::string opStr; |
||||
switch (op) { |
||||
case ComparisonOp::EQ: |
||||
opStr = renderer.opEq; |
||||
break; |
||||
case ComparisonOp::NE: |
||||
opStr = renderer.opNe; |
||||
break; |
||||
case ComparisonOp::LT: |
||||
opStr = renderer.opLt; |
||||
break; |
||||
case ComparisonOp::LE: |
||||
opStr = renderer.opLe; |
||||
break; |
||||
case ComparisonOp::GT: |
||||
opStr = renderer.opGt; |
||||
break; |
||||
case ComparisonOp::GE: |
||||
opStr = renderer.opGe; |
||||
break; |
||||
case ComparisonOp::LIKE: |
||||
opStr = renderer.opLike; |
||||
break; |
||||
case ComparisonOp::IS_NULL: |
||||
opStr = renderer.opIsNull; |
||||
break; |
||||
case ComparisonOp::IS_NOT_NULL: |
||||
opStr = renderer.opIsNotNull; |
||||
break; |
||||
} |
||||
|
||||
std::string result = field + " " + opStr; |
||||
if (value) { |
||||
result += " " + renderer.formatValue(*value); |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
// ConditionGroup render implementation
|
||||
std::string ConditionGroup::render(const Renderer& renderer) const |
||||
{ |
||||
if (children.empty()) |
||||
return ""; |
||||
|
||||
std::string opStr = (op == LogicalOp::AND) ? renderer.opAnd : renderer.opOr; |
||||
std::string result = renderer.groupStart; |
||||
|
||||
bool first = true; |
||||
for (const auto& child : children) { |
||||
if (!first) |
||||
result += " " + opStr + " "; |
||||
result += child->render(renderer); |
||||
first = false; |
||||
} |
||||
|
||||
result += renderer.groupEnd; |
||||
return result; |
||||
} |
||||
|
||||
// SpecificationBuilder implementation
|
||||
SpecificationBuilder::SpecificationBuilder() |
||||
{ |
||||
root = std::make_unique<ConditionGroup>(LogicalOp::AND); |
||||
current = root.get(); |
||||
} |
||||
|
||||
SpecificationBuilder& SpecificationBuilder::field(const std::string& name) |
||||
{ |
||||
lastField = name; |
||||
return *this; |
||||
} |
||||
|
||||
SpecificationBuilder& SpecificationBuilder::like(std::string pattern) |
||||
{ |
||||
addCondition(ComparisonOp::LIKE, std::move(pattern)); |
||||
return *this; |
||||
} |
||||
|
||||
SpecificationBuilder& SpecificationBuilder::isNull() |
||||
{ |
||||
addCondition(ComparisonOp::IS_NULL); |
||||
return *this; |
||||
} |
||||
|
||||
SpecificationBuilder& SpecificationBuilder::isNotNull() |
||||
{ |
||||
addCondition(ComparisonOp::IS_NOT_NULL); |
||||
return *this; |
||||
} |
||||
|
||||
SpecificationBuilder& SpecificationBuilder::andGroup() |
||||
{ |
||||
startGroup(LogicalOp::AND); |
||||
return *this; |
||||
} |
||||
|
||||
SpecificationBuilder& SpecificationBuilder::orGroup() |
||||
{ |
||||
startGroup(LogicalOp::OR); |
||||
return *this; |
||||
} |
||||
|
||||
SpecificationBuilder& SpecificationBuilder::endGroup() |
||||
{ |
||||
if (stack.empty()) |
||||
return *this; |
||||
current = stack.back(); |
||||
stack.pop_back(); |
||||
return *this; |
||||
} |
||||
|
||||
std::unique_ptr<ISpecificationExpr> SpecificationBuilder::build() |
||||
{ |
||||
return std::move(root); |
||||
} |
||||
|
||||
void SpecificationBuilder::startGroup(LogicalOp opType) |
||||
{ |
||||
auto newGroup = std::make_unique<ConditionGroup>(opType); |
||||
auto* newGroupPtr = newGroup.get(); |
||||
current->add(std::move(newGroup)); |
||||
|
||||
stack.push_back(current); |
||||
current = newGroupPtr; |
||||
} |
||||
|
||||
// Helper function
|
||||
SpecificationBuilder makeSpecification() |
||||
{ |
||||
return SpecificationBuilder(); |
||||
} |
||||
|
||||
} // namespace nxl::helpers
|
||||
@ -0,0 +1,198 @@
|
||||
#pragma once |
||||
|
||||
#include <string> |
||||
#include <vector> |
||||
#include <memory> |
||||
#include <variant> |
||||
#include <optional> |
||||
#include <functional> |
||||
#include <any> |
||||
#include <stdexcept> |
||||
|
||||
namespace nxl::helpers { |
||||
|
||||
class ISpecificationExpr; |
||||
class Condition; |
||||
class ConditionGroup; |
||||
|
||||
enum class ComparisonOp { EQ, NE, LT, LE, GT, GE, LIKE, IS_NULL, IS_NOT_NULL }; |
||||
enum class LogicalOp { AND, OR }; |
||||
|
||||
class ISpecificationExpr |
||||
{ |
||||
public: |
||||
virtual ~ISpecificationExpr() = default; |
||||
virtual std::string render(const struct Renderer& renderer) const = 0; |
||||
}; |
||||
|
||||
class ConditionValue |
||||
{ |
||||
public: |
||||
template <typename T> ConditionValue(T value) : value_(std::move(value)) {} |
||||
|
||||
const std::any& get() const { return value_; } |
||||
|
||||
template <typename T> const T& as() const |
||||
{ |
||||
return std::any_cast<const T&>(value_); |
||||
} |
||||
|
||||
template <typename T> bool is() const { return value_.type() == typeid(T); } |
||||
|
||||
private: |
||||
std::any value_; |
||||
}; |
||||
|
||||
class Condition : public ISpecificationExpr |
||||
{ |
||||
std::string field; |
||||
ComparisonOp op; |
||||
std::optional<ConditionValue> value; // Optional for operators like IS_NULL
|
||||
|
||||
public: |
||||
Condition(std::string f, ComparisonOp o, std::optional<ConditionValue> v = {}) |
||||
: field(std::move(f)), op(o), value(std::move(v)) |
||||
{} |
||||
|
||||
const std::string& getField() const { return field; } |
||||
ComparisonOp getOp() const { return op; } |
||||
const std::optional<ConditionValue>& getValue() const { return value; } |
||||
|
||||
std::string render(const struct Renderer& renderer) const override; |
||||
}; |
||||
|
||||
// Logical group of conditions
|
||||
class ConditionGroup : public ISpecificationExpr |
||||
{ |
||||
LogicalOp op; |
||||
std::vector<std::unique_ptr<ISpecificationExpr>> children; |
||||
|
||||
public: |
||||
ConditionGroup(LogicalOp o) : op(o) {} |
||||
|
||||
void add(std::unique_ptr<ISpecificationExpr> expr) |
||||
{ |
||||
children.push_back(std::move(expr)); |
||||
} |
||||
|
||||
LogicalOp getOp() const { return op; } |
||||
const std::vector<std::unique_ptr<ISpecificationExpr>>& getChildren() const |
||||
{ |
||||
return children; |
||||
} |
||||
|
||||
std::string render(const struct Renderer& renderer) const override; |
||||
}; |
||||
|
||||
// Renderer interface - defines how to render operators and grouping
|
||||
struct Renderer |
||||
{ |
||||
std::string opEq; |
||||
std::string opNe; |
||||
std::string opLt; |
||||
std::string opLe; |
||||
std::string opGt; |
||||
std::string opGe; |
||||
std::string opLike; |
||||
std::string opIsNull; |
||||
std::string opIsNotNull; |
||||
|
||||
std::string opAnd; |
||||
std::string opOr; |
||||
|
||||
std::string groupStart; |
||||
std::string groupEnd; |
||||
|
||||
// Value formatting function
|
||||
std::function<std::string(const ConditionValue&)> formatValue; |
||||
}; |
||||
|
||||
// Fluent builder for specifications
|
||||
class SpecificationBuilder |
||||
{ |
||||
std::unique_ptr<ConditionGroup> root; |
||||
ConditionGroup* current; |
||||
std::vector<ConditionGroup*> stack; |
||||
std::string lastField; |
||||
|
||||
public: |
||||
SpecificationBuilder(); |
||||
|
||||
// Set the field for the next condition
|
||||
SpecificationBuilder& field(const std::string& name); |
||||
|
||||
template <typename T> SpecificationBuilder& equals(T value) |
||||
{ |
||||
addCondition(ComparisonOp::EQ, std::move(value)); |
||||
return *this; |
||||
} |
||||
|
||||
template <typename T> SpecificationBuilder& notEquals(T value) |
||||
{ |
||||
addCondition(ComparisonOp::NE, std::move(value)); |
||||
return *this; |
||||
} |
||||
|
||||
template <typename T> SpecificationBuilder& lessThan(T value) |
||||
{ |
||||
addCondition(ComparisonOp::LT, std::move(value)); |
||||
return *this; |
||||
} |
||||
|
||||
template <typename T> SpecificationBuilder& lessOrEqual(T value) |
||||
{ |
||||
addCondition(ComparisonOp::LE, std::move(value)); |
||||
return *this; |
||||
} |
||||
|
||||
template <typename T> SpecificationBuilder& greaterThan(T value) |
||||
{ |
||||
addCondition(ComparisonOp::GT, std::move(value)); |
||||
return *this; |
||||
} |
||||
|
||||
template <typename T> SpecificationBuilder& greaterOrEqual(T value) |
||||
{ |
||||
addCondition(ComparisonOp::GE, std::move(value)); |
||||
return *this; |
||||
} |
||||
SpecificationBuilder& like(std::string pattern); |
||||
SpecificationBuilder& isNull(); |
||||
SpecificationBuilder& isNotNull(); |
||||
|
||||
SpecificationBuilder& andGroup(); |
||||
SpecificationBuilder& orGroup(); |
||||
SpecificationBuilder& endGroup(); |
||||
|
||||
std::unique_ptr<ISpecificationExpr> build(); |
||||
|
||||
private: |
||||
template <typename T> void addCondition(ComparisonOp op, T value) |
||||
{ |
||||
if (lastField.empty()) { |
||||
throw std::runtime_error("No field specified for condition"); |
||||
} |
||||
|
||||
auto condition = std::make_unique<Condition>( |
||||
lastField, op, ConditionValue(std::move(value))); |
||||
current->add(std::move(condition)); |
||||
lastField.clear(); |
||||
} |
||||
|
||||
void addCondition(ComparisonOp op) |
||||
{ |
||||
if (lastField.empty()) { |
||||
throw std::runtime_error("No field specified for condition"); |
||||
} |
||||
|
||||
auto condition = std::make_unique<Condition>(lastField, op, std::nullopt); |
||||
current->add(std::move(condition)); |
||||
lastField.clear(); |
||||
} |
||||
void startGroup(LogicalOp opType); |
||||
}; |
||||
|
||||
// Helper function to create a specification builder
|
||||
SpecificationBuilder makeSpecification(); |
||||
|
||||
} // namespace nxl::helpers
|
||||
@ -0,0 +1,31 @@
|
||||
#pragma once |
||||
|
||||
#include "domain/entities/Item.h" |
||||
#include "domain/helpers/Specification.h" |
||||
#include <chrono> |
||||
|
||||
namespace nxl::autostore::domain { |
||||
|
||||
using ItemExpirationSpec = std::unique_ptr<nxl::helpers::ISpecificationExpr>; |
||||
|
||||
class ItemExpirationPolicy |
||||
{ |
||||
public: |
||||
using TimePoint = std::chrono::system_clock::time_point; |
||||
constexpr static const char* FIELD_EXP_DATE{"expiration_date"}; |
||||
|
||||
bool isExpired(const Item& item, const TimePoint& currentTime) const |
||||
{ |
||||
return item.expirationDate <= currentTime; |
||||
} |
||||
|
||||
ItemExpirationSpec getExpiredSpecification(const TimePoint& currentTime) const |
||||
{ |
||||
return nxl::helpers::SpecificationBuilder() |
||||
.field(FIELD_EXP_DATE) |
||||
.lessOrEqual(currentTime) |
||||
.build(); |
||||
} |
||||
}; |
||||
|
||||
} // namespace nxl::autostore::domain
|
||||
@ -0,0 +1,55 @@
|
||||
#include "CvBlocker.h" |
||||
|
||||
namespace nxl::autostore::infrastructure { |
||||
|
||||
void CvBlocker::block() |
||||
{ |
||||
notified = false; |
||||
std::unique_lock<std::mutex> lock(mutex); |
||||
blocked = true; |
||||
conditionVar.wait(lock); |
||||
blocked = false; |
||||
} |
||||
|
||||
void CvBlocker::blockFor(const std::chrono::milliseconds& duration) |
||||
{ |
||||
notified = false; |
||||
std::unique_lock<std::mutex> lock(mutex); |
||||
blocked = true; |
||||
conditionVar.wait_for(lock, duration); |
||||
blocked = false; |
||||
} |
||||
|
||||
void CvBlocker::blockUntil(const TimePoint& timePoint) |
||||
{ |
||||
blockUntilTimePoint(timePoint); |
||||
} |
||||
|
||||
void CvBlocker::notify() |
||||
{ |
||||
notified = true; |
||||
conditionVar.notify_all(); |
||||
} |
||||
|
||||
bool CvBlocker::isBlocked() |
||||
{ |
||||
return blocked; |
||||
} |
||||
|
||||
bool CvBlocker::wasNotified() |
||||
{ |
||||
return notified; |
||||
} |
||||
|
||||
template <class Clock, class Duration> |
||||
void CvBlocker::blockUntilTimePoint( |
||||
const std::chrono::time_point<Clock, Duration>& timePoint) |
||||
{ |
||||
notified = false; |
||||
std::unique_lock<std::mutex> lock(mutex); |
||||
blocked = true; |
||||
conditionVar.wait_until(lock, timePoint); |
||||
blocked = false; |
||||
} |
||||
|
||||
} // namespace nxl::autostore::infrastructure
|
||||
@ -0,0 +1,33 @@
|
||||
#pragma once |
||||
|
||||
#include "application/interfaces/IBlocker.h" |
||||
#include <condition_variable> |
||||
#include <mutex> |
||||
#include <atomic> |
||||
|
||||
namespace nxl::autostore::infrastructure { |
||||
|
||||
class CvBlocker : public application::IBlocker |
||||
{ |
||||
public: |
||||
~CvBlocker() override = default; |
||||
void block() override; |
||||
void blockFor(const std::chrono::milliseconds& duration) override; |
||||
void blockUntil(const TimePoint& timePoint) override; |
||||
void notify() override; |
||||
bool isBlocked() override; |
||||
bool wasNotified() override; |
||||
|
||||
private: |
||||
template <class Clock, class Duration> |
||||
void blockUntilTimePoint( |
||||
const std::chrono::time_point<Clock, Duration>& timePoint); |
||||
|
||||
private: |
||||
std::condition_variable conditionVar; |
||||
std::mutex mutex; |
||||
std::atomic<bool> notified{false}; |
||||
std::atomic<bool> blocked{false}; |
||||
}; |
||||
|
||||
} // namespace nxl::autostore::infrastructure
|
||||
@ -0,0 +1,43 @@
|
||||
#include "SystemThreadManager.h" |
||||
|
||||
namespace nxl::autostore::infrastructure { |
||||
|
||||
SystemThreadManager::SystemThreadHandle::SystemThreadHandle( |
||||
std::thread&& thread) |
||||
: thread{std::move(thread)} |
||||
{} |
||||
|
||||
SystemThreadManager::SystemThreadHandle::~SystemThreadHandle() |
||||
{ |
||||
if (thread.joinable()) { |
||||
thread.join(); |
||||
} |
||||
} |
||||
|
||||
void SystemThreadManager::SystemThreadHandle::join() |
||||
{ |
||||
thread.join(); |
||||
} |
||||
|
||||
bool SystemThreadManager::SystemThreadHandle::joinable() const |
||||
{ |
||||
return thread.joinable(); |
||||
} |
||||
|
||||
application::IThreadManager::ThreadHandlePtr |
||||
SystemThreadManager::createThread(std::function<void()> func) |
||||
{ |
||||
return std::make_unique<SystemThreadHandle>(std::thread(func)); |
||||
} |
||||
|
||||
std::thread::id SystemThreadManager::getCurrentThreadId() const |
||||
{ |
||||
return std::this_thread::get_id(); |
||||
} |
||||
|
||||
void SystemThreadManager::sleep(const std::chrono::milliseconds& duration) |
||||
{ |
||||
std::this_thread::sleep_for(duration); |
||||
} |
||||
|
||||
} // namespace nxl::autostore::infrastructure
|
||||
@ -0,0 +1,27 @@
|
||||
#pragma once |
||||
|
||||
#include "application/interfaces/IThreadManager.h" |
||||
|
||||
namespace nxl::autostore::infrastructure { |
||||
|
||||
class SystemThreadManager : public application::IThreadManager |
||||
{ |
||||
public: |
||||
class SystemThreadHandle : public ThreadHandle |
||||
{ |
||||
public: |
||||
explicit SystemThreadHandle(std::thread&& thread); |
||||
~SystemThreadHandle() override; |
||||
void join() override; |
||||
bool joinable() const override; |
||||
|
||||
private: |
||||
std::thread thread; |
||||
}; |
||||
|
||||
ThreadHandlePtr createThread(std::function<void()> func) override; |
||||
std::thread::id getCurrentThreadId() const override; |
||||
void sleep(const std::chrono::milliseconds& duration) override; |
||||
}; |
||||
|
||||
} // namespace nxl::autostore::infrastructure
|
||||
@ -0,0 +1,22 @@
|
||||
#include "SystemTimeProvider.h" |
||||
|
||||
namespace nxl::autostore::infrastructure { |
||||
|
||||
SystemTimeProvider::Clock::time_point SystemTimeProvider::now() const |
||||
{ |
||||
return Clock::now(); |
||||
} |
||||
|
||||
std::tm SystemTimeProvider::to_tm(const Clock::time_point& timePoint) const |
||||
{ |
||||
auto time_t_now = Clock::to_time_t(timePoint); |
||||
return *std::localtime(&time_t_now); |
||||
} |
||||
|
||||
SystemTimeProvider::Clock::time_point |
||||
SystemTimeProvider::from_tm(const std::tm& tm) const |
||||
{ |
||||
return Clock::from_time_t(std::mktime(const_cast<std::tm*>(&tm))); |
||||
} |
||||
|
||||
} // namespace nxl::autostore::infrastructure
|
||||
@ -0,0 +1,15 @@
|
||||
#pragma once |
||||
|
||||
#include "application/interfaces/ITimeProvider.h" |
||||
|
||||
namespace nxl::autostore::infrastructure { |
||||
|
||||
class SystemTimeProvider : public application::ITimeProvider |
||||
{ |
||||
public: |
||||
Clock::time_point now() const override; |
||||
std::tm to_tm(const Clock::time_point& timePoint) const override; |
||||
Clock::time_point from_tm(const std::tm& tm) const override; |
||||
}; |
||||
|
||||
} // namespace nxl::autostore::infrastructure
|
||||
@ -0,0 +1,96 @@
|
||||
#include "FileJwtAuthService.h" |
||||
#include <jwt-cpp/jwt.h> |
||||
#include <fstream> |
||||
#include <nlohmann/json.hpp> |
||||
#include <picosha2.h> |
||||
|
||||
namespace nxl::autostore::infrastructure { |
||||
|
||||
namespace { |
||||
// hardcoded secret key for demo purposes
|
||||
constexpr const char* secretKey{"secret-key"}; |
||||
} // namespace
|
||||
|
||||
/**
|
||||
* @note Normally that would be generated by the OP |
||||
*/ |
||||
std::string FileJwtAuthService::generateToken(std::string_view userId) |
||||
{ |
||||
auto token = |
||||
jwt::create() |
||||
.set_issuer("autostore") |
||||
.set_type("JWS") |
||||
.set_payload_claim("sub", jwt::claim(std::string(userId))) |
||||
.set_expires_at(std::chrono::system_clock::now() + std::chrono::hours(24)) |
||||
.sign(jwt::algorithm::hs256{secretKey}); |
||||
|
||||
return token; |
||||
} |
||||
|
||||
std::optional<domain::User::Id_t> |
||||
FileJwtAuthService::extractUserId(std::string_view token) |
||||
{ |
||||
// Check cache first
|
||||
std::string tokenStr(token); |
||||
auto cacheIt = uidCache.find(tokenStr); |
||||
if (cacheIt != uidCache.end()) { |
||||
return cacheIt->second; |
||||
} |
||||
|
||||
try { |
||||
auto decoded = jwt::decode(tokenStr); |
||||
|
||||
auto verifier = jwt::verify() |
||||
.allow_algorithm(jwt::algorithm::hs256{secretKey}) |
||||
.with_issuer("autostore"); |
||||
|
||||
verifier.verify(decoded); |
||||
|
||||
auto subClaim = decoded.get_payload_claim("sub"); |
||||
auto userId = subClaim.as_string(); |
||||
|
||||
if (uidCache.size() >= MAX_UID_CACHE_SIZE) { |
||||
// Remove the oldest entry (first element) to make space
|
||||
uidCache.erase(uidCache.begin()); |
||||
} |
||||
uidCache[tokenStr] = userId; |
||||
|
||||
return userId; |
||||
} catch (const std::exception& e) { |
||||
return std::nullopt; |
||||
} |
||||
} |
||||
|
||||
/**
|
||||
* @note Normally that wouldn't be this app's concern |
||||
*/ |
||||
std::optional<domain::User::Id_t> |
||||
FileJwtAuthService::authenticateUser(std::string_view username, |
||||
std::string_view password) |
||||
{ |
||||
try { |
||||
std::string passwordHash; |
||||
picosha2::hash256_hex_string(password, passwordHash); |
||||
|
||||
std::ifstream file(dbPath); |
||||
if (!file.is_open()) { |
||||
return std::nullopt; |
||||
} |
||||
|
||||
nlohmann::json usersJson; |
||||
file >> usersJson; |
||||
|
||||
for (const auto& userJson : usersJson) { |
||||
if (userJson["username"] == username |
||||
&& userJson["password"] == passwordHash) { |
||||
return userJson["id"].get<std::string>(); |
||||
} |
||||
} |
||||
|
||||
return std::nullopt; |
||||
} catch (const std::exception& e) { |
||||
return std::nullopt; |
||||
} |
||||
} |
||||
|
||||
} // namespace nxl::autostore::infrastructure
|
||||
@ -0,0 +1,31 @@
|
||||
#pragma once |
||||
|
||||
#include "application/interfaces/IAuthService.h" |
||||
#include <unordered_map> |
||||
#include <string> |
||||
#include <string_view> |
||||
#include <optional> |
||||
|
||||
namespace nxl::autostore::infrastructure { |
||||
|
||||
class FileJwtAuthService : public application::IAuthService |
||||
{ |
||||
public: |
||||
FileJwtAuthService(std::string dbPath) : dbPath{std::move(dbPath)} {} |
||||
|
||||
std::string generateToken(std::string_view userId) override; |
||||
|
||||
std::optional<domain::User::Id_t> |
||||
extractUserId(std::string_view token) override; |
||||
|
||||
std::optional<domain::User::Id_t> |
||||
authenticateUser(std::string_view username, |
||||
std::string_view password) override; |
||||
|
||||
private: |
||||
static constexpr size_t MAX_UID_CACHE_SIZE = 1024; |
||||
std::string dbPath; |
||||
std::unordered_map<std::string, domain::User::Id_t> uidCache; |
||||
}; |
||||
|
||||
} // 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", domain::User::NULL_ID); |
||||
|
||||
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,37 @@
|
||||
#include "HttpJwtMiddleware.h" |
||||
|
||||
namespace nxl::autostore::infrastructure { |
||||
|
||||
httplib::Server::HandlerResponse HttpJwtMiddleware::authenticate( |
||||
const httplib::Request& req, httplib::Response& res, |
||||
application::IAuthService& authService, ILoggerPtr log) |
||||
{ |
||||
log->v(1, "Pre-request handler: %s", req.path); |
||||
|
||||
// Skip authentication for login endpoint
|
||||
if (req.path == "/api/v1/login") { |
||||
log->v(1, "Skipping authentication for login endpoint"); |
||||
return httplib::Server::HandlerResponse::Unhandled; |
||||
} |
||||
|
||||
auto it = req.headers.find("Authorization"); |
||||
if (it != req.headers.end()) { |
||||
auto authHeader = it->second; |
||||
if (authHeader.find("Bearer ") == 0) { |
||||
auto token = authHeader.substr(7); // Remove "Bearer " prefix
|
||||
if (authService.extractUserId(token)) { |
||||
log->v(1, "Authorized request"); |
||||
return httplib::Server::HandlerResponse::Unhandled; |
||||
} |
||||
} |
||||
} |
||||
|
||||
log->v(1, "Unauthorized request"); |
||||
res.status = 401; |
||||
res.set_content( |
||||
R"({"status":"error","message":"Unauthorized - Invalid or missing token"})", |
||||
"application/json"); |
||||
return httplib::Server::HandlerResponse::Handled; |
||||
} |
||||
|
||||
} // namespace nxl::autostore::infrastructure
|
||||
@ -0,0 +1,20 @@
|
||||
#pragma once |
||||
|
||||
#include "application/interfaces/IAuthService.h" |
||||
#include "autostore/ILogger.h" |
||||
#include <httplib.h> |
||||
|
||||
namespace nxl::autostore::infrastructure { |
||||
|
||||
class HttpJwtMiddleware |
||||
{ |
||||
public: |
||||
HttpJwtMiddleware() = default; |
||||
~HttpJwtMiddleware() = default; |
||||
|
||||
static httplib::Server::HandlerResponse |
||||
authenticate(const httplib::Request& req, httplib::Response& res, |
||||
application::IAuthService& authService, ILoggerPtr logger); |
||||
}; |
||||
|
||||
} // namespace nxl::autostore::infrastructure
|
||||
@ -0,0 +1,364 @@
|
||||
/**
|
||||
* HTTP-based order service implementation with retry logic and |
||||
* connection pooling |
||||
* |
||||
* FLOW OVERVIEW: |
||||
* 1. orderItem() validates input and enqueues order request |
||||
* 2. Background worker thread processes queue sequentially (FIFO) |
||||
* 3. Failed requests are retried with exponential backoff (1s, 2s, 4s...) |
||||
* 4. HTTP clients are cached per host and auto-cleaned when unused |
||||
* 5. Service shuts down gracefully, completing queued orders |
||||
* |
||||
* IMPORTANT LIMITATIONS: |
||||
* - Uses single worker thread - retries of failed requests will block |
||||
* processing of new orders until retry delay expires |
||||
* - Not suitable for time-critical operations due to sequential processing |
||||
* - Designed for fire-and-forget order notifications, not real-time |
||||
* transactions |
||||
*/ |
||||
|
||||
#include "HttpOrderService.h" |
||||
#include "autostore/Version.h" |
||||
#include <httplib.h> |
||||
#include <stdexcept> |
||||
#include <regex> |
||||
#include <thread> |
||||
#include <queue> |
||||
#include <mutex> |
||||
#include <condition_variable> |
||||
#include <atomic> |
||||
#include <chrono> |
||||
#include <memory> |
||||
#include <unordered_map> |
||||
|
||||
namespace nxl::autostore::infrastructure { |
||||
|
||||
namespace { |
||||
|
||||
constexpr int MAX_RETRIES = 3; |
||||
constexpr int CONNECTION_TIMEOUT_SECONDS = 5; |
||||
constexpr int READ_TIMEOUT_SECONDS = 5; |
||||
constexpr int WRITE_TIMEOUT_SECONDS = 5; |
||||
constexpr char CONTENT_TYPE_JSON[] = "application/json"; |
||||
|
||||
std::pair<std::string, std::string> parseUrl(const std::string& url) |
||||
{ |
||||
static const std::regex url_regex( |
||||
R"(^(https?:\/\/)?([^\/:]+)(?::(\d+))?(\/[^\?]*)?(\?.*)?$)"); |
||||
|
||||
std::smatch matches; |
||||
if (!std::regex_match(url, matches, url_regex) || matches.size() < 5) { |
||||
throw std::runtime_error("Invalid URL format: " + url); |
||||
} |
||||
|
||||
std::string host = matches[2].str(); |
||||
std::string port = matches[3].str(); |
||||
std::string path = matches[4].str(); |
||||
std::string query = matches[5].str(); |
||||
|
||||
if (!port.empty()) { |
||||
host += ":" + port; |
||||
} |
||||
|
||||
if (path.empty()) { |
||||
path = "/"; |
||||
} |
||||
|
||||
path += query; |
||||
return {host, path}; |
||||
} |
||||
|
||||
std::string createOrderPayload(const domain::Item& item) |
||||
{ |
||||
// Escape JSON special characters in strings
|
||||
auto escapeJson = [](const std::string& str) { |
||||
std::string escaped; |
||||
escaped.reserve(str.size() + 10); // Reserve extra space for escapes
|
||||
|
||||
for (char c : str) { |
||||
switch (c) { |
||||
case '"': |
||||
escaped += "\\\""; |
||||
break; |
||||
case '\\': |
||||
escaped += "\\\\"; |
||||
break; |
||||
case '\b': |
||||
escaped += "\\b"; |
||||
break; |
||||
case '\f': |
||||
escaped += "\\f"; |
||||
break; |
||||
case '\n': |
||||
escaped += "\\n"; |
||||
break; |
||||
case '\r': |
||||
escaped += "\\r"; |
||||
break; |
||||
case '\t': |
||||
escaped += "\\t"; |
||||
break; |
||||
default: |
||||
escaped += c; |
||||
break; |
||||
} |
||||
} |
||||
return escaped; |
||||
}; |
||||
|
||||
return R"({"itemName": ")" + escapeJson(item.name) + R"(", "itemId": ")" |
||||
+ escapeJson(item.id) + "\"}"; |
||||
} |
||||
|
||||
} // namespace
|
||||
|
||||
struct OrderRequest |
||||
{ |
||||
std::string url; |
||||
std::string payload; |
||||
int retryCount = 0; |
||||
std::chrono::system_clock::time_point nextAttemptTime; |
||||
|
||||
OrderRequest() = default; |
||||
OrderRequest(std::string url, std::string payload, int rc = 0, |
||||
std::chrono::system_clock::time_point nat = |
||||
std::chrono::system_clock::now()) |
||||
: url{std::move(url)}, payload{std::move(payload)}, retryCount{rc}, |
||||
nextAttemptTime(nat) |
||||
{} |
||||
}; |
||||
|
||||
class HttpOrderService::Impl |
||||
{ |
||||
public: |
||||
explicit Impl(ILoggerPtr logger) |
||||
: log{std::move(logger)}, shutdownRequested{false} |
||||
{ |
||||
if (!log) { |
||||
throw std::invalid_argument("Logger cannot be null"); |
||||
} |
||||
|
||||
userAgent = "Autostore/" + nxl::getVersionString(); |
||||
workerThread = std::thread(&Impl::processQueue, this); |
||||
} |
||||
|
||||
~Impl() |
||||
{ |
||||
shutdown(); |
||||
if (workerThread.joinable()) { |
||||
workerThread.join(); |
||||
} |
||||
} |
||||
|
||||
void enqueueOrder(const std::string& url, std::string payload) |
||||
{ |
||||
{ |
||||
std::lock_guard<std::mutex> lock(queueMutex); |
||||
if (shutdownRequested) { |
||||
throw std::runtime_error( |
||||
"Service is shutting down, cannot enqueue new orders"); |
||||
} |
||||
orderQueue.emplace(url, std::move(payload)); |
||||
} |
||||
queueCondition.notify_one(); |
||||
} |
||||
|
||||
private: |
||||
void shutdown() |
||||
{ |
||||
{ |
||||
std::lock_guard<std::mutex> lock(queueMutex); |
||||
shutdownRequested = true; |
||||
} |
||||
queueCondition.notify_one(); |
||||
} |
||||
|
||||
bool shouldShutdown() const |
||||
{ |
||||
return shutdownRequested && orderQueue.empty(); |
||||
} |
||||
|
||||
bool isRequestReady(const OrderRequest& request) const |
||||
{ |
||||
return request.nextAttemptTime <= std::chrono::system_clock::now(); |
||||
} |
||||
|
||||
void processQueue() |
||||
{ |
||||
while (true) { |
||||
std::unique_lock<std::mutex> lock(queueMutex); |
||||
|
||||
// Wait for orders or shutdown signal
|
||||
queueCondition.wait( |
||||
lock, [this] { return !orderQueue.empty() || shutdownRequested; }); |
||||
|
||||
if (shouldShutdown()) { |
||||
break; |
||||
} |
||||
|
||||
if (orderQueue.empty()) { |
||||
continue; |
||||
} |
||||
|
||||
// Check if the front request is ready to be processed
|
||||
if (!isRequestReady(orderQueue.front())) { |
||||
// Wait until the next attempt time
|
||||
auto waitTime = |
||||
orderQueue.front().nextAttemptTime - std::chrono::system_clock::now(); |
||||
if (waitTime > std::chrono::milliseconds(0)) { |
||||
queueCondition.wait_for(lock, waitTime); |
||||
} |
||||
continue; |
||||
} |
||||
|
||||
// Extract request for processing
|
||||
OrderRequest request = std::move(orderQueue.front()); |
||||
orderQueue.pop(); |
||||
|
||||
// Release lock before processing to avoid blocking other operations
|
||||
lock.unlock(); |
||||
|
||||
processRequest(request); |
||||
} |
||||
} |
||||
|
||||
void processRequest(OrderRequest& request) |
||||
{ |
||||
try { |
||||
sendPostRequest(request.url, request.payload); |
||||
log->i("Order request sent successfully to: %s", request.url.c_str()); |
||||
} catch (const std::exception& e) { |
||||
log->e("Failed to send order request to %s: %s", request.url.c_str(), |
||||
e.what()); |
||||
handleFailedRequest(request); |
||||
} |
||||
} |
||||
|
||||
void handleFailedRequest(OrderRequest& request) |
||||
{ |
||||
if (request.retryCount < MAX_RETRIES) { |
||||
request.retryCount++; |
||||
// Exponential backoff: 1s, 2s, 4s, 8s...
|
||||
auto delay = std::chrono::seconds(1 << (request.retryCount - 1)); |
||||
request.nextAttemptTime = std::chrono::system_clock::now() + delay; |
||||
|
||||
log->w("Retrying order request to %s (attempt %d/%d) in %ld seconds", |
||||
request.url.c_str(), request.retryCount, MAX_RETRIES, |
||||
delay.count()); |
||||
|
||||
{ |
||||
std::lock_guard<std::mutex> lock(queueMutex); |
||||
if (!shutdownRequested) { |
||||
orderQueue.push(std::move(request)); |
||||
} |
||||
} |
||||
queueCondition.notify_one(); |
||||
} else { |
||||
log->e("Max retries exceeded for order request to: %s", |
||||
request.url.c_str()); |
||||
} |
||||
} |
||||
|
||||
std::shared_ptr<httplib::Client> getOrCreateClient(const std::string& host) |
||||
{ |
||||
std::lock_guard<std::mutex> lock(clientsMutex); |
||||
|
||||
auto it = clients.find(host); |
||||
if (it != clients.end()) { |
||||
// Check if client is still valid
|
||||
auto client = it->second.lock(); |
||||
if (client) { |
||||
return client; |
||||
} else { |
||||
// Remove expired weak_ptr
|
||||
clients.erase(it); |
||||
} |
||||
} |
||||
|
||||
// Create new client
|
||||
auto client = std::make_shared<httplib::Client>(host); |
||||
configureClient(*client); |
||||
clients[host] = client; |
||||
return client; |
||||
} |
||||
|
||||
void configureClient(httplib::Client& client) |
||||
{ |
||||
client.set_connection_timeout(CONNECTION_TIMEOUT_SECONDS, 0); |
||||
client.set_read_timeout(READ_TIMEOUT_SECONDS, 0); |
||||
client.set_write_timeout(WRITE_TIMEOUT_SECONDS, 0); |
||||
|
||||
// Enable keep-alive for better performance
|
||||
client.set_keep_alive(true); |
||||
|
||||
// Set reasonable limits
|
||||
client.set_compress(true); |
||||
} |
||||
|
||||
void sendPostRequest(const std::string& url, const std::string& payload) |
||||
{ |
||||
auto [host, path] = parseUrl(url); |
||||
auto client = getOrCreateClient(host); |
||||
|
||||
httplib::Headers headers = {{"Content-Type", CONTENT_TYPE_JSON}, |
||||
{"User-Agent", userAgent}, |
||||
{"Accept", CONTENT_TYPE_JSON}}; |
||||
|
||||
log->i("Sending POST request to: %s%s", host.c_str(), path.c_str()); |
||||
log->v(1, "Payload: %s", payload.c_str()); |
||||
|
||||
auto res = client->Post(path, headers, payload, CONTENT_TYPE_JSON); |
||||
|
||||
if (!res) { |
||||
throw std::runtime_error("Failed to connect to: " + host); |
||||
} |
||||
|
||||
log->v(2, "Response status: %d", res->status); |
||||
log->v(3, "Response body: %s", res->body.c_str()); |
||||
|
||||
if (res->status < 200 || res->status >= 300) { |
||||
std::string error_msg = |
||||
"HTTP request failed with status: " + std::to_string(res->status) |
||||
+ " for URL: " + url; |
||||
if (!res->body.empty()) { |
||||
error_msg += " Response: " + res->body; |
||||
} |
||||
throw std::runtime_error(error_msg); |
||||
} |
||||
} |
||||
|
||||
ILoggerPtr log; |
||||
std::queue<OrderRequest> orderQueue; |
||||
std::mutex queueMutex; |
||||
std::condition_variable queueCondition; |
||||
std::thread workerThread; |
||||
std::atomic<bool> shutdownRequested; |
||||
|
||||
// Use weak_ptr to allow automatic cleanup of unused clients
|
||||
std::unordered_map<std::string, std::weak_ptr<httplib::Client>> clients; |
||||
std::mutex clientsMutex; |
||||
std::string userAgent; |
||||
}; |
||||
|
||||
HttpOrderService::HttpOrderService(ILoggerPtr logger) |
||||
: impl{std::make_unique<Impl>(std::move(logger))} |
||||
{} |
||||
|
||||
HttpOrderService::~HttpOrderService() = default; |
||||
|
||||
void HttpOrderService::orderItem(const domain::Item& item) |
||||
{ |
||||
if (item.orderUrl.empty()) { |
||||
throw std::runtime_error("Order URL is empty for item: " + item.name); |
||||
} |
||||
|
||||
if (item.orderUrl.find("://") == std::string::npos) { |
||||
throw std::runtime_error("Invalid URL format for item: " + item.name |
||||
+ " (missing protocol)"); |
||||
} |
||||
|
||||
std::string payload = createOrderPayload(item); |
||||
impl->enqueueOrder(item.orderUrl, std::move(payload)); |
||||
} |
||||
|
||||
} // namespace nxl::autostore::infrastructure
|
||||
@ -0,0 +1,24 @@
|
||||
#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); |
||||
virtual ~HttpOrderService(); |
||||
void orderItem(const domain::Item& item) override; |
||||
|
||||
private: |
||||
ILoggerPtr log; |
||||
void sendPostRequest(std::string_view url, std::string_view payload); |
||||
|
||||
class Impl; |
||||
std::unique_ptr<Impl> impl; |
||||
}; |
||||
|
||||
} // namespace nxl::autostore::infrastructure
|
||||
@ -0,0 +1,123 @@
|
||||
#include "infrastructure/http/HttpServer.h" |
||||
#include "infrastructure/http/HttpJwtMiddleware.h" |
||||
#include <iostream> |
||||
#include <future> |
||||
|
||||
namespace nxl::autostore::infrastructure { |
||||
|
||||
namespace { |
||||
constexpr std::chrono::seconds defaultStartupTimeout{5}; |
||||
} |
||||
|
||||
HttpServer::HttpServer(ILoggerPtr logger, |
||||
application::IAuthService& authService) |
||||
: log{std::move(logger)}, authService{authService} |
||||
{} |
||||
|
||||
HttpServer::~HttpServer() |
||||
{ |
||||
if (running) { |
||||
stop(); |
||||
} |
||||
} |
||||
|
||||
bool HttpServer::start(int port, std::string_view host) |
||||
{ |
||||
if (running) { |
||||
return true; |
||||
} |
||||
|
||||
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]() { |
||||
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; |
||||
} |
||||
|
||||
server.set_logger([this](const httplib::Request& req, const auto& res) { |
||||
log->v(1, "Request: %s\n%s", req.path, req.body); |
||||
log->v(1, "Response: %d, %s", res.status, res.body); |
||||
}); |
||||
|
||||
// JWT authentication middleware
|
||||
server.set_pre_request_handler( |
||||
[this](const httplib::Request& req, |
||||
httplib::Response& res) -> httplib::Server::HandlerResponse { |
||||
return HttpJwtMiddleware::authenticate(req, res, authService, log); |
||||
}); |
||||
|
||||
// Signal that the server has bound to the port
|
||||
startupPromise.set_value(true); |
||||
|
||||
log->info("Server thread listening on %s:%d", host, port); |
||||
|
||||
bool listenResult = server.listen_after_bind(); |
||||
log->info("Server stopped, listen result: %d", listenResult); |
||||
} catch (const std::exception& e) { |
||||
log->error("Server thread exception: %s", e.what()); |
||||
startupPromise.set_exception(std::current_exception()); |
||||
} catch (...) { |
||||
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) { |
||||
log->error("Failed to start HTTP server - timeout"); |
||||
return false; |
||||
} |
||||
|
||||
try { |
||||
// Check if the server bound to the port successfully
|
||||
if (!startupFuture.get()) { |
||||
log->error("Failed to start HTTP server - could not bind to port"); |
||||
return false; |
||||
} |
||||
} catch (const std::exception& e) { |
||||
log->error("Failed to start HTTP server - %s", e.what()); |
||||
return false; |
||||
} |
||||
|
||||
running = true; |
||||
log->info("HTTP server is running"); |
||||
return true; |
||||
} |
||||
|
||||
void HttpServer::stop() |
||||
{ |
||||
if (!running) { |
||||
return; |
||||
} |
||||
|
||||
log->info("Stopping HTTP server..."); |
||||
server.stop(); |
||||
if (serverThread.joinable()) { |
||||
log->info("Waiting for server thread to join..."); |
||||
serverThread.join(); |
||||
} |
||||
|
||||
running = false; |
||||
log->info("HTTP server stopped"); |
||||
} |
||||
|
||||
bool HttpServer::isRunning() const |
||||
{ |
||||
return running; |
||||
} |
||||
|
||||
httplib::Server& HttpServer::getServer() |
||||
{ |
||||
return server; |
||||
} |
||||
|
||||
} // namespace nxl::autostore::infrastructure
|
||||
@ -0,0 +1,32 @@
|
||||
#pragma once |
||||
|
||||
#include "autostore/ILogger.h" |
||||
#include "application/interfaces/IAuthService.h" |
||||
#include <httplib.h> |
||||
#include <memory> |
||||
#include <string> |
||||
#include <thread> |
||||
|
||||
namespace nxl::autostore::infrastructure { |
||||
|
||||
class HttpServer |
||||
{ |
||||
public: |
||||
HttpServer(ILoggerPtr logger, application::IAuthService& authService); |
||||
~HttpServer(); |
||||
|
||||
bool start(int port = 50080, std::string_view host = "0.0.0.0"); |
||||
void stop(); |
||||
bool isRunning() const; |
||||
|
||||
httplib::Server& getServer(); |
||||
|
||||
private: |
||||
ILoggerPtr log; |
||||
application::IAuthService& authService; |
||||
bool running{false}; |
||||
httplib::Server server; |
||||
std::thread serverThread; |
||||
}; |
||||
|
||||
} // namespace nxl::autostore::infrastructure
|
||||
@ -0,0 +1,132 @@
|
||||
#include "infrastructure/repositories/FileItemRepository.h" |
||||
#include "infrastructure/helpers/JsonItem.h" |
||||
#include <fstream> |
||||
#include <algorithm> |
||||
#include <chrono> |
||||
#include <ctime> |
||||
#include <iterator> |
||||
#include "FileItemRepository.h" |
||||
|
||||
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(domain::Item::Id_t 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::findByOwner(domain::User::Id_t 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::findWhere( |
||||
std::function<bool(const domain::Item&)> predicate) |
||||
{ |
||||
std::lock_guard<std::mutex> lock(mtx); |
||||
std::vector<domain::Item> matchedItems; |
||||
std::copy_if(items.begin(), items.end(), std::back_inserter(matchedItems), |
||||
predicate); |
||||
return matchedItems; |
||||
} |
||||
|
||||
std::vector<domain::Item> |
||||
nxl::autostore::infrastructure::FileItemRepository::findWhere( |
||||
const domain::ItemExpirationSpec& spec) |
||||
{ |
||||
// Not implemented since no SQL-like query language is used
|
||||
throw std::runtime_error("Not implemented"); |
||||
} |
||||
|
||||
void FileItemRepository::remove(domain::Item::Id_t 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,32 @@
|
||||
#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(domain::Item::Id_t id) override; |
||||
std::vector<domain::Item> findByOwner(domain::User::Id_t userId) override; |
||||
std::vector<domain::Item> |
||||
findWhere(std::function<bool(const domain::Item&)> predicate) override; |
||||
virtual std::vector<domain::Item> |
||||
findWhere(const domain::ItemExpirationSpec& spec) override; |
||||
void remove(domain::Item::Id_t 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,58 @@
|
||||
#include "webapi/controllers/AuthController.h" |
||||
#include "infrastructure/helpers/JsonItem.h" |
||||
#include "infrastructure/helpers/Jsend.h" |
||||
#include "application/commands/LoginUser.h" |
||||
#include <nlohmann/json.hpp> |
||||
|
||||
namespace nxl::autostore::webapi { |
||||
|
||||
AuthController::AuthController(Context&& context) |
||||
: BaseController(std::move(context)) |
||||
{} |
||||
|
||||
std::vector<BaseController::RouteConfig> AuthController::getRoutes() const |
||||
{ |
||||
return {{"/api/v1/login", "POST", |
||||
[this](const httplib::Request& req, httplib::Response& res) { |
||||
const_cast<AuthController*>(this)->loginUser(req, res); |
||||
}}}; |
||||
} |
||||
|
||||
void AuthController::loginUser(const httplib::Request& req, |
||||
httplib::Response& res) |
||||
{ |
||||
try { |
||||
if (req.body.empty()) { |
||||
sendError(res, "Request body is empty", |
||||
httplib::StatusCode::BadRequest_400); |
||||
return; |
||||
} |
||||
|
||||
auto requestBody = nlohmann::json::parse(req.body); |
||||
std::string username = requestBody.value("username", ""); |
||||
std::string password = requestBody.value("password", ""); |
||||
|
||||
if (username.empty() || password.empty()) { |
||||
sendError(res, "Username and password are required", |
||||
httplib::StatusCode::BadRequest_400); |
||||
return; |
||||
} |
||||
|
||||
try { |
||||
std::string token = |
||||
getContext<Context>().loginUserUc.execute(username, password); |
||||
nlohmann::json responseData = nlohmann::json::object(); |
||||
responseData["token"] = token; |
||||
res.status = httplib::StatusCode::OK_200; |
||||
res.set_content(infrastructure::Jsend::success(responseData), |
||||
"application/json"); |
||||
} catch (const std::exception& e) { |
||||
sendError(res, "Authentication failed: " + std::string(e.what()), |
||||
httplib::StatusCode::Unauthorized_401); |
||||
} |
||||
} catch (const std::exception& e) { |
||||
sendError(res, e.what(), httplib::StatusCode::BadRequest_400); |
||||
} |
||||
} |
||||
|
||||
} // namespace nxl::autostore::webapi
|
||||
@ -0,0 +1,26 @@
|
||||
#pragma once |
||||
|
||||
#include "webapi/controllers/BaseController.h" |
||||
#include "application/commands/LoginUser.h" |
||||
#include <httplib.h> |
||||
|
||||
namespace nxl::autostore::webapi { |
||||
|
||||
class AuthController : public BaseController |
||||
{ |
||||
public: |
||||
struct Context |
||||
{ |
||||
application::LoginUser loginUserUc; |
||||
}; |
||||
|
||||
AuthController(Context&& context); |
||||
|
||||
protected: |
||||
std::vector<RouteConfig> getRoutes() const override; |
||||
|
||||
private: |
||||
void loginUser(const httplib::Request& req, httplib::Response& res); |
||||
}; |
||||
|
||||
} // namespace nxl::autostore::webapi
|
||||
@ -0,0 +1,23 @@
|
||||
#include "webapi/controllers/BaseController.h" |
||||
|
||||
namespace nxl::autostore::webapi { |
||||
|
||||
void BaseController::registerRoutes(httplib::Server& server) |
||||
{ |
||||
auto routes = getRoutes(); |
||||
for (const auto& route : routes) { |
||||
if (route.method == "GET") { |
||||
server.Get(route.path, route.handler); |
||||
} else if (route.method == "POST") { |
||||
server.Post(route.path, route.handler); |
||||
} else if (route.method == "PUT") { |
||||
server.Put(route.path, route.handler); |
||||
} else if (route.method == "DELETE") { |
||||
server.Delete(route.path, route.handler); |
||||
} else if (route.method == "PATCH") { |
||||
server.Patch(route.path, route.handler); |
||||
} |
||||
} |
||||
} |
||||
|
||||
} // namespace nxl::autostore::webapi
|
||||
@ -0,0 +1,86 @@
|
||||
#pragma once |
||||
|
||||
#include "infrastructure/helpers/Jsend.h" |
||||
#include <httplib.h> |
||||
#include <functional> |
||||
#include <string_view> |
||||
#include <nlohmann/json.hpp> |
||||
|
||||
namespace nxl::autostore::webapi { |
||||
|
||||
class BaseController |
||||
{ |
||||
public: |
||||
using HttpRequestHandler = |
||||
std::function<void(const httplib::Request&, httplib::Response&)>; |
||||
|
||||
struct RouteConfig |
||||
{ |
||||
std::string path; |
||||
std::string method; |
||||
HttpRequestHandler handler; |
||||
}; |
||||
|
||||
template <typename Context> |
||||
BaseController(Context&& context) |
||||
: contextStorage( |
||||
std::make_unique<ContextHolder<Context>>(std::move(context))) |
||||
{} |
||||
|
||||
virtual ~BaseController() = default; |
||||
|
||||
void registerRoutes(httplib::Server& server); |
||||
|
||||
protected: |
||||
virtual std::vector<RouteConfig> getRoutes() const = 0; |
||||
|
||||
void sendError(httplib::Response& res, std::string_view message, int status) |
||||
{ |
||||
res.status = status; |
||||
res.set_content(infrastructure::Jsend::error(message, status), |
||||
"application/json"); |
||||
} |
||||
|
||||
template <typename T> T& getContext() |
||||
{ |
||||
return static_cast<ContextHolder<T>*>(contextStorage.get())->getContext(); |
||||
} |
||||
|
||||
std::string extractUserToken(const httplib::Request& req) |
||||
{ |
||||
auto header = req.get_header_value("Authorization"); |
||||
if (header.empty()) { |
||||
throw std::runtime_error("Authorization header is missing"); |
||||
} |
||||
|
||||
if (header.substr(0, 7) != "Bearer ") { |
||||
throw std::runtime_error("Authorization header is invalid"); |
||||
} |
||||
|
||||
return header.substr(7); |
||||
} |
||||
|
||||
template <typename T, typename U> |
||||
std::optional<U> extractUserId(const httplib::Request& req) |
||||
{ |
||||
auto token = extractUserToken(req); |
||||
return getContext<T>().authService.extractUserId(token); |
||||
} |
||||
|
||||
private: |
||||
struct ContextHolderBase |
||||
{ |
||||
virtual ~ContextHolderBase() = default; |
||||
}; |
||||
|
||||
template <typename T> struct ContextHolder : ContextHolderBase |
||||
{ |
||||
ContextHolder(T&& ctx) : context(std::move(ctx)) {} |
||||
T& getContext() { return context; } |
||||
T context; |
||||
}; |
||||
|
||||
std::unique_ptr<ContextHolderBase> contextStorage; |
||||
}; |
||||
|
||||
} // namespace nxl::autostore::webapi
|
||||
@ -0,0 +1,128 @@
|
||||
#include "webapi/controllers/StoreController.h" |
||||
#include "infrastructure/helpers/JsonItem.h" |
||||
#include "infrastructure/helpers/Jsend.h" |
||||
#include "application/commands/AddItem.h" |
||||
#include "application/queries/ListItems.h" |
||||
#include "application/queries/GetItem.h" |
||||
#include "application/commands/DeleteItem.h" |
||||
#include "application/exceptions/AutoStoreExceptions.h" |
||||
#include <optional> |
||||
|
||||
namespace nxl::autostore::webapi { |
||||
|
||||
StoreController::StoreController(Context&& context) |
||||
: BaseController(std::move(context)) |
||||
{} |
||||
|
||||
std::vector<BaseController::RouteConfig> StoreController::getRoutes() const |
||||
{ |
||||
return {{"/api/v1/items", "POST", |
||||
[this](const httplib::Request& req, httplib::Response& res) { |
||||
const_cast<StoreController*>(this)->addItem(req, res); |
||||
}}, |
||||
{"/api/v1/items", "GET", |
||||
[this](const httplib::Request& req, httplib::Response& res) { |
||||
const_cast<StoreController*>(this)->listItems(req, res); |
||||
}}, |
||||
{"/api/v1/items/(.*)", "GET", |
||||
[this](const httplib::Request& req, httplib::Response& res) { |
||||
const_cast<StoreController*>(this)->getItem(req, res); |
||||
}}, |
||||
{"/api/v1/items/(.*)", "DELETE", |
||||
[this](const httplib::Request& req, httplib::Response& res) { |
||||
const_cast<StoreController*>(this)->deleteItem(req, res); |
||||
}}}; |
||||
} |
||||
|
||||
void StoreController::addItem(const httplib::Request& req, |
||||
httplib::Response& res) |
||||
{ |
||||
try { |
||||
auto userId = extractUserId<Context, domain::User::Id_t>(req); |
||||
assertUserId(userId); |
||||
auto item = infrastructure::JsonItem::fromJson(req.body); |
||||
item.userId = userId.value(); |
||||
auto itemId = getContext<Context>().addItemUc.execute(std::move(item)); |
||||
nlohmann::json responseData = nlohmann::json::object(); |
||||
responseData["id"] = itemId; |
||||
res.status = httplib::StatusCode::Created_201; |
||||
res.set_content(infrastructure::Jsend::success(responseData), |
||||
"application/json"); |
||||
} catch (const std::exception& e) { |
||||
sendError(res, e.what(), httplib::StatusCode::InternalServerError_500); |
||||
} |
||||
} |
||||
|
||||
void StoreController::listItems(const httplib::Request& req, |
||||
httplib::Response& res) |
||||
{ |
||||
try { |
||||
auto userId = extractUserId<Context, domain::User::Id_t>(req); |
||||
assertUserId(userId); |
||||
auto items = getContext<Context>().listItemsUc.execute(userId.value()); |
||||
|
||||
nlohmann::json responseData = nlohmann::json::array(); |
||||
for (const auto& item : items) { |
||||
responseData.push_back(infrastructure::JsonItem::toJsonObj(item)); |
||||
} |
||||
|
||||
res.status = httplib::StatusCode::OK_200; |
||||
res.set_content(infrastructure::Jsend::success(responseData), |
||||
"application/json"); |
||||
} catch (const std::exception& e) { |
||||
sendError(res, e.what(), httplib::StatusCode::InternalServerError_500); |
||||
} |
||||
} |
||||
|
||||
void StoreController::getItem(const httplib::Request& req, |
||||
httplib::Response& res) |
||||
{ |
||||
try { |
||||
auto itemId = req.matches[1]; |
||||
auto userId = extractUserId<Context, domain::User::Id_t>(req); |
||||
assertUserId(userId); |
||||
auto item = getContext<Context>().getItemUc.execute(itemId, userId.value()); |
||||
|
||||
auto responseData = infrastructure::JsonItem::toJsonObj(*item); |
||||
res.status = httplib::StatusCode::OK_200; |
||||
res.set_content(infrastructure::Jsend::success(responseData), |
||||
"application/json"); |
||||
} catch (const nxl::autostore::application::ItemNotFoundException& e) { |
||||
sendError(res, e.what(), httplib::StatusCode::NotFound_404); |
||||
} catch (const std::exception& e) { |
||||
sendError(res, e.what(), httplib::StatusCode::InternalServerError_500); |
||||
} |
||||
} |
||||
|
||||
void StoreController::deleteItem(const httplib::Request& req, |
||||
httplib::Response& res) |
||||
{ |
||||
try { |
||||
auto itemId = req.matches[1]; |
||||
auto userId = extractUserId<Context, domain::User::Id_t>(req); |
||||
assertUserId(userId); |
||||
getContext<Context>().deleteItemUc.execute(itemId, userId.value()); |
||||
|
||||
nlohmann::json responseData = nlohmann::json::object(); |
||||
responseData["message"] = "Item deleted successfully"; |
||||
res.status = httplib::StatusCode::NoContent_204; |
||||
|
||||
// Actually, no content should follow 204 response
|
||||
// res.set_content(infrastructure::Jsend::success(responseData),
|
||||
// "application/json");
|
||||
} catch (const nxl::autostore::application::ItemNotFoundException& e) { |
||||
sendError(res, e.what(), httplib::StatusCode::NotFound_404); |
||||
} catch (const std::exception& e) { |
||||
sendError(res, e.what(), httplib::StatusCode::InternalServerError_500); |
||||
} |
||||
} |
||||
|
||||
void StoreController::assertUserId( |
||||
std::optional<domain::User::Id_t> userId) const |
||||
{ |
||||
if (!userId) { |
||||
throw std::runtime_error("User ID not found in request"); |
||||
} |
||||
} |
||||
|
||||
} // namespace nxl::autostore::webapi
|
||||
@ -0,0 +1,40 @@
|
||||
#pragma once |
||||
|
||||
#include "webapi/controllers/BaseController.h" |
||||
#include "application/commands/AddItem.h" |
||||
#include "application/queries/ListItems.h" |
||||
#include "application/queries/GetItem.h" |
||||
#include "application/commands/DeleteItem.h" |
||||
#include "application/interfaces/IAuthService.h" |
||||
#include "infrastructure/helpers/JsonItem.h" |
||||
#include <httplib.h> |
||||
|
||||
namespace nxl::autostore::webapi { |
||||
|
||||
class StoreController : public BaseController |
||||
{ |
||||
public: |
||||
struct Context |
||||
{ |
||||
application::AddItem addItemUc; |
||||
application::ListItems listItemsUc; |
||||
application::GetItem getItemUc; |
||||
application::DeleteItem deleteItemUc; |
||||
application::IAuthService& authService; |
||||
}; |
||||
|
||||
StoreController(Context&& context); |
||||
|
||||
protected: |
||||
std::vector<RouteConfig> getRoutes() const override; |
||||
|
||||
private: |
||||
void addItem(const httplib::Request& req, httplib::Response& res); |
||||
void listItems(const httplib::Request& req, httplib::Response& res); |
||||
void getItem(const httplib::Request& req, httplib::Response& res); |
||||
void deleteItem(const httplib::Request& req, httplib::Response& res); |
||||
|
||||
void assertUserId(std::optional<domain::User::Id_t> userId) const; |
||||
}; |
||||
|
||||
} // namespace nxl::autostore::webapi
|
||||
@ -0,0 +1,38 @@
|
||||
cmake_minimum_required(VERSION 3.20) |
||||
|
||||
enable_testing() |
||||
|
||||
find_package(Catch2 CONFIG REQUIRED) |
||||
find_package(trompeloeil CONFIG REQUIRED) |
||||
|
||||
|
||||
# Macro to create test executable |
||||
function(add_test_target TEST_NAME SOURCE_FILE) |
||||
add_executable(${TEST_NAME} |
||||
${SOURCE_FILE} |
||||
) |
||||
|
||||
target_link_libraries(${TEST_NAME} |
||||
PRIVATE |
||||
AutoStoreLib |
||||
Catch2::Catch2WithMain |
||||
trompeloeil::trompeloeil |
||||
) |
||||
|
||||
target_include_directories(${TEST_NAME} |
||||
PRIVATE |
||||
${PROJECT_SOURCE_DIR}/lib/include |
||||
${PROJECT_SOURCE_DIR}/lib/src |
||||
${PROJECT_SOURCE_DIR}/tests |
||||
) |
||||
|
||||
# Add test to CTest |
||||
add_test(NAME ${TEST_NAME} COMMAND ${TEST_NAME}) |
||||
endfunction() |
||||
|
||||
# Create test executables |
||||
add_test_target(FileItemRepositoryTest integration/FileItemRepository.test.cpp) |
||||
# add_integration_test(FileUserRepositoryTest integration/FileUserRepository.test.cpp) |
||||
add_test_target(AddItemTest unit/AddItem.test.cpp) |
||||
add_test_target(SpecificationTest unit/Specification.test.cpp) |
||||
add_test_target(TaskSchedulerTest unit/TaskScheduler.test.cpp) |
||||
@ -0,0 +1,57 @@
|
||||
#pragma once |
||||
|
||||
#include "domain/entities/Item.h" |
||||
#include "domain/entities/User.h" |
||||
#include <chrono> |
||||
#include <string> |
||||
|
||||
namespace nxl::autostore::domain { |
||||
// Equality operator for Item to make trompeloeil work
|
||||
inline bool operator==(const Item& lhs, const Item& rhs) |
||||
{ |
||||
return lhs.id == rhs.id && lhs.name == rhs.name |
||||
&& lhs.orderUrl == rhs.orderUrl && lhs.userId == rhs.userId |
||||
&& lhs.expirationDate == rhs.expirationDate; |
||||
} |
||||
} // namespace nxl::autostore::domain
|
||||
|
||||
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_ORDER_URL_1 = "https://example.com/order1"; |
||||
constexpr const char* TEST_USER_ID_1 = "user123"; |
||||
constexpr const char* TEST_USER_ID_2 = "user456"; |
||||
|
||||
// Fixed test timepoint: 2020-01-01 12:00
|
||||
constexpr std::chrono::system_clock::time_point TEST_TIMEPOINT_NOW = |
||||
std::chrono::system_clock::time_point(std::chrono::seconds(1577880000)); |
||||
|
||||
// Helper function to create a test item with default values
|
||||
nxl::autostore::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)) |
||||
{ |
||||
nxl::autostore::domain::Item item; |
||||
item.id = id; |
||||
item.name = name; |
||||
item.orderUrl = orderUrl; |
||||
item.userId = userId; |
||||
item.expirationDate = expirationDate; |
||||
return item; |
||||
} |
||||
|
||||
// Helper function to create an expired test item
|
||||
nxl::autostore::domain::Item createExpiredTestItem() |
||||
{ |
||||
return createTestItem(TEST_ITEM_ID_1, TEST_ITEM_NAME_1, TEST_ORDER_URL_1, |
||||
TEST_USER_ID_1, |
||||
TEST_TIMEPOINT_NOW - std::chrono::hours(1)); |
||||
} |
||||
|
||||
} // namespace test
|
||||
@ -0,0 +1,392 @@
|
||||
#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
|
||||
auto savedItemId = repository.save(testItem); |
||||
|
||||
// Then
|
||||
auto foundItem = repository.findById(savedItemId); |
||||
REQUIRE(foundItem.has_value()); |
||||
REQUIRE(foundItem->id == savedItemId); |
||||
REQUIRE(foundItem->name == Test::TEST_ITEM_NAME_1); |
||||
REQUIRE(foundItem->orderUrl == Test::TEST_ORDER_URL_1); |
||||
REQUIRE(foundItem->userId == Test::TEST_USER_ID_1); |
||||
} |
||||
|
||||
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
|
||||
auto savedItemId = repository.save(testItem); |
||||
|
||||
// Then
|
||||
auto userItems = repository.findByOwner(Test::TEST_USER_ID_1); |
||||
REQUIRE(userItems.size() == 1); |
||||
REQUIRE(userItems[0].id == savedItemId); |
||||
REQUIRE(userItems[0].name == Test::TEST_ITEM_NAME_1); |
||||
REQUIRE(userItems[0].orderUrl == Test::TEST_ORDER_URL_1); |
||||
REQUIRE(userItems[0].userId == Test::TEST_USER_ID_1); |
||||
} |
||||
|
||||
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
|
||||
auto firstItemId = repository.save(firstItem); |
||||
auto secondItemId = repository.save(secondItem); |
||||
|
||||
// Then
|
||||
auto allItems = |
||||
repository.findWhere([](const domain::Item&) { return true; }); |
||||
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 == firstItemId) { |
||||
REQUIRE(item.name == Test::TEST_ITEM_NAME_1); |
||||
REQUIRE(item.orderUrl == Test::TEST_ORDER_URL_1); |
||||
REQUIRE(item.userId == Test::TEST_USER_ID_1); |
||||
foundFirst = true; |
||||
} else if (item.id == secondItemId) { |
||||
REQUIRE(item.name == Test::TEST_ITEM_NAME_2); |
||||
REQUIRE(item.orderUrl == Test::TEST_ORDER_URL_2); |
||||
REQUIRE(item.userId == Test::TEST_USER_ID_2); |
||||
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
|
||||
auto firstItemId = repository.save(firstItem); |
||||
auto secondItemId = repository.save(secondItem); |
||||
|
||||
// Then
|
||||
auto userItems = repository.findByOwner(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 == firstItemId) { |
||||
REQUIRE(item.name == Test::TEST_ITEM_NAME_1); |
||||
REQUIRE(item.orderUrl == Test::TEST_ORDER_URL_1); |
||||
REQUIRE(item.userId == Test::TEST_USER_ID_1); |
||||
foundFirst = true; |
||||
} else if (item.id == secondItemId) { |
||||
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(); |
||||
auto savedItemId = repository.save(testItem); |
||||
|
||||
// When
|
||||
testItem.id = savedItemId; |
||||
testItem.name = "updateditemname"; |
||||
testItem.orderUrl = "https://updated.example.com/order"; |
||||
testItem.userId = Test::TEST_USER_ID_2; |
||||
auto updatedItemId = repository.save(testItem); |
||||
|
||||
// Then
|
||||
REQUIRE(savedItemId == updatedItemId); // ID should not change for updates
|
||||
auto foundItem = repository.findById(updatedItemId); |
||||
REQUIRE(foundItem.has_value()); |
||||
REQUIRE(foundItem->id == updatedItemId); |
||||
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(); |
||||
auto savedItemId = repository.save(testItem); |
||||
|
||||
// When
|
||||
repository.remove(savedItemId); |
||||
|
||||
// Then
|
||||
auto foundItem = repository.findById(savedItemId); |
||||
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(); |
||||
auto savedItemId = repository.save(testItem); |
||||
|
||||
// When
|
||||
repository.remove(savedItemId); |
||||
|
||||
// Then
|
||||
auto userItems = repository.findByOwner(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(); |
||||
auto firstItemId = repository.save(firstItem); |
||||
auto secondItemId = repository.save(secondItem); |
||||
|
||||
// When
|
||||
repository.remove(firstItemId); |
||||
|
||||
// Then
|
||||
auto allItems = |
||||
repository.findWhere([](const domain::Item&) { return true; }); |
||||
REQUIRE(allItems.size() == 1); |
||||
REQUIRE(allItems[0].id == secondItemId); |
||||
REQUIRE(allItems[0].name == Test::TEST_ITEM_NAME_2); |
||||
REQUIRE(allItems[0].orderUrl == Test::TEST_ORDER_URL_2); |
||||
REQUIRE(allItems[0].userId == Test::TEST_USER_ID_2); |
||||
} |
||||
|
||||
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.findByOwner(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(); |
||||
auto savedItemId = repository.save(testItem); |
||||
|
||||
// When
|
||||
repository.remove(Test::NON_EXISTENT_ID); |
||||
|
||||
// Then
|
||||
auto allItems = |
||||
repository.findWhere([](const domain::Item&) { return true; }); |
||||
REQUIRE(allItems.size() == 1); |
||||
REQUIRE(allItems[0].id == savedItemId); |
||||
REQUIRE(allItems[0].name == Test::TEST_ITEM_NAME_1); |
||||
REQUIRE(allItems[0].orderUrl == Test::TEST_ORDER_URL_1); |
||||
REQUIRE(allItems[0].userId == Test::TEST_USER_ID_1); |
||||
} |
||||
|
||||
SECTION( |
||||
"when repository is created with existing data file then it loads the data") |
||||
{ |
||||
// Given
|
||||
std::string savedItemId; |
||||
{ |
||||
infrastructure::FileItemRepository firstRepository(testDbPath); |
||||
domain::Item testItem = Test::createTestItem(); |
||||
savedItemId = firstRepository.save(testItem); |
||||
} |
||||
|
||||
// When
|
||||
infrastructure::FileItemRepository secondRepository(testDbPath); |
||||
|
||||
// Then
|
||||
auto foundItem = secondRepository.findById(savedItemId); |
||||
REQUIRE(foundItem.has_value()); |
||||
REQUIRE(foundItem->id == savedItemId); |
||||
REQUIRE(foundItem->name == Test::TEST_ITEM_NAME_1); |
||||
REQUIRE(foundItem->orderUrl == Test::TEST_ORDER_URL_1); |
||||
REQUIRE(foundItem->userId == Test::TEST_USER_ID_1); |
||||
} |
||||
|
||||
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.findWhere([](const domain::Item&) { return true; }); |
||||
REQUIRE(allItems.empty()); |
||||
} |
||||
|
||||
// Clean up test environment
|
||||
Test::cleanupTestEnvironment(); |
||||
} |
||||
@ -0,0 +1,19 @@
|
||||
#pragma once |
||||
|
||||
#include "application/interfaces/IBlocker.h" |
||||
#include <trompeloeil.hpp> |
||||
|
||||
namespace test { |
||||
|
||||
class MockBlocker : public nxl::autostore::application::IBlocker |
||||
{ |
||||
public: |
||||
MAKE_MOCK0(block, void(), override); |
||||
MAKE_MOCK1(blockFor, void(const std::chrono::milliseconds&), override); |
||||
MAKE_MOCK1(blockUntil, void(const TimePoint&), override); |
||||
MAKE_MOCK0(notify, void(), override); |
||||
MAKE_MOCK0(isBlocked, bool(), override); |
||||
MAKE_MOCK0(wasNotified, bool(), override); |
||||
}; |
||||
|
||||
} // namespace test
|
||||
@ -0,0 +1,24 @@
|
||||
#pragma once |
||||
|
||||
#include "application/interfaces/IItemRepository.h" |
||||
#include <trompeloeil.hpp> |
||||
|
||||
namespace test { |
||||
|
||||
using nxl::autostore::domain::Item; |
||||
using nxl::autostore::domain::User; |
||||
using nxl::autostore::domain::ItemExpirationSpec; |
||||
|
||||
class MockItemRepository : public nxl::autostore::application::IItemRepository |
||||
{ |
||||
public: |
||||
MAKE_MOCK1(save, Item::Id_t(const Item&), override); |
||||
MAKE_MOCK1(findById, std::optional<Item>(Item::Id_t), override); |
||||
MAKE_MOCK1(findByOwner, std::vector<Item>(User::Id_t), override); |
||||
MAKE_MOCK1(findWhere, std::vector<Item>(std::function<bool(const Item&)>), |
||||
override); |
||||
MAKE_MOCK1(findWhere, std::vector<Item>(const ItemExpirationSpec&), override); |
||||
MAKE_MOCK1(remove, void(Item::Id_t), override); |
||||
}; |
||||
|
||||
} // namespace test
|
||||
@ -0,0 +1,14 @@
|
||||
#pragma once |
||||
|
||||
#include "application/interfaces/IOrderService.h" |
||||
#include <trompeloeil.hpp> |
||||
|
||||
namespace test { |
||||
|
||||
class MockOrderService : public nxl::autostore::application::IOrderService |
||||
{ |
||||
public: |
||||
MAKE_MOCK1(orderItem, void(const nxl::autostore::domain::Item&), override); |
||||
}; |
||||
|
||||
} // namespace test
|
||||
@ -0,0 +1,24 @@
|
||||
#pragma once |
||||
|
||||
#include "application/interfaces/IThreadManager.h" |
||||
#include <trompeloeil.hpp> |
||||
|
||||
namespace test { |
||||
|
||||
class MockThreadHandle |
||||
: public nxl::autostore::application::IThreadManager::ThreadHandle |
||||
{ |
||||
public: |
||||
MAKE_MOCK0(join, void(), override); |
||||
MAKE_CONST_MOCK0(joinable, bool(), override); |
||||
}; |
||||
|
||||
class MockThreadManager : public nxl::autostore::application::IThreadManager |
||||
{ |
||||
public: |
||||
MAKE_MOCK1(createThread, ThreadHandlePtr(std::function<void()>), override); |
||||
MAKE_CONST_MOCK0(getCurrentThreadId, std::thread::id(), override); |
||||
MAKE_MOCK1(sleep, void(const std::chrono::milliseconds&), override); |
||||
}; |
||||
|
||||
} // namespace test
|
||||
@ -0,0 +1,16 @@
|
||||
#pragma once |
||||
|
||||
#include "application/interfaces/ITimeProvider.h" |
||||
#include <trompeloeil.hpp> |
||||
|
||||
namespace test { |
||||
|
||||
class MockTimeProvider : public nxl::autostore::application::ITimeProvider |
||||
{ |
||||
public: |
||||
MAKE_MOCK0(now, Clock::time_point(), const override); |
||||
MAKE_MOCK1(to_tm, std::tm(const Clock::time_point&), const override); |
||||
MAKE_MOCK1(from_tm, Clock::time_point(const std::tm&), const override); |
||||
}; |
||||
|
||||
} // namespace test
|
||||
@ -0,0 +1,51 @@
|
||||
#pragma once |
||||
#include <autostore/ILogger.h> |
||||
#include <iostream> |
||||
#include <mutex> |
||||
|
||||
namespace test { |
||||
|
||||
class TestLogger : public nxl::autostore::ILogger |
||||
{ |
||||
public: |
||||
TestLogger() = default; |
||||
virtual ~TestLogger() = default; |
||||
|
||||
void log(LogLevel level, std::string_view message) override; |
||||
void vlog(int8_t, std::string_view message) override; |
||||
|
||||
private: |
||||
std::mutex mutex_; |
||||
}; |
||||
|
||||
void TestLogger::log(LogLevel level, std::string_view message) |
||||
{ |
||||
std::lock_guard<std::mutex> lock(mutex_); |
||||
const char* levelStr = ""; |
||||
switch (level) { |
||||
case LogLevel::Info: |
||||
levelStr = "INFO"; |
||||
break; |
||||
case LogLevel::Warning: |
||||
levelStr = "WARNING"; |
||||
break; |
||||
case LogLevel::Error: |
||||
levelStr = "ERROR"; |
||||
break; |
||||
case LogLevel::Debug: |
||||
levelStr = "DEBUG"; |
||||
break; |
||||
case LogLevel::Verbose: |
||||
levelStr = "VERBOSE"; |
||||
break; |
||||
} |
||||
std::cout << "[" << levelStr << "] " << message << std::endl; |
||||
} |
||||
|
||||
void TestLogger::vlog(int8_t level, std::string_view message) |
||||
{ |
||||
std::lock_guard<std::mutex> lock(mutex_); |
||||
std::cout << "[V" << static_cast<int>(level) << "] " << message << std::endl; |
||||
} |
||||
|
||||
} // namespace test
|
||||
@ -0,0 +1,223 @@
|
||||
#include "application/commands/AddItem.h" |
||||
#include "domain/entities/Item.h" |
||||
#include "mocks/MockItemRepository.h" |
||||
#include "mocks/MockTimeProvider.h" |
||||
#include "mocks/MockOrderService.h" |
||||
#include "helpers/AddItemTestHelpers.h" |
||||
#include <catch2/catch_test_macros.hpp> |
||||
#include <catch2/matchers/catch_matchers_string.hpp> |
||||
#include <trompeloeil.hpp> |
||||
#include <memory> |
||||
#include <stdexcept> |
||||
|
||||
using trompeloeil::_; |
||||
|
||||
using namespace nxl::autostore; |
||||
using namespace std::chrono; |
||||
|
||||
TEST_CASE("AddItem Unit Tests", "[unit][AddItem]") |
||||
{ |
||||
test::MockItemRepository mockRepository; |
||||
test::MockTimeProvider mockClock; |
||||
test::MockOrderService mockOrderService; |
||||
|
||||
SECTION( |
||||
"when user id is present and item is not expired then the item is saved") |
||||
{ |
||||
// Given
|
||||
auto testItem = test::createTestItem(); |
||||
auto expectedItemId = "saved_item_id"; |
||||
|
||||
REQUIRE_CALL(mockRepository, save(testItem)).RETURN(expectedItemId); |
||||
REQUIRE_CALL(mockClock, now()).RETURN(test::TEST_TIMEPOINT_NOW); |
||||
FORBID_CALL(mockOrderService, orderItem(_)); |
||||
|
||||
application::AddItem addItem(mockRepository, mockClock, mockOrderService); |
||||
|
||||
// When
|
||||
auto resultItemId = addItem.execute(std::move(testItem)); |
||||
|
||||
// Then
|
||||
REQUIRE(resultItemId == expectedItemId); |
||||
} |
||||
|
||||
SECTION("when item has null user id then a runtime error is thrown") |
||||
{ |
||||
// Given
|
||||
auto testItem = test::createTestItem(); |
||||
testItem.userId = domain::User::NULL_ID; |
||||
|
||||
FORBID_CALL(mockRepository, save(_)); |
||||
FORBID_CALL(mockClock, now()); |
||||
FORBID_CALL(mockOrderService, orderItem(_)); |
||||
|
||||
application::AddItem addItem(mockRepository, mockClock, mockOrderService); |
||||
|
||||
// When & Then
|
||||
REQUIRE_THROWS_AS(addItem.execute(std::move(testItem)), std::runtime_error); |
||||
} |
||||
|
||||
SECTION("when item is expired then the order is placed") |
||||
{ |
||||
// Given
|
||||
auto testItem = test::createExpiredTestItem(); |
||||
|
||||
REQUIRE_CALL(mockClock, now()).RETURN(test::TEST_TIMEPOINT_NOW); |
||||
REQUIRE_CALL(mockOrderService, orderItem(_)); |
||||
FORBID_CALL(mockRepository, save(_)); |
||||
|
||||
application::AddItem addItem(mockRepository, mockClock, mockOrderService); |
||||
|
||||
// When
|
||||
addItem.execute(std::move(testItem)); |
||||
|
||||
// Then
|
||||
// Order was placed (verified by REQUIRE_CALL above)
|
||||
} |
||||
|
||||
SECTION("when item is expired then null id is returned") |
||||
{ |
||||
// Given
|
||||
auto testItem = test::createExpiredTestItem(); |
||||
|
||||
REQUIRE_CALL(mockClock, now()).RETURN(test::TEST_TIMEPOINT_NOW); |
||||
REQUIRE_CALL(mockOrderService, orderItem(_)); |
||||
FORBID_CALL(mockRepository, save(_)); |
||||
|
||||
application::AddItem addItem(mockRepository, mockClock, mockOrderService); |
||||
|
||||
// When
|
||||
auto resultItemId = addItem.execute(std::move(testItem)); |
||||
|
||||
// Then
|
||||
REQUIRE(resultItemId == domain::Item::NULL_ID); |
||||
} |
||||
|
||||
SECTION("when item expiration date is exactly current time then the order is " |
||||
"placed") |
||||
{ |
||||
// Given
|
||||
auto testItem = test::createTestItem(); |
||||
testItem.expirationDate = test::TEST_TIMEPOINT_NOW; |
||||
|
||||
REQUIRE_CALL(mockClock, now()).RETURN(test::TEST_TIMEPOINT_NOW); |
||||
REQUIRE_CALL(mockOrderService, orderItem(_)); |
||||
FORBID_CALL(mockRepository, save(_)); |
||||
|
||||
application::AddItem addItem(mockRepository, mockClock, mockOrderService); |
||||
|
||||
// When
|
||||
addItem.execute(std::move(testItem)); |
||||
|
||||
// Then
|
||||
// Order was placed (verified by REQUIRE_CALL above)
|
||||
} |
||||
|
||||
SECTION("when item expiration date is exactly current time then null id is " |
||||
"returned") |
||||
{ |
||||
// Given
|
||||
auto testItem = test::createTestItem(); |
||||
testItem.expirationDate = test::TEST_TIMEPOINT_NOW; |
||||
|
||||
REQUIRE_CALL(mockClock, now()).RETURN(test::TEST_TIMEPOINT_NOW); |
||||
REQUIRE_CALL(mockOrderService, orderItem(_)); |
||||
FORBID_CALL(mockRepository, save(_)); |
||||
|
||||
application::AddItem addItem(mockRepository, mockClock, mockOrderService); |
||||
|
||||
// When
|
||||
auto resultItemId = addItem.execute(std::move(testItem)); |
||||
|
||||
// Then
|
||||
REQUIRE(resultItemId == domain::Item::NULL_ID); |
||||
} |
||||
|
||||
SECTION("when item expiration date is in the future then the item is saved") |
||||
{ |
||||
// Given
|
||||
auto testItem = test::createTestItem(); |
||||
auto expectedItemId = "saved_item_id"; |
||||
|
||||
REQUIRE_CALL(mockClock, now()).RETURN(test::TEST_TIMEPOINT_NOW); |
||||
REQUIRE_CALL(mockRepository, save(testItem)).RETURN(expectedItemId); |
||||
FORBID_CALL(mockOrderService, orderItem(_)); |
||||
|
||||
application::AddItem addItem(mockRepository, mockClock, mockOrderService); |
||||
|
||||
// When
|
||||
addItem.execute(std::move(testItem)); |
||||
|
||||
// Then
|
||||
// Item was saved (verified by REQUIRE_CALL above)
|
||||
} |
||||
|
||||
SECTION( |
||||
"when item expiration date is in the future then the item id is returned") |
||||
{ |
||||
// Given
|
||||
auto testItem = test::createTestItem(); |
||||
auto expectedItemId = "saved_item_id"; |
||||
|
||||
REQUIRE_CALL(mockClock, now()).RETURN(test::TEST_TIMEPOINT_NOW); |
||||
REQUIRE_CALL(mockRepository, save(testItem)).RETURN(expectedItemId); |
||||
FORBID_CALL(mockOrderService, orderItem(_)); |
||||
|
||||
application::AddItem addItem(mockRepository, mockClock, mockOrderService); |
||||
|
||||
// When
|
||||
auto resultItemId = addItem.execute(std::move(testItem)); |
||||
|
||||
// Then
|
||||
REQUIRE(resultItemId == expectedItemId); |
||||
} |
||||
|
||||
SECTION( |
||||
"when repository save throws exception then a runtime error is thrown") |
||||
{ |
||||
// Given
|
||||
auto testItem = test::createTestItem(); |
||||
auto expectedException = std::runtime_error("Repository error"); |
||||
|
||||
REQUIRE_CALL(mockClock, now()).RETURN(test::TEST_TIMEPOINT_NOW); |
||||
REQUIRE_CALL(mockRepository, save(testItem)).THROW(expectedException); |
||||
FORBID_CALL(mockOrderService, orderItem(_)); |
||||
|
||||
application::AddItem addItem(mockRepository, mockClock, mockOrderService); |
||||
|
||||
// When & Then
|
||||
REQUIRE_THROWS_AS(addItem.execute(std::move(testItem)), std::runtime_error); |
||||
} |
||||
|
||||
SECTION("when order service throws exception then a runtime error is thrown") |
||||
{ |
||||
// Given
|
||||
auto testItem = test::createExpiredTestItem(); |
||||
auto expectedException = std::runtime_error("Order service error"); |
||||
|
||||
REQUIRE_CALL(mockClock, now()).RETURN(test::TEST_TIMEPOINT_NOW); |
||||
REQUIRE_CALL(mockOrderService, orderItem(_)).THROW(expectedException); |
||||
FORBID_CALL(mockRepository, save(_)); |
||||
|
||||
application::AddItem addItem(mockRepository, mockClock, mockOrderService); |
||||
|
||||
// When & Then
|
||||
REQUIRE_THROWS_AS(addItem.execute(std::move(testItem)), std::runtime_error); |
||||
} |
||||
|
||||
SECTION("when clock throws exception then a runtime error is thrown") |
||||
{ |
||||
// Given
|
||||
auto testItem = test::createTestItem(); |
||||
auto expectedException = std::runtime_error("Clock error"); |
||||
|
||||
REQUIRE_CALL(mockClock, now()).THROW(expectedException); |
||||
FORBID_CALL(mockRepository, save(_)); |
||||
FORBID_CALL(mockOrderService, orderItem(_)); |
||||
|
||||
application::AddItem addItem(mockRepository, mockClock, mockOrderService); |
||||
|
||||
// When & Then
|
||||
REQUIRE_THROWS_AS(addItem.execute(std::move(testItem)), std::runtime_error); |
||||
} |
||||
} |
||||
@ -0,0 +1,692 @@
|
||||
#include "domain/helpers/Specification.h" |
||||
#include <catch2/catch_test_macros.hpp> |
||||
#include <catch2/matchers/catch_matchers_string.hpp> |
||||
#include <trompeloeil.hpp> |
||||
#include <memory> |
||||
#include <stdexcept> |
||||
#include <chrono> |
||||
|
||||
using trompeloeil::_; |
||||
using namespace nxl::helpers; |
||||
|
||||
TEST_CASE("Specification Unit Tests", "[unit][Specification]") |
||||
{ |
||||
const Renderer TEST_RENDERER{ |
||||
.opEq = "=", |
||||
.opNe = "!=", |
||||
.opLt = "<", |
||||
.opLe = "<=", |
||||
.opGt = ">", |
||||
.opGe = ">=", |
||||
.opLike = "LIKE", |
||||
.opIsNull = "IS NULL", |
||||
.opIsNotNull = "IS NOT NULL", |
||||
.opAnd = "AND", |
||||
.opOr = "OR", |
||||
.groupStart = "(", |
||||
.groupEnd = ")", |
||||
.formatValue = [](const ConditionValue& v) -> std::string { |
||||
if (v.is<std::string>()) { |
||||
return "'" + v.as<std::string>() + "'"; |
||||
} else if (v.is<const char*>()) { |
||||
return "'" + std::string(v.as<const char*>()) + "'"; |
||||
} else if (v.is<bool>()) { |
||||
return v.as<bool>() ? "TRUE" : "FALSE"; |
||||
} else if (v.is<int>()) { |
||||
return std::to_string(v.as<int>()); |
||||
} else if (v.is<double>()) { |
||||
return std::to_string(v.as<double>()); |
||||
} else if (v.is<std::chrono::system_clock::time_point>()) { |
||||
auto time = v.as<std::chrono::system_clock::time_point>(); |
||||
auto time_t = std::chrono::system_clock::to_time_t(time); |
||||
std::string time_str = std::ctime(&time_t); |
||||
if (!time_str.empty() && time_str.back() == '\n') { |
||||
time_str.pop_back(); |
||||
} |
||||
return "'" + time_str + "'"; |
||||
} |
||||
return "'unknown'"; |
||||
}}; |
||||
|
||||
SECTION("when single equals string condition is added then proper operator " |
||||
"is rendered") |
||||
{ |
||||
// Given
|
||||
auto spec = makeSpecification().field("F").equals("foo").build(); |
||||
|
||||
// When
|
||||
auto result = spec->render(TEST_RENDERER); |
||||
|
||||
// Then
|
||||
REQUIRE(result == "(F = 'foo')"); |
||||
} |
||||
|
||||
SECTION( |
||||
"when condition with chrono time_point is used then it renders correctly") |
||||
{ |
||||
// Given
|
||||
auto now = std::chrono::system_clock::now(); |
||||
auto spec = makeSpecification().field("timestamp").greaterThan(now).build(); |
||||
|
||||
// When
|
||||
auto result = spec->render(TEST_RENDERER); |
||||
|
||||
// Then
|
||||
REQUIRE(result.find("timestamp >") != std::string::npos); |
||||
REQUIRE(result.find("'") != std::string::npos); |
||||
} |
||||
|
||||
SECTION("when single not equals string condition is added then proper " |
||||
"operator is rendered") |
||||
{ |
||||
// Given
|
||||
auto spec = makeSpecification().field("F").notEquals("foo").build(); |
||||
|
||||
// When
|
||||
auto result = spec->render(TEST_RENDERER); |
||||
|
||||
// Then
|
||||
REQUIRE(result == "(F != 'foo')"); |
||||
} |
||||
|
||||
SECTION( |
||||
"when single less than condition is added then proper operator is rendered") |
||||
{ |
||||
// Given
|
||||
auto spec = makeSpecification().field("F").lessThan(42).build(); |
||||
|
||||
// When
|
||||
auto result = spec->render(TEST_RENDERER); |
||||
|
||||
// Then
|
||||
REQUIRE(result == "(F < 42)"); |
||||
} |
||||
|
||||
SECTION("when single less than or equal condition is added then proper " |
||||
"operator is rendered") |
||||
{ |
||||
// Given
|
||||
auto spec = makeSpecification().field("F").lessOrEqual(42).build(); |
||||
|
||||
// When
|
||||
auto result = spec->render(TEST_RENDERER); |
||||
|
||||
// Then
|
||||
REQUIRE(result == "(F <= 42)"); |
||||
} |
||||
|
||||
SECTION("when single greater than condition is added then proper operator is " |
||||
"rendered") |
||||
{ |
||||
// Given
|
||||
auto spec = makeSpecification().field("F").greaterThan(42).build(); |
||||
|
||||
// When
|
||||
auto result = spec->render(TEST_RENDERER); |
||||
|
||||
// Then
|
||||
REQUIRE(result == "(F > 42)"); |
||||
} |
||||
|
||||
SECTION("when single greater than or equal condition is added then proper " |
||||
"operator is rendered") |
||||
{ |
||||
// Given
|
||||
auto spec = makeSpecification().field("F").greaterOrEqual(42).build(); |
||||
|
||||
// When
|
||||
auto result = spec->render(TEST_RENDERER); |
||||
|
||||
// Then
|
||||
REQUIRE(result == "(F >= 42)"); |
||||
} |
||||
|
||||
SECTION( |
||||
"when single like condition is added then proper operator is rendered") |
||||
{ |
||||
// Given
|
||||
auto spec = makeSpecification().field("F").like("%foo%").build(); |
||||
|
||||
// When
|
||||
auto result = spec->render(TEST_RENDERER); |
||||
|
||||
// Then
|
||||
REQUIRE(result == "(F LIKE '%foo%')"); |
||||
} |
||||
|
||||
SECTION( |
||||
"when single is null condition is added then proper operator is rendered") |
||||
{ |
||||
// Given
|
||||
auto spec = makeSpecification().field("F").isNull().build(); |
||||
|
||||
// When
|
||||
auto result = spec->render(TEST_RENDERER); |
||||
|
||||
// Then
|
||||
REQUIRE(result == "(F IS NULL)"); |
||||
} |
||||
|
||||
SECTION("when single is not null condition is added then proper operator is " |
||||
"rendered") |
||||
{ |
||||
// Given
|
||||
auto spec = makeSpecification().field("F").isNotNull().build(); |
||||
|
||||
// When
|
||||
auto result = spec->render(TEST_RENDERER); |
||||
|
||||
// Then
|
||||
REQUIRE(result == "(F IS NOT NULL)"); |
||||
} |
||||
|
||||
SECTION("when condition with double value is used then it renders correctly") |
||||
{ |
||||
// Given
|
||||
auto spec = makeSpecification().field("price").equals(19.99).build(); |
||||
|
||||
// When
|
||||
auto result = spec->render(TEST_RENDERER); |
||||
|
||||
// Then
|
||||
REQUIRE(result == "(price = 19.990000)"); |
||||
} |
||||
|
||||
SECTION("when condition with boolean value is used then it renders correctly") |
||||
{ |
||||
// Given
|
||||
auto spec = makeSpecification().field("active").equals(true).build(); |
||||
|
||||
// When
|
||||
auto result = spec->render(TEST_RENDERER); |
||||
|
||||
// Then
|
||||
REQUIRE(result == "(active = TRUE)"); |
||||
} |
||||
|
||||
SECTION( |
||||
"when condition with const char* value is used then it renders correctly") |
||||
{ |
||||
// Given
|
||||
auto spec = makeSpecification().field("name").equals("test").build(); |
||||
|
||||
// When
|
||||
auto result = spec->render(TEST_RENDERER); |
||||
|
||||
// Then
|
||||
REQUIRE(result == "(name = 'test')"); |
||||
} |
||||
|
||||
SECTION("when multiple AND conditions are added then they are rendered with " |
||||
"AND operator") |
||||
{ |
||||
// Given
|
||||
auto spec = makeSpecification() |
||||
.field("name") |
||||
.equals("John") |
||||
.field("age") |
||||
.greaterThan(30) |
||||
.field("active") |
||||
.equals(true) |
||||
.build(); |
||||
|
||||
// When
|
||||
auto result = spec->render(TEST_RENDERER); |
||||
|
||||
// Then
|
||||
REQUIRE(result == "(name = 'John' AND age > 30 AND active = TRUE)"); |
||||
} |
||||
|
||||
SECTION("when AND group is created then conditions are grouped properly") |
||||
{ |
||||
// Given
|
||||
auto spec = makeSpecification() |
||||
.field("name") |
||||
.equals("John") |
||||
.andGroup() |
||||
.field("age") |
||||
.greaterThan(30) |
||||
.field("active") |
||||
.equals(true) |
||||
.endGroup() |
||||
.build(); |
||||
|
||||
// When
|
||||
auto result = spec->render(TEST_RENDERER); |
||||
|
||||
// Then
|
||||
REQUIRE(result == "(name = 'John' AND (age > 30 AND active = TRUE))"); |
||||
} |
||||
|
||||
SECTION( |
||||
"when OR group is created then conditions are grouped with OR operator") |
||||
{ |
||||
// Given
|
||||
auto spec = makeSpecification() |
||||
.orGroup() |
||||
.field("name") |
||||
.equals("John") |
||||
.field("age") |
||||
.greaterThan(30) |
||||
.field("active") |
||||
.equals(true) |
||||
.endGroup() |
||||
.build(); |
||||
|
||||
// When
|
||||
auto result = spec->render(TEST_RENDERER); |
||||
|
||||
// Then
|
||||
REQUIRE(result == "((name = 'John' OR age > 30 OR active = TRUE))"); |
||||
} |
||||
|
||||
SECTION("when nested groups are created then they are rendered properly") |
||||
{ |
||||
// Given
|
||||
auto spec = makeSpecification() |
||||
.field("category") |
||||
.equals("electronics") |
||||
.andGroup() |
||||
.field("price") |
||||
.lessThan(1000) |
||||
.orGroup() |
||||
.field("brand") |
||||
.equals("Sony") |
||||
.field("brand") |
||||
.equals("Samsung") |
||||
.endGroup() |
||||
.endGroup() |
||||
.build(); |
||||
|
||||
// When
|
||||
auto result = spec->render(TEST_RENDERER); |
||||
|
||||
// Then
|
||||
REQUIRE( |
||||
result |
||||
== "(category = 'electronics' AND (price < 1000 AND (brand = 'Sony' " |
||||
"OR brand = 'Samsung')))"); |
||||
} |
||||
|
||||
SECTION("when complex specification with multiple nested groups is created " |
||||
"then it renders correctly") |
||||
{ |
||||
// Given
|
||||
auto spec = makeSpecification() |
||||
.orGroup() |
||||
.andGroup() |
||||
.field("type") |
||||
.equals("food") |
||||
.field("expiration_date") |
||||
.lessOrEqual("2023-01-01") |
||||
.endGroup() |
||||
.andGroup() |
||||
.field("type") |
||||
.equals("non-food") |
||||
.field("expiration_date") |
||||
.lessOrEqual("2023-03-01") |
||||
.endGroup() |
||||
.endGroup() |
||||
.build(); |
||||
|
||||
// When
|
||||
auto result = spec->render(TEST_RENDERER); |
||||
|
||||
// Then
|
||||
REQUIRE(result |
||||
== "(((type = 'food' AND expiration_date <= '2023-01-01') OR (type " |
||||
"= 'non-food' AND expiration_date <= '2023-03-01')))"); |
||||
} |
||||
|
||||
SECTION("when empty group is created then it renders as empty") |
||||
{ |
||||
// Given
|
||||
auto spec = makeSpecification() |
||||
.field("name") |
||||
.equals("John") |
||||
.andGroup() |
||||
.endGroup() |
||||
.build(); |
||||
|
||||
// When
|
||||
auto result = spec->render(TEST_RENDERER); |
||||
|
||||
// Then
|
||||
REQUIRE(result == "(name = 'John' AND )"); |
||||
} |
||||
|
||||
SECTION("when endGroup is called without matching startGroup then it should " |
||||
"handle gracefully") |
||||
{ |
||||
// Given
|
||||
auto spec = |
||||
makeSpecification().field("name").equals("John").endGroup().build(); |
||||
|
||||
// When
|
||||
auto result = spec->render(TEST_RENDERER); |
||||
|
||||
// Then
|
||||
REQUIRE(result == "(name = 'John')"); |
||||
} |
||||
|
||||
SECTION("when condition is added without field then exception is thrown") |
||||
{ |
||||
// Given
|
||||
auto builder = makeSpecification(); |
||||
|
||||
// When/Then
|
||||
REQUIRE_THROWS_AS(builder.equals("value"), std::runtime_error); |
||||
REQUIRE_THROWS_WITH(builder.equals("value"), |
||||
"No field specified for condition"); |
||||
} |
||||
|
||||
SECTION("when field is called multiple times then last field is used") |
||||
{ |
||||
// Given
|
||||
auto spec = makeSpecification() |
||||
.field("field1") |
||||
.field("field2") |
||||
.equals("value") |
||||
.build(); |
||||
|
||||
// When
|
||||
auto result = spec->render(TEST_RENDERER); |
||||
|
||||
// Then
|
||||
REQUIRE(result == "(field2 = 'value')"); |
||||
} |
||||
|
||||
SECTION("when multiple conditions with same field are added then they are " |
||||
"rendered correctly") |
||||
{ |
||||
// Given
|
||||
auto spec = makeSpecification() |
||||
.field("age") |
||||
.greaterThan(18) |
||||
.field("age") |
||||
.lessThan(65) |
||||
.build(); |
||||
|
||||
// When
|
||||
auto result = spec->render(TEST_RENDERER); |
||||
|
||||
// Then
|
||||
REQUIRE(result == "(age > 18 AND age < 65)"); |
||||
} |
||||
|
||||
SECTION("when specification has only conditions with no explicit grouping " |
||||
"then AND is used") |
||||
{ |
||||
// Given
|
||||
auto spec = makeSpecification() |
||||
.field("field1") |
||||
.equals("value1") |
||||
.field("field2") |
||||
.equals("value2") |
||||
.field("field3") |
||||
.equals("value3") |
||||
.build(); |
||||
|
||||
// When
|
||||
auto result = spec->render(TEST_RENDERER); |
||||
|
||||
// Then
|
||||
REQUIRE( |
||||
result |
||||
== "(field1 = 'value1' AND field2 = 'value2' AND field3 = 'value3')"); |
||||
} |
||||
|
||||
SECTION("when mixed data types are used then they render correctly") |
||||
{ |
||||
// Given
|
||||
auto now = std::chrono::system_clock::now(); |
||||
auto spec = makeSpecification() |
||||
.field("name") |
||||
.equals("John") |
||||
.field("age") |
||||
.greaterThan(30) |
||||
.field("salary") |
||||
.greaterOrEqual(50000.50) |
||||
.field("active") |
||||
.equals(true) |
||||
.field("created_at") |
||||
.lessThan(now) |
||||
.build(); |
||||
|
||||
// When
|
||||
auto result = spec->render(TEST_RENDERER); |
||||
|
||||
// Then
|
||||
REQUIRE(result.find("(name = 'John' AND age > 30 AND salary >= " |
||||
"50000.500000 AND active = TRUE AND created_at < '") |
||||
!= std::string::npos); |
||||
REQUIRE(result.find("'") != std::string::npos); |
||||
} |
||||
|
||||
SECTION("when ConditionValue is created with string then it stores and " |
||||
"retrieves correctly") |
||||
{ |
||||
// Given
|
||||
ConditionValue value(std::string("test")); |
||||
|
||||
// Then
|
||||
REQUIRE(value.is<std::string>()); |
||||
REQUIRE_FALSE(value.is<int>()); |
||||
REQUIRE(value.as<std::string>() == "test"); |
||||
} |
||||
|
||||
SECTION("when ConditionValue is created with int then it stores and " |
||||
"retrieves correctly") |
||||
{ |
||||
// Given
|
||||
ConditionValue value(42); |
||||
|
||||
// Then
|
||||
REQUIRE(value.is<int>()); |
||||
REQUIRE_FALSE(value.is<std::string>()); |
||||
REQUIRE(value.as<int>() == 42); |
||||
} |
||||
|
||||
SECTION("when ConditionValue is created with double then it stores and " |
||||
"retrieves correctly") |
||||
{ |
||||
// Given
|
||||
ConditionValue value(3.14); |
||||
|
||||
// Then
|
||||
REQUIRE(value.is<double>()); |
||||
REQUIRE_FALSE(value.is<int>()); |
||||
REQUIRE(value.as<double>() == 3.14); |
||||
} |
||||
|
||||
SECTION("when ConditionValue is created with bool then it stores and " |
||||
"retrieves correctly") |
||||
{ |
||||
// Given
|
||||
ConditionValue value(true); |
||||
|
||||
// Then
|
||||
REQUIRE(value.is<bool>()); |
||||
REQUIRE_FALSE(value.is<int>()); |
||||
REQUIRE(value.as<bool>() == true); |
||||
} |
||||
|
||||
SECTION("when ConditionValue is created with time_point then it stores and " |
||||
"retrieves correctly") |
||||
{ |
||||
// Given
|
||||
auto now = std::chrono::system_clock::now(); |
||||
ConditionValue value(now); |
||||
|
||||
// Then
|
||||
REQUIRE(value.is<std::chrono::system_clock::time_point>()); |
||||
REQUIRE_FALSE(value.is<int>()); |
||||
REQUIRE(value.as<std::chrono::system_clock::time_point>() == now); |
||||
} |
||||
|
||||
SECTION( |
||||
"when renderer has custom formatValue function then it is used correctly") |
||||
{ |
||||
// Given
|
||||
Renderer customRenderer{ |
||||
.opEq = "=", |
||||
.opNe = "!=", |
||||
.opLt = "<", |
||||
.opLe = "<=", |
||||
.opGt = ">", |
||||
.opGe = ">=", |
||||
.opLike = "LIKE", |
||||
.opIsNull = "IS NULL", |
||||
.opIsNotNull = "IS NOT NULL", |
||||
.opAnd = "AND", |
||||
.opOr = "OR", |
||||
.groupStart = "[", |
||||
.groupEnd = "]", |
||||
.formatValue = [](const ConditionValue& v) -> std::string { |
||||
if (v.is<std::string>()) { |
||||
return "\"" + v.as<std::string>() + "\""; |
||||
} else if (v.is<const char*>()) { |
||||
return "\"" + std::string(v.as<const char*>()) + "\""; |
||||
} else if (v.is<int>()) { |
||||
return "INT:" + std::to_string(v.as<int>()); |
||||
} |
||||
return "CUSTOM"; |
||||
}}; |
||||
|
||||
auto spec = makeSpecification() |
||||
.field("name") |
||||
.equals(std::string("John")) |
||||
.field("age") |
||||
.greaterThan(30) |
||||
.build(); |
||||
|
||||
// When
|
||||
auto result = spec->render(customRenderer); |
||||
|
||||
// Then
|
||||
REQUIRE(result == "[name = \"John\" AND age > INT:30]"); |
||||
} |
||||
|
||||
SECTION( |
||||
"when renderer has custom grouping symbols then they are used correctly") |
||||
{ |
||||
// Given
|
||||
Renderer customRenderer{ |
||||
.opEq = "=", |
||||
.opNe = "!=", |
||||
.opLt = "<", |
||||
.opLe = "<=", |
||||
.opGt = ">", |
||||
.opGe = ">=", |
||||
.opLike = "LIKE", |
||||
.opIsNull = "IS NULL", |
||||
.opIsNotNull = "IS NOT NULL", |
||||
.opAnd = "AND", |
||||
.opOr = "OR", |
||||
.groupStart = "{", |
||||
.groupEnd = "}", |
||||
.formatValue = [](const ConditionValue& v) -> std::string { |
||||
if (v.is<std::string>()) { |
||||
return "'" + v.as<std::string>() + "'"; |
||||
} else if (v.is<const char*>()) { |
||||
return "'" + std::string(v.as<const char*>()) + "'"; |
||||
} else if (v.is<int>()) { |
||||
return std::to_string(v.as<int>()); |
||||
} |
||||
return "'unknown'"; |
||||
}}; |
||||
|
||||
auto spec = makeSpecification() |
||||
.field("name") |
||||
.equals(std::string("John")) |
||||
.andGroup() |
||||
.field("age") |
||||
.greaterThan(30) |
||||
.endGroup() |
||||
.build(); |
||||
|
||||
// When
|
||||
auto result = spec->render(customRenderer); |
||||
|
||||
// Then
|
||||
REQUIRE(result == "{name = 'John' AND {age > 30}}"); |
||||
} |
||||
|
||||
SECTION( |
||||
"when renderer has custom logical operators then they are used correctly") |
||||
{ |
||||
// Given
|
||||
Renderer customRenderer{ |
||||
.opEq = "=", |
||||
.opNe = "!=", |
||||
.opLt = "<", |
||||
.opLe = "<=", |
||||
.opGt = ">", |
||||
.opGe = ">=", |
||||
.opLike = "LIKE", |
||||
.opIsNull = "IS NULL", |
||||
.opIsNotNull = "IS NOT NULL", |
||||
.opAnd = "&&", |
||||
.opOr = "||", |
||||
.groupStart = "(", |
||||
.groupEnd = ")", |
||||
.formatValue = [](const ConditionValue& v) -> std::string { |
||||
if (v.is<std::string>()) { |
||||
return "'" + v.as<std::string>() + "'"; |
||||
} else if (v.is<const char*>()) { |
||||
return "'" + std::string(v.as<const char*>()) + "'"; |
||||
} else if (v.is<int>()) { |
||||
return std::to_string(v.as<int>()); |
||||
} else if (v.is<bool>()) { |
||||
return v.as<bool>() ? "TRUE" : "FALSE"; |
||||
} |
||||
return "'unknown'"; |
||||
}}; |
||||
|
||||
auto spec = makeSpecification() |
||||
.orGroup() |
||||
.field("name") |
||||
.equals(std::string("John")) |
||||
.orGroup() |
||||
.field("age") |
||||
.greaterThan(30) |
||||
.field("active") |
||||
.equals(true) |
||||
.endGroup() |
||||
.endGroup() |
||||
.build(); |
||||
|
||||
// When
|
||||
auto result = spec->render(customRenderer); |
||||
|
||||
// Then
|
||||
REQUIRE(result == "((name = 'John' || (age > 30 || active = TRUE)))"); |
||||
} |
||||
|
||||
SECTION( |
||||
"when ConditionGroup has no children then render returns empty string") |
||||
{ |
||||
// Given
|
||||
ConditionGroup group(LogicalOp::AND); |
||||
|
||||
// When
|
||||
auto result = group.render(TEST_RENDERER); |
||||
|
||||
// Then
|
||||
REQUIRE(result == ""); |
||||
} |
||||
|
||||
SECTION("when makeSpecification helper is used then it creates valid builder") |
||||
{ |
||||
// Given
|
||||
auto builder = makeSpecification(); |
||||
|
||||
// When
|
||||
auto spec = builder.field("test").equals("value").build(); |
||||
|
||||
// Then
|
||||
REQUIRE(spec != nullptr); |
||||
auto result = spec->render(TEST_RENDERER); |
||||
REQUIRE(result == "(test = 'value')"); |
||||
} |
||||
} |
||||
@ -0,0 +1,436 @@
|
||||
#include "application/services/TaskScheduler.h" |
||||
#include "mocks/TestLogger.h" |
||||
#include "mocks/MockTimeProvider.h" |
||||
#include "mocks/MockThreadManager.h" |
||||
#include "mocks/MockBlocker.h" |
||||
#include <catch2/catch_test_macros.hpp> |
||||
#include <catch2/matchers/catch_matchers_string.hpp> |
||||
#include <memory> |
||||
#include <atomic> |
||||
|
||||
using trompeloeil::_; |
||||
|
||||
using namespace nxl::autostore; |
||||
using namespace std::chrono; |
||||
using nxl::autostore::application::TaskScheduler; |
||||
|
||||
namespace test { |
||||
|
||||
// Fixed test timepoint: 2020-01-01 12:00
|
||||
constexpr std::chrono::system_clock::time_point TIMEPOINT_NOW = |
||||
std::chrono::system_clock::time_point(std::chrono::seconds(1577880000)); |
||||
|
||||
} // namespace test
|
||||
|
||||
TEST_CASE("TaskScheduler Unit Tests", "[unit][TaskScheduler]") |
||||
{ |
||||
// Common mock objects that all sections can use
|
||||
auto logger = std::make_shared<test::TestLogger>(); |
||||
auto timeProvider = std::make_unique<test::MockTimeProvider>(); |
||||
auto threadMgr = std::make_unique<test::MockThreadManager>(); |
||||
auto blocker = std::make_unique<test::MockBlocker>(); |
||||
|
||||
SECTION("when start is called then createThread is called") |
||||
{ |
||||
// Given
|
||||
// Expect createThread to be called
|
||||
REQUIRE_CALL(*threadMgr, createThread(_)) |
||||
.RETURN(std::make_unique<test::MockThreadHandle>()); |
||||
|
||||
TaskScheduler scheduler(logger, *timeProvider, *threadMgr, |
||||
std::move(blocker)); |
||||
|
||||
// When
|
||||
scheduler.start(); |
||||
} |
||||
|
||||
SECTION("when scheduler is created then it is not running") |
||||
{ |
||||
// Given - recreate blocker for this test since it was moved in previous
|
||||
// section
|
||||
auto testBlocker = std::make_unique<test::MockBlocker>(); |
||||
|
||||
// When
|
||||
TaskScheduler scheduler(logger, *timeProvider, *threadMgr, |
||||
std::move(testBlocker)); |
||||
|
||||
// Then - calling stop on a non-running scheduler should not cause issues
|
||||
// and no thread operations should be called
|
||||
FORBID_CALL(*threadMgr, createThread(_)); |
||||
scheduler.stop(); |
||||
} |
||||
|
||||
SECTION("when task is scheduled with OnStart mode then it executes " |
||||
"immediately after start") |
||||
{ |
||||
// Given
|
||||
bool taskExecuted = false; |
||||
std::function<void()> threadFn; |
||||
|
||||
// Recreate blocker for this test
|
||||
auto testBlocker = std::make_unique<test::MockBlocker>(); |
||||
|
||||
// Expect createThread to be called, save thread function
|
||||
REQUIRE_CALL(*threadMgr, createThread(_)) |
||||
.RETURN(std::make_unique<test::MockThreadHandle>()) |
||||
.LR_SIDE_EFFECT(threadFn = std::move(_1)); |
||||
|
||||
ALLOW_CALL(*timeProvider, now()).LR_RETURN(test::TIMEPOINT_NOW); |
||||
FORBID_CALL(*testBlocker, blockFor(_)); |
||||
|
||||
TaskScheduler scheduler(logger, *timeProvider, *threadMgr, |
||||
std::move(testBlocker)); |
||||
|
||||
auto taskFunction = [&]() { |
||||
taskExecuted = true; |
||||
scheduler.stop(); // prevent infinite loop in threadFn
|
||||
}; |
||||
|
||||
// When
|
||||
scheduler.schedule(taskFunction, 0, 0, 0, TaskScheduler::RunMode::OnStart); |
||||
scheduler.start(); |
||||
threadFn(); |
||||
|
||||
// Then
|
||||
REQUIRE(taskExecuted); |
||||
scheduler.stop(); |
||||
} |
||||
|
||||
SECTION( |
||||
"when task is scheduled with Once mode then it executes at specified time") |
||||
{ |
||||
// Given
|
||||
auto threadHandle = std::make_unique<test::MockThreadHandle>(); |
||||
bool taskExecuted = false; |
||||
std::function<void()> threadFn; |
||||
auto currentTime = test::TIMEPOINT_NOW; // current "now", starts at 12:00
|
||||
std::chrono::seconds timeDelta{5}; |
||||
std::chrono::milliseconds actualDelay{0}; |
||||
|
||||
auto initialTime = test::TIMEPOINT_NOW; |
||||
auto expectedExecutionTime = initialTime + timeDelta; |
||||
|
||||
// Set up thread handle expectations before moving it
|
||||
ALLOW_CALL(*threadHandle, join()); |
||||
ALLOW_CALL(*threadHandle, joinable()).RETURN(true); |
||||
|
||||
// Recreate blocker for this test
|
||||
auto testBlocker = std::make_unique<test::MockBlocker>(); |
||||
|
||||
// Expect createThread to be called, save thread function
|
||||
REQUIRE_CALL(*threadMgr, createThread(_)) |
||||
.LR_RETURN(std::move(threadHandle)) |
||||
.LR_SIDE_EFFECT(threadFn = std::move(_1)); |
||||
|
||||
// Mock time provider calls - return initial time first, then execution time
|
||||
ALLOW_CALL(*timeProvider, now()).LR_RETURN(currentTime); |
||||
|
||||
// Allow blocker calls, save delay value
|
||||
ALLOW_CALL(*testBlocker, blockFor(_)) |
||||
.LR_SIDE_EFFECT(actualDelay += _1; currentTime += _1 // let the time flow
|
||||
); |
||||
ALLOW_CALL(*testBlocker, notify()); |
||||
|
||||
TaskScheduler scheduler(logger, *timeProvider, *threadMgr, |
||||
std::move(testBlocker)); |
||||
|
||||
auto taskFunction = [&]() { |
||||
taskExecuted = true; |
||||
scheduler.stop(); // prevent infinite loop in threadFn
|
||||
}; |
||||
|
||||
// When
|
||||
scheduler.schedule(taskFunction, 12, 0, timeDelta.count(), |
||||
TaskScheduler::RunMode::Once); |
||||
scheduler.start(); |
||||
|
||||
// Execute the thread function to simulate the scheduler thread
|
||||
threadFn(); |
||||
|
||||
// Then
|
||||
REQUIRE(taskExecuted); |
||||
REQUIRE(actualDelay == timeDelta); |
||||
} |
||||
|
||||
SECTION("when task is scheduled with Forever and OnStart mode then it " |
||||
"executes repeatedly") |
||||
{ |
||||
// Given
|
||||
auto threadHandle = std::make_unique<test::MockThreadHandle>(); |
||||
std::function<void()> threadFn; |
||||
int executionCount = 0; |
||||
auto currentTime = test::TIMEPOINT_NOW; |
||||
|
||||
// Set up thread handle expectations before moving it
|
||||
ALLOW_CALL(*threadHandle, join()); |
||||
ALLOW_CALL(*threadHandle, joinable()).RETURN(true); |
||||
|
||||
// Recreate blocker for this test
|
||||
auto testBlocker = std::make_unique<test::MockBlocker>(); |
||||
|
||||
// Expect createThread to be called, save thread function
|
||||
REQUIRE_CALL(*threadMgr, createThread(_)) |
||||
.LR_RETURN(std::move(threadHandle)) |
||||
.LR_SIDE_EFFECT(threadFn = std::move(_1)); |
||||
|
||||
// Mock time provider calls
|
||||
ALLOW_CALL(*timeProvider, now()).LR_RETURN(currentTime); |
||||
|
||||
// Allow blocker calls and simulate time passage
|
||||
ALLOW_CALL(*testBlocker, blockFor(_)).LR_SIDE_EFFECT(currentTime += _1); |
||||
ALLOW_CALL(*testBlocker, notify()); |
||||
|
||||
TaskScheduler scheduler(logger, *timeProvider, *threadMgr, |
||||
std::move(testBlocker)); |
||||
|
||||
auto taskFunction = [&]() { |
||||
executionCount++; |
||||
if (executionCount >= 3) { |
||||
scheduler.stop(); // stop after 3 executions
|
||||
} |
||||
}; |
||||
|
||||
// When
|
||||
scheduler.schedule(taskFunction, 0, 0, 0, |
||||
TaskScheduler::RunMode::Forever |
||||
| TaskScheduler::RunMode::OnStart); |
||||
scheduler.start(); |
||||
|
||||
// Execute the thread function to simulate the scheduler thread
|
||||
threadFn(); |
||||
|
||||
// Then
|
||||
REQUIRE(executionCount >= 3); |
||||
} |
||||
|
||||
SECTION("when invalid time parameters are provided then exception is thrown") |
||||
{ |
||||
// Given - recreate blocker for this test
|
||||
auto testBlocker = std::make_unique<test::MockBlocker>(); |
||||
|
||||
TaskScheduler scheduler(logger, *timeProvider, *threadMgr, |
||||
std::move(testBlocker)); |
||||
|
||||
// When & Then - invalid hour
|
||||
REQUIRE_THROWS_AS( |
||||
scheduler.schedule([]() {}, -1, 0, 0, TaskScheduler::RunMode::Once), |
||||
std::invalid_argument); |
||||
REQUIRE_THROWS_AS( |
||||
scheduler.schedule([]() {}, 24, 0, 0, TaskScheduler::RunMode::Once), |
||||
std::invalid_argument); |
||||
|
||||
// When & Then - invalid minute
|
||||
REQUIRE_THROWS_AS( |
||||
scheduler.schedule([]() {}, 0, -1, 0, TaskScheduler::RunMode::Once), |
||||
std::invalid_argument); |
||||
REQUIRE_THROWS_AS( |
||||
scheduler.schedule([]() {}, 0, 60, 0, TaskScheduler::RunMode::Once), |
||||
std::invalid_argument); |
||||
|
||||
// When & Then - invalid second
|
||||
REQUIRE_THROWS_AS( |
||||
scheduler.schedule([]() {}, 0, 0, -1, TaskScheduler::RunMode::Once), |
||||
std::invalid_argument); |
||||
REQUIRE_THROWS_AS( |
||||
scheduler.schedule([]() {}, 0, 0, 61, TaskScheduler::RunMode::Once), |
||||
std::invalid_argument); |
||||
} |
||||
|
||||
SECTION("when invalid mode combination is used then exception is thrown") |
||||
{ |
||||
// Given - recreate blocker for this test
|
||||
auto testBlocker = std::make_unique<test::MockBlocker>(); |
||||
|
||||
TaskScheduler scheduler(logger, *timeProvider, *threadMgr, |
||||
std::move(testBlocker)); |
||||
|
||||
// When & Then
|
||||
REQUIRE_THROWS_AS(scheduler.schedule([]() {}, 0, 0, 0, |
||||
TaskScheduler::RunMode::Forever |
||||
| TaskScheduler::RunMode::Once), |
||||
std::invalid_argument); |
||||
} |
||||
|
||||
SECTION("when multiple tasks are scheduled then all execute") |
||||
{ |
||||
// Given
|
||||
auto threadHandle = std::make_unique<test::MockThreadHandle>(); |
||||
std::function<void()> threadFn; |
||||
bool task1Executed = false; |
||||
bool task2Executed = false; |
||||
|
||||
// Set up thread handle expectations before moving it
|
||||
ALLOW_CALL(*threadHandle, join()); |
||||
ALLOW_CALL(*threadHandle, joinable()).RETURN(true); |
||||
|
||||
// Recreate blocker for this test
|
||||
auto testBlocker = std::make_unique<test::MockBlocker>(); |
||||
|
||||
// Expect createThread to be called, save thread function
|
||||
REQUIRE_CALL(*threadMgr, createThread(_)) |
||||
.LR_RETURN(std::move(threadHandle)) |
||||
.LR_SIDE_EFFECT(threadFn = std::move(_1)); |
||||
|
||||
// Mock time provider calls
|
||||
ALLOW_CALL(*timeProvider, now()).LR_RETURN(test::TIMEPOINT_NOW); |
||||
|
||||
// Allow blocker calls
|
||||
ALLOW_CALL(*testBlocker, blockFor(_)); |
||||
ALLOW_CALL(*testBlocker, notify()); |
||||
|
||||
TaskScheduler scheduler(logger, *timeProvider, *threadMgr, |
||||
std::move(testBlocker)); |
||||
|
||||
auto taskFunction1 = [&]() { task1Executed = true; }; |
||||
|
||||
auto taskFunction2 = [&]() { |
||||
task2Executed = true; |
||||
scheduler.stop(); // stop after both tasks have had a chance to execute
|
||||
}; |
||||
|
||||
// When
|
||||
scheduler.schedule(taskFunction1, 0, 0, 0, TaskScheduler::RunMode::OnStart); |
||||
scheduler.schedule(taskFunction2, 0, 0, 0, TaskScheduler::RunMode::OnStart); |
||||
scheduler.start(); |
||||
|
||||
// Execute the thread function to simulate the scheduler thread
|
||||
threadFn(); |
||||
|
||||
// Then
|
||||
REQUIRE(task1Executed); |
||||
REQUIRE(task2Executed); |
||||
} |
||||
|
||||
SECTION("when task is scheduled with Forever mode then it repeats") |
||||
{ |
||||
// Given
|
||||
auto threadHandle = std::make_unique<test::MockThreadHandle>(); |
||||
std::function<void()> threadFn; |
||||
int executionCount = 0; |
||||
auto currentTime = test::TIMEPOINT_NOW; |
||||
|
||||
// Set up thread handle expectations before moving it
|
||||
ALLOW_CALL(*threadHandle, join()); |
||||
ALLOW_CALL(*threadHandle, joinable()).RETURN(true); |
||||
|
||||
// Recreate blocker for this test
|
||||
auto testBlocker = std::make_unique<test::MockBlocker>(); |
||||
|
||||
// Expect createThread to be called, save thread function
|
||||
REQUIRE_CALL(*threadMgr, createThread(_)) |
||||
.LR_RETURN(std::move(threadHandle)) |
||||
.LR_SIDE_EFFECT(threadFn = std::move(_1)); |
||||
|
||||
// Mock time provider calls - simulate time advancing
|
||||
ALLOW_CALL(*timeProvider, now()).LR_RETURN(currentTime); |
||||
|
||||
// Allow blocker calls and simulate time passage
|
||||
ALLOW_CALL(*testBlocker, blockFor(_)).LR_SIDE_EFFECT(currentTime += _1); |
||||
ALLOW_CALL(*testBlocker, notify()); |
||||
|
||||
TaskScheduler scheduler(logger, *timeProvider, *threadMgr, |
||||
std::move(testBlocker)); |
||||
|
||||
auto taskFunction = [&]() { |
||||
executionCount++; |
||||
if (executionCount >= 2) { |
||||
scheduler.stop(); // stop after 2 executions
|
||||
} |
||||
}; |
||||
|
||||
// Schedule task to run at a specific time (not immediately) and repeat
|
||||
// forever This ensures the task doesn't get stuck in an infinite OnStart
|
||||
// loop
|
||||
scheduler.schedule(taskFunction, 12, 0, 1, TaskScheduler::RunMode::Forever); |
||||
|
||||
// When
|
||||
scheduler.start(); |
||||
|
||||
// Execute the thread function to simulate the scheduler thread
|
||||
threadFn(); |
||||
|
||||
// Then
|
||||
REQUIRE(executionCount >= 2); |
||||
} |
||||
|
||||
SECTION("when task is scheduled with Forever and OnStart mode then it " |
||||
"executes on start and at scheduled time only") |
||||
{ |
||||
// Given
|
||||
auto threadHandle = std::make_unique<test::MockThreadHandle>(); |
||||
std::function<void()> threadFn; |
||||
std::vector<std::chrono::system_clock::time_point> executionTimes; |
||||
auto currentTime = test::TIMEPOINT_NOW; // 2020-01-01 12:00:00
|
||||
|
||||
// Schedule task for 12:00:05 (5 seconds after current time)
|
||||
auto scheduledTimeDelta = std::chrono::seconds{5}; |
||||
auto scheduledTime = currentTime + scheduledTimeDelta; |
||||
|
||||
// Set up thread handle expectations before moving it
|
||||
ALLOW_CALL(*threadHandle, join()); |
||||
ALLOW_CALL(*threadHandle, joinable()).RETURN(true); |
||||
|
||||
// Recreate blocker for this test
|
||||
auto testBlocker = std::make_unique<test::MockBlocker>(); |
||||
|
||||
// Expect createThread to be called, save thread function
|
||||
REQUIRE_CALL(*threadMgr, createThread(_)) |
||||
.LR_RETURN(std::move(threadHandle)) |
||||
.LR_SIDE_EFFECT(threadFn = std::move(_1)); |
||||
// Mock time provider calls - simulate time advancing
|
||||
ALLOW_CALL(*timeProvider, now()).LR_RETURN(currentTime); |
||||
|
||||
// Also add a timeout mechanism in case the scheduler doesn't execute as
|
||||
// expected
|
||||
auto timeoutTime = test::TIMEPOINT_NOW + std::chrono::minutes(2); |
||||
|
||||
// Allow blocker calls and simulate time passage
|
||||
ALLOW_CALL(*testBlocker, blockFor(_)) |
||||
.LR_SIDE_EFFECT( |
||||
// Advance time by the blocked amount
|
||||
currentTime += _1;); |
||||
ALLOW_CALL(*testBlocker, notify()); |
||||
|
||||
TaskScheduler scheduler(logger, *timeProvider, *threadMgr, |
||||
std::move(testBlocker)); |
||||
|
||||
auto taskFunction = [&]() { |
||||
// Record the current time when this execution happens
|
||||
executionTimes.push_back(currentTime); |
||||
|
||||
// Stop after 2 executions (the expected behavior)
|
||||
if (executionTimes.size() >= 2) { |
||||
scheduler.stop(); |
||||
} |
||||
}; |
||||
|
||||
// When - schedule task with both Forever and OnStart modes
|
||||
// Set time to 12:00:05 (5 seconds after our test TIMEPOINT_NOW)
|
||||
scheduler.schedule(taskFunction, 12, 0, 5, |
||||
TaskScheduler::RunMode::Forever |
||||
| TaskScheduler::RunMode::OnStart); |
||||
scheduler.start(); |
||||
|
||||
// Execute the thread function to simulate the scheduler thread
|
||||
threadFn(); |
||||
|
||||
// Then - task should have executed exactly twice:
|
||||
// 1. Once immediately due to OnStart (at TIMEPOINT_NOW)
|
||||
// 2. Once at the scheduled time (12:00:05)
|
||||
// But NOT more than that (which would indicate the bug)
|
||||
|
||||
// With the bug, executionTimes will have many entries due to infinite loop
|
||||
// Without the bug, we should have exactly 2 entries
|
||||
REQUIRE(executionTimes.size() == 2); |
||||
|
||||
// First execution should be at the initial time (OnStart)
|
||||
REQUIRE(executionTimes[0] == test::TIMEPOINT_NOW); |
||||
|
||||
// Second execution should be at or after the scheduled time
|
||||
REQUIRE(executionTimes[1] >= scheduledTime); |
||||
|
||||
// Verify that time has advanced appropriately
|
||||
// Current time should be at or after the scheduled time
|
||||
REQUIRE(currentTime >= scheduledTime); |
||||
} |
||||
} |
||||
@ -0,0 +1,14 @@
|
||||
{ |
||||
"name": "autostore", |
||||
"version-string": "1.0.0", |
||||
"dependencies": [ |
||||
"cpp-httplib", |
||||
"nlohmann-json", |
||||
"jwt-cpp", |
||||
"spdlog", |
||||
"picosha2", |
||||
"catch2", |
||||
"trompeloeil" |
||||
], |
||||
"builtin-baseline": "ef7dbf94b9198bc58f45951adcf1f041fcbc5ea0" |
||||
} |
||||
@ -0,0 +1,31 @@
|
||||
FROM golang:1.25.1-alpine3.22 |
||||
|
||||
WORKDIR /usr/src/app |
||||
|
||||
# Install system dependencies |
||||
RUN apk add --no-cache \ |
||||
git \ |
||||
bash \ |
||||
curl \ |
||||
sudo |
||||
|
||||
# Configure user and group IDs (default: 1000:1000) |
||||
ARG USER_ID=1000 |
||||
ARG GROUP_ID=1000 |
||||
|
||||
# Create a group and user with specific UID/GID |
||||
RUN addgroup -g ${GROUP_ID} developer \ |
||||
&& adduser -D -u ${USER_ID} -G developer -s /bin/bash developer \ |
||||
&& echo "developer ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/developer \ |
||||
&& chmod 0440 /etc/sudoers.d/developer |
||||
|
||||
RUN chown -R ${USER_ID}:${GROUP_ID} /usr/src/app |
||||
|
||||
USER developer |
||||
|
||||
# Install Go tools |
||||
RUN go install github.com/go-delve/delve/cmd/dlv@latest |
||||
|
||||
EXPOSE 3000 |
||||
|
||||
CMD ["go", "run", "main.go"] |
||||
@ -0,0 +1,25 @@
|
||||
{ |
||||
"name": "Go dev container", |
||||
"dockerComposeFile": "./docker-compose.yml", |
||||
"service": "app", |
||||
"workspaceFolder": "/usr/src/app", |
||||
"customizations": { |
||||
"vscode": { |
||||
"settings": { |
||||
"terminal.integrated.defaultProfile.linux": "bash", |
||||
"go.useLanguageServer": true, |
||||
"go.gopath": "/go", |
||||
"go.goroot": "/usr/local/go" |
||||
}, |
||||
"extensions": [ |
||||
"golang.go", |
||||
"ms-vscode.go-tools", |
||||
"ms-vscode.vscode-go", |
||||
"ms-vscode.vscode-docker" |
||||
] |
||||
} |
||||
}, |
||||
"forwardPorts": [3000], |
||||
"remoteUser": "developer", |
||||
"postCreateCommand": "sudo chown -R developer:1000 /usr/src/app && go mod tidy" |
||||
} |
||||
@ -0,0 +1,29 @@
|
||||
version: "3.9" |
||||
services: |
||||
app: |
||||
build: |
||||
context: .. |
||||
dockerfile: .devcontainer/Dockerfile |
||||
args: |
||||
USER_ID: ${USER_ID:-1000} |
||||
GROUP_ID: ${GROUP_ID:-1000} |
||||
image: dev-golang-img |
||||
container_name: dev-golang |
||||
user: "developer" |
||||
volumes: |
||||
- ../:/usr/src/app:cached |
||||
- golang_modules:/go/pkg/mod |
||||
environment: |
||||
NODE_ENV: development |
||||
ports: |
||||
- "50080:3000" |
||||
networks: |
||||
- dev-network |
||||
command: sleep infinity |
||||
|
||||
volumes: |
||||
golang_modules: |
||||
|
||||
networks: |
||||
dev-network: |
||||
driver: bridge |
||||
@ -0,0 +1,183 @@
|
||||
# Specification Pattern Implementation |
||||
|
||||
The Specification pattern allows you to encapsulate business logic for filtering and querying data. Instead of writing SQL queries directly, you build specifications using Go code that can later be converted to SQL, used for in-memory filtering, or other purposes. |
||||
|
||||
## Core Components |
||||
|
||||
### 1. Basic Data Structures (`condition_spec.go`) |
||||
|
||||
#### `Condition` |
||||
Represents a single comparison operation: |
||||
```go |
||||
type Condition struct { |
||||
Field string // Field name (e.g., "age", "name") |
||||
Operator string // Comparison operator (e.g., "=", ">", "IN") |
||||
Value interface{} // Value to compare against |
||||
} |
||||
``` |
||||
|
||||
#### `LogicalGroup` |
||||
Combines multiple conditions with logical operators: |
||||
```go |
||||
type LogicalGroup struct { |
||||
Operator string // "AND", "OR", or "NOT" |
||||
Conditions []Condition // Simple conditions |
||||
Spec *Spec // Nested specification for complex logic |
||||
} |
||||
``` |
||||
|
||||
#### `Spec` |
||||
The main specification container (can hold either a single condition or a logical group): |
||||
```go |
||||
type Spec struct { |
||||
Condition *Condition // For simple conditions |
||||
LogicalGroup *LogicalGroup // For complex logic |
||||
} |
||||
``` |
||||
|
||||
### 2. Builder Functions |
||||
|
||||
These functions create specifications in a fluent, readable way: |
||||
|
||||
```go |
||||
// Simple conditions |
||||
userSpec := Eq("name", "John") // name = "John" |
||||
ageSpec := Gt("age", 18) // age > 18 |
||||
roleSpec := In("role", []interface{}{"admin", "moderator"}) // role IN ("admin", "moderator") |
||||
|
||||
// Logical combinations |
||||
complexSpec := And( |
||||
Eq("status", "active"), |
||||
Or( |
||||
Gt("age", 21), |
||||
Eq("role", "admin"), |
||||
), |
||||
) |
||||
``` |
||||
|
||||
### 3. Specification Interface (`simple_specification.go`) |
||||
|
||||
The `Specification[T]` interface provides methods for: |
||||
- **Evaluation**: `IsSatisfiedBy(candidate T) bool` |
||||
- **Composition**: `And()`, `Or()`, `Not()` |
||||
- **Introspection**: `GetConditions()`, `GetSpec()` |
||||
|
||||
## How It Works |
||||
|
||||
### 1. Building Specifications |
||||
|
||||
```go |
||||
// Create a specification for active users over 18 |
||||
spec := And( |
||||
Eq("status", "active"), |
||||
Gt("age", 18), |
||||
) |
||||
``` |
||||
|
||||
### 2. Evaluating Specifications |
||||
|
||||
The `SimpleSpecification` uses reflection to evaluate conditions against Go objects: |
||||
|
||||
```go |
||||
user := &User{Name: "John", Age: 25, Status: "active"} |
||||
specification := NewSimpleSpecification[*User](spec) |
||||
isMatch := specification.IsSatisfiedBy(user) // true |
||||
``` |
||||
|
||||
### 3. Field Access |
||||
|
||||
The implementation looks for field values in this order: |
||||
1. `GetFieldName()` method (e.g., `GetAge()`) |
||||
2. `FieldName()` method (e.g., `Age()`) |
||||
3. Exported struct field |
||||
|
||||
### 4. Type Comparisons |
||||
|
||||
The system handles different data types intelligently: |
||||
- **Numbers**: Direct comparison with type conversion |
||||
- **Strings**: Lexicographic comparison |
||||
- **Time**: Special handling for `time.Time` and objects with `Time()` methods |
||||
- **Collections**: `IN` and `NOT IN` operations |
||||
- **Nil values**: Proper null handling |
||||
|
||||
## Examples |
||||
|
||||
### Simple Usage |
||||
```go |
||||
// Find all active users |
||||
activeSpec := Eq("status", "active") |
||||
spec := NewSimpleSpecification[*User](activeSpec) |
||||
|
||||
users := []User{{Status: "active"}, {Status: "inactive"}} |
||||
for _, user := range users { |
||||
if spec.IsSatisfiedBy(&user) { |
||||
fmt.Println("Active user:", user.Name) |
||||
} |
||||
} |
||||
``` |
||||
|
||||
### Complex Logic |
||||
```go |
||||
// Find admin users OR users with high scores |
||||
complexSpec := Or( |
||||
Eq("role", "admin"), |
||||
And( |
||||
Gt("score", 90), |
||||
Eq("status", "active"), |
||||
), |
||||
) |
||||
``` |
||||
|
||||
### Time Comparisons |
||||
```go |
||||
// Find expired items |
||||
expiredSpec := Lte("expirationDate", time.Now()) |
||||
``` |
||||
|
||||
## Key Benefits |
||||
|
||||
1. **Type Safety**: Compile-time checking with generics |
||||
2. **Readability**: Business logic expressed in readable Go code |
||||
3. **Reusability**: Specifications can be composed and reused |
||||
4. **Testability**: Easy to test business rules in isolation |
||||
5. **Flexibility**: Can be converted to SQL, used for filtering, etc. |
||||
|
||||
## Performance Considerations |
||||
|
||||
- Reflection is used for field access (consider caching for high-frequency operations) |
||||
- Complex nested specifications may impact performance |
||||
- Time comparisons handle multiple formats but may be slower than direct comparisons |
||||
|
||||
## Common Patterns |
||||
|
||||
### Repository Integration |
||||
```go |
||||
type UserRepository interface { |
||||
FindWhere(ctx context.Context, spec Specification[*User]) ([]*User, error) |
||||
} |
||||
``` |
||||
|
||||
### Business Rule Encapsulation |
||||
```go |
||||
func ActiveUserSpec() *Spec { |
||||
return And( |
||||
Eq("status", "active"), |
||||
Neq("deletedAt", nil), |
||||
) |
||||
} |
||||
``` |
||||
|
||||
### Dynamic Query Building |
||||
```go |
||||
func BuildUserSearchSpec(filters UserFilters) *Spec { |
||||
conditions := []*Spec{} |
||||
|
||||
if filters.Status != "" { |
||||
conditions = append(conditions, Eq("status", filters.Status)) |
||||
} |
||||
if filters.MinAge > 0 { |
||||
conditions = append(conditions, Gte("age", filters.MinAge)) |
||||
} |
||||
|
||||
return And(conditions...) |
||||
} |
||||
@ -0,0 +1,94 @@
|
||||
package main |
||||
|
||||
import ( |
||||
"context" |
||||
"log" |
||||
"net/http" |
||||
"os" |
||||
"os/signal" |
||||
"syscall" |
||||
"time" |
||||
|
||||
"autostore/internal/application/interfaces" |
||||
"autostore/internal/config" |
||||
"autostore/internal/container" |
||||
) |
||||
|
||||
func main() { |
||||
// Load configuration
|
||||
cfg, err := config.Load() |
||||
if err != nil { |
||||
log.Fatalf("Failed to load configuration: %v", err) |
||||
} |
||||
|
||||
// Validate configuration
|
||||
if err := cfg.Validate(); err != nil { |
||||
log.Fatalf("Invalid configuration: %v", err) |
||||
} |
||||
|
||||
// Create dependency injection container
|
||||
container := container.NewContainer(cfg) |
||||
if err := container.Initialize(); err != nil { |
||||
log.Fatalf("Failed to initialize container: %v", err) |
||||
} |
||||
|
||||
// Get server and scheduler from container
|
||||
server := container.GetServer() |
||||
scheduler := container.GetExpiredItemsScheduler() |
||||
logger := container.GetLogger() |
||||
|
||||
// Setup graceful shutdown
|
||||
shutdownComplete := make(chan struct{}) |
||||
go setupGracefulShutdown(server, scheduler, logger, shutdownComplete) |
||||
|
||||
// Start scheduler
|
||||
if err := scheduler.Start(context.Background()); err != nil { |
||||
logger.Error(context.Background(), "Failed to start scheduler", "error", err) |
||||
log.Fatalf("Failed to start scheduler: %v", err) |
||||
} |
||||
|
||||
// Start server
|
||||
if err := server.Start(); err != nil { |
||||
if err == http.ErrServerClosed { |
||||
// This is expected during graceful shutdown
|
||||
logger.Info(context.Background(), "Server shutdown complete") |
||||
} else { |
||||
logger.Error(context.Background(), "Server failed to start", "error", err) |
||||
log.Fatalf("Server failed to start: %v", err) |
||||
} |
||||
} |
||||
|
||||
// Wait for graceful shutdown to complete
|
||||
<-shutdownComplete |
||||
logger.Info(context.Background(), "Application exiting gracefully") |
||||
} |
||||
|
||||
func setupGracefulShutdown(server interface { |
||||
Shutdown(ctx context.Context) error |
||||
}, scheduler interface { |
||||
Stop() error |
||||
}, logger interfaces.ILogger, shutdownComplete chan struct{}) { |
||||
sigChan := make(chan os.Signal, 1) |
||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) |
||||
|
||||
sig := <-sigChan |
||||
logger.Info(context.Background(), "Received shutdown signal", "signal", sig) |
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) |
||||
defer cancel() |
||||
|
||||
if err := scheduler.Stop(); err != nil { |
||||
logger.Error(ctx, "Scheduler shutdown failed", "error", err) |
||||
} else { |
||||
logger.Info(ctx, "Scheduler shutdown completed gracefully") |
||||
} |
||||
|
||||
if err := server.Shutdown(ctx); err != nil { |
||||
logger.Error(ctx, "Server shutdown failed", "error", err) |
||||
} else { |
||||
logger.Info(ctx, "Server shutdown completed gracefully") |
||||
} |
||||
|
||||
// Signal that shutdown is complete
|
||||
close(shutdownComplete) |
||||
} |
||||
@ -0,0 +1,4 @@
|
||||
.devcontainer |
||||
.git |
||||
.gitignore |
||||
README.md |
||||
@ -0,0 +1,33 @@
|
||||
FROM golang:1.21-alpine AS builder |
||||
|
||||
WORKDIR /app |
||||
|
||||
COPY go.mod go.sum ./ |
||||
|
||||
# Download all dependencies |
||||
RUN go mod download |
||||
|
||||
COPY . . |
||||
|
||||
# Generate go.sum |
||||
RUN go mod tidy |
||||
|
||||
# Run tests |
||||
RUN go test ./tests/unit -v && \ |
||||
go test ./tests/integration -v |
||||
|
||||
# Build the application |
||||
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main ./cmd |
||||
|
||||
# Runtime stage |
||||
FROM alpine:latest |
||||
|
||||
RUN apk --no-cache add ca-certificates |
||||
|
||||
WORKDIR /root/ |
||||
|
||||
COPY --from=builder /app/main . |
||||
|
||||
EXPOSE 3000 |
||||
|
||||
CMD ["./main"] |
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue