Compare commits
12 Commits
master
...
cpp17-init
| Author | SHA1 | Date |
|---|---|---|
|
|
fc955dd1bb | 5 months ago |
|
|
a6d835d48f | 5 months ago |
|
|
15da327b15 | 5 months ago |
|
|
efc44bd9fa | 5 months ago |
|
|
3a63b9dfcf | 5 months ago |
|
|
74955aa3f9 | 5 months ago |
|
|
e150513ea9 | 5 months ago |
|
|
d3745718a2 | 5 months ago |
|
|
bf0634ae52 | 5 months ago |
|
|
a7c13a1cca | 5 months ago |
|
|
46cd5958b5 | 5 months ago |
|
|
1f0d185a14 | 5 months ago |
288 changed files with 1529 additions and 35716 deletions
@ -1,254 +0,0 @@ |
|||||||
--- |
|
||||||
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 |
|
||||||
... |
|
||||||
@ -1,44 +1,111 @@ |
|||||||
# Ovierview |
# About this Repository |
||||||
|
|
||||||
Read top-level `README.md` for more information on this repository. |
This repository hosts multiple implementations of the same back-end application. The aim is to provide quick, side-by-side comparisons of different technologies (languages, frameworks, libraries) while preserving consistent business logic across all implementations. |
||||||
|
|
||||||
# Authentication |
Following principles such as **SOLID** and maintainable architectural patterns (**Clean, Hexagonal, Onion, or even DDD**) is recommended to clearly showcase the strengths and idioms of each technology. |
||||||
|
|
||||||
No external service is used. JWT tokens are created and verified using `jwt-cpp` library. |
Some over-engineering is acceptable to demonstrate architectural features, but please keep implementations readable and avoid excessive complexity (e.g., skip event sourcing or strict CQRS unless intentionally focusing on those patterns for comparison). |
||||||
Default, pre-defined user databse is a simple json file (`app/defaults/users.json`). |
|
||||||
|
|
||||||
# Build and Run |
--- |
||||||
|
|
||||||
```bash |
### Project Idea: AutoStore |
||||||
cd docker |
|
||||||
docker compose build |
|
||||||
docker compose up |
|
||||||
``` |
|
||||||
|
|
||||||
Note: do not use this for development. See `.devcontainer` directory for development setup. |
A system to store items with expiration dates. When items expire, new ones are automatically ordered by making a POST request to the configured order URL. |
||||||
|
|
||||||
For non-container development, see Dockerfile and replicate the steps. Simple `build-and-test.sh` |
#### Business Rules (Domain) |
||||||
script would look like this: |
|
||||||
|
|
||||||
```bash |
1. **Each item has a name and an expiration date.** |
||||||
#/bin/bash |
2. **Expired items are automatically removed from the store.** |
||||||
|
3. **When an item expires, a new item of the same type is automatically ordered.** |
||||||
|
4. **Expired items can be added to the store, triggering immediate ordering.** |
||||||
|
5. **Every item belongs to a user.** |
||||||
|
|
||||||
|
#### Application Requirements |
||||||
|
|
||||||
|
1. **Users can register and log in to obtain a JWT.** |
||||||
|
2. **Authenticated users manage their personal collection of items via an HTTP API.** |
||||||
|
3. **Each item has an associated "order URL".** |
||||||
|
4. **When an item expires, the system must notify the "order URL" with an HTTP POST request.** |
||||||
|
5. **This call should occur immediately when the item's expiration date is reached, or when an expired item is added.** |
||||||
|
6. **Upon startup, the system must verify expiration dates for all items.** |
||||||
|
7. **Persistent storage must be used (file, database, etc.).** |
||||||
|
|
||||||
OUT_DIR=./out |
--- |
||||||
DEBUG_DIR=$OUT_DIR/build/debug |
|
||||||
|
|
||||||
cmake -DCMAKE_BUILD_TYPE=Debug \ |
## Layer Boundaries |
||||||
-DCMAKE_EXPORT_COMPILE_COMMANDS=TRUE \ |
|
||||||
-DCMAKE_TOOLCHAIN_FILE=/opt/vcpkg/scripts/buildsystems/vcpkg.cmake \ |
|
||||||
-S /workspace -B $DEBUG_DIR |
|
||||||
|
|
||||||
cd "$DEBUG_DIR" |
| Layer | Responsibility | Internal Dependencies | External Dependencies | |
||||||
cmake --build . -- -j8 |
|------------------|--------------------------------------------------------------- |----------------------|-----------------------| |
||||||
ctest --output-on-failure . |
| **Domain** | Entities, value objects, domain services (pure business logic) | None | None (language only) | |
||||||
|
| **Application** | Use cases, orchestration, DTOs, infrastructure interfaces | Domain | None or minimal | |
||||||
|
| **Infrastructure**| Implementations (repositories, HTTP, auth), background jobs | Application | Any (framework/lib) | |
||||||
|
| **Presentation** | API controllers, DTOs, auth middleware | Application | UI/web/CLI/others | |
||||||
|
| **Assembly** | Main app, DI, startup logic, job scheduling | Any layer | DI container, config, framework, etc.| |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
### Possible directory layout (will vary from tech to tech) |
||||||
|
|
||||||
|
```plaintext |
||||||
|
AutoStore/ |
||||||
|
├── App |
||||||
|
│ ├── Main |
||||||
|
│ ├── AppConfig |
||||||
|
│ └── ... |
||||||
|
├── Extern |
||||||
|
│ ├── <jwt-lib, http-client, etc.> |
||||||
|
│ └── <...downloaded libraries and git submodules> |
||||||
|
├── Src |
||||||
|
│ ├── Domain/ |
||||||
|
│ │ ├── Entities/ |
||||||
|
│ │ │ ├── User |
||||||
|
│ │ │ └── Item |
||||||
|
│ │ └── Services/ |
||||||
|
│ │ └── ExpirationPolicy |
||||||
|
│ ├── Application/ |
||||||
|
│ │ ├── UseCases/ |
||||||
|
│ │ │ ├── RegisterUser |
||||||
|
│ │ │ ├── LoginUser |
||||||
|
│ │ │ ├── AddItem |
||||||
|
│ │ │ ├── GetItem |
||||||
|
│ │ │ ├── DeleteItem |
||||||
|
│ │ │ └── HandleExpiredItems |
||||||
|
│ │ ├── Interfaces/ |
||||||
|
│ │ │ ├── IUserRepository |
||||||
|
│ │ │ ├── IItemRepository |
||||||
|
│ │ │ ├── IAuthService |
||||||
|
│ │ │ └── IClock |
||||||
|
│ │ ├── Dto/ |
||||||
|
│ │ └── Services/ |
||||||
|
│ ├── Infrastructure/ |
||||||
|
│ │ ├── Repositories/ |
||||||
|
│ │ │ ├── FileUserRepository |
||||||
|
│ │ │ └── FileItemRepository |
||||||
|
│ │ ├── Adapters/ |
||||||
|
│ │ │ ├── JwtAuthAdapter |
||||||
|
│ │ │ ├── OrderUrlHttpClient |
||||||
|
│ │ │ ├── SystemClockImpl |
||||||
|
│ │ │ └── <... some extern lib adapters> |
||||||
|
│ │ └── Helpers/ |
||||||
|
│ │ └── <... DRY helpers> |
||||||
|
│ └── WebApi/ |
||||||
|
│ ├── Controllers/ |
||||||
|
│ │ ├── StoreController |
||||||
|
│ │ └── UserController |
||||||
|
│ └── Auth/ |
||||||
|
│ └── JwtMiddleware |
||||||
|
└── Tests |
||||||
|
├── Unit/ |
||||||
|
└── Integration/ |
||||||
``` |
``` |
||||||
|
|
||||||
# Testing |
## Build and Run |
||||||
|
|
||||||
|
Ideally, each implementation should include a `<impl>/docker/docker-compose.yml` file so that you can simply run: |
||||||
|
|
||||||
Unit tests are added to ctest and executed on docker build. Execute `ctest .` in build dir to run it. |
```bash |
||||||
|
docker compose up |
||||||
|
``` |
||||||
|
to build and run the application. |
||||||
|
|
||||||
Use top-level testing/tavern scripts to run functional tests. |
Otherwise, please provide a `<impl>/README.md` file with setup and running instructions. |
||||||
|
|||||||
@ -0,0 +1,55 @@ |
|||||||
|
# C++17 AutoStore Implementation Plan |
||||||
|
|
||||||
|
This document outlines the steps to implement the C++17 version of the AutoStore application. Implemented classes should use `nxl::` namespace prefix. |
||||||
|
|
||||||
|
## Phase 1: Project Scaffolding & Build System |
||||||
|
|
||||||
|
- [x] Initialize a CMake project structure. |
||||||
|
- [x] Set up the root `CMakeLists.txt` to manage the `app` and `lib` subdirectories. |
||||||
|
- [x] Create the `lib` directory for the static library. |
||||||
|
- [x] Create the `app` directory for the executable. |
||||||
|
- [x] Configure `vcpkg` for dependency management and integrate it with CMake. |
||||||
|
- [x] Add a dependency for an HTTP library (e.g., `cpp-httplib`) via `vcpkg`. |
||||||
|
- [x] Add a dependency for a testing framework (e.g., `catch2`) via `vcpkg`. |
||||||
|
|
||||||
|
## Phase 2: Library (`lib`) - Dummy Implementation |
||||||
|
|
||||||
|
- [x] Create the directory structure for the library: `lib/src`, `lib/include`. |
||||||
|
- [x] Create `lib/CMakeLists.txt` to build a static library. |
||||||
|
- [x] In `lib/include/autostore`, define the public interface for the `App` to use. |
||||||
|
- [x] Create a dummy `AutoStore` class in `lib/include/autostore/AutoStore.h` and a source file in `lib/src/AutoStore.cpp`. |
||||||
|
- [ ] Define initial classes for core domain and application logic inside the library (e.g., `ItemRepository`, `UserService`, etc.) to establish the architecture. These will be mostly private to the library initially and implemented later. |
||||||
|
- [ ] Ensure the project compiles and links successfully with the dummy implementations. |
||||||
|
|
||||||
|
## Phase 3: Application (`app`) - Dummy Implementation |
||||||
|
|
||||||
|
- [ ] Create the directory structure for the application: `app/src`. |
||||||
|
- [x] Create `app/CMakeLists.txt` to build the executable. |
||||||
|
- [ ] Link the `app` against the `lib` static library. |
||||||
|
- [ ] Implement the main `App` class in `app/src/App.h` and `app/src/App.cpp`. |
||||||
|
- [ ] The `App` class will have a constructor `App(int argc, char** argv)` and an `exec()` method. |
||||||
|
- [ ] Implement signal handling (for `SIGINT`, `SIGTERM`) in the `App` class for graceful shutdown. |
||||||
|
- [x] In `app/src/Main.cpp`, instantiate and run the `App` class. |
||||||
|
- [ ] Ensure the project compiles and links successfully with the dummy implementations. |
||||||
|
|
||||||
|
## Phase 4: Core Logic Implementation |
||||||
|
|
||||||
|
- [ ] Implement the Domain layer in `lib/src/domain`. |
||||||
|
- [ ] Implement the Application layer in `lib/src/application`. |
||||||
|
- [ ] Implement the Infrastructure layer in `lib/src/infrastructure` (e.g., file-based persistence, HTTP client for ordering). |
||||||
|
- [ ] Implement the Presentation layer (HTTP API) using the chosen HTTP library. |
||||||
|
- [ ] Implement the startup logic to check for expired items. |
||||||
|
- [ ] Implement a background mechanism (e.g., a thread) to periodically check for expired items. |
||||||
|
|
||||||
|
## Phase 5: Testing |
||||||
|
|
||||||
|
- [ ] Set up a `tests` directory. |
||||||
|
- [ ] Create `tests/CMakeLists.txt` to build the test runner. |
||||||
|
- [ ] Write unit tests for the Domain layer. |
||||||
|
- [ ] Write unit tests for the Application layer, using mocks for infrastructure interfaces. |
||||||
|
- [ ] Write integration tests for the Infrastructure layer. |
||||||
|
|
||||||
|
## Phase 6: Containerization |
||||||
|
|
||||||
|
- [x] Create a `Dockerfile` to build the C++ application in a container. |
||||||
|
- [x] Create a `docker-compose.yml` file to easily build and run the application. |
||||||
@ -1,13 +0,0 @@ |
|||||||
[ |
|
||||||
{ |
|
||||||
"username": "admin", |
|
||||||
"password": "8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918", |
|
||||||
"id": "1000" |
|
||||||
}, |
|
||||||
{ |
|
||||||
"username": "user", |
|
||||||
"password": "04f8996da763b7a969b1028ee3007569eaf3a635486ddab211d512c85b9df8fb", |
|
||||||
"id": "1001" |
|
||||||
} |
|
||||||
] |
|
||||||
|
|
||||||
@ -1,48 +0,0 @@ |
|||||||
#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 |
|
||||||
@ -1,49 +0,0 @@ |
|||||||
#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; |
|
||||||
}; |
|
||||||
@ -1,82 +0,0 @@ |
|||||||
# 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,25 @@ |
|||||||
|
# General request sequence with authentication |
||||||
|
|
||||||
|
```mermaid |
||||||
|
sequenceDiagram |
||||||
|
participant Client as HTTP Client |
||||||
|
participant Router as Request Router |
||||||
|
participant Auth as JwtMiddleware |
||||||
|
participant Controller as Controller |
||||||
|
participant UseCase as Use Case |
||||||
|
|
||||||
|
Client->>Router: POST /api/items (with JWT) |
||||||
|
Router->>Auth: Forward request |
||||||
|
|
||||||
|
alt Authentication successful |
||||||
|
Auth->>Auth: Validate JWT |
||||||
|
Auth->>Controller: Forward authenticated request |
||||||
|
Controller->>Controller: Parse request body to DTO |
||||||
|
Controller->>UseCase: execute() |
||||||
|
UseCase-->>Controller: Result (success/error) |
||||||
|
Controller->>Controller: Convert result to HTTP response |
||||||
|
Controller-->>Client: HTTP Response (2xx) |
||||||
|
else Authentication fails |
||||||
|
Auth-->>Client: 401 Unauthorized |
||||||
|
end |
||||||
|
``` |
||||||
@ -1,28 +1,13 @@ |
|||||||
FROM kuyoh/vcpkg:2025.06.13-ubuntu24.04 AS builder |
FROM kuyoh/vcpkg:2025.06.13-ubuntu24.04 AS base |
||||||
|
|
||||||
WORKDIR /workspace |
WORKDIR /workspace |
||||||
|
|
||||||
COPY ../CMakeLists.txt . |
|
||||||
COPY ../vcpkg.json . |
|
||||||
|
|
||||||
RUN vcpkg install |
|
||||||
|
|
||||||
# Cche stays valid if only code changes |
|
||||||
COPY .. . |
COPY .. . |
||||||
|
|
||||||
|
# generate and build |
||||||
RUN cmake -DCMAKE_TOOLCHAIN_FILE:STRING=${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake \ |
RUN cmake -DCMAKE_TOOLCHAIN_FILE:STRING=${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake \ |
||||||
-DCMAKE_EXPORT_COMPILE_COMMANDS:BOOL=TRUE -DCMAKE_BUILD_TYPE:STRING=Release \ |
-DCMAKE_EXPORT_COMPILE_COMMANDS:BOOL=TRUE -DCMAKE_BUILD_TYPE:STRING=Release \ |
||||||
-H/workspace -B/workspace/build -G Ninja |
-H/workspace -B/workspace/build -G Ninja |
||||||
RUN cmake --build /workspace/build --config Release --target all -j 8 -- |
RUN cmake --build /workspace/build --config Release --target all -j 6 -- |
||||||
|
|
||||||
# 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"] |
CMD ["/workspace/build/bin/AutoStore"] |
||||||
|
|||||||
@ -1,98 +0,0 @@ |
|||||||
#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
|
|
||||||
@ -1,132 +1,97 @@ |
|||||||
#include "AutoStore.h" |
#include "AutoStore.h" |
||||||
|
#include "DiContainer.h" |
||||||
#include "infrastructure/repositories/FileItemRepository.h" |
#include "infrastructure/repositories/FileItemRepository.h" |
||||||
#include "infrastructure/adapters/SystemTimeProvider.h" |
#include "infrastructure/adapters/SystemClock.h" |
||||||
#include "infrastructure/http/HttpOrderService.h" |
#include "infrastructure/http/HttpOrderService.h" |
||||||
#include "infrastructure/auth/FileJwtAuthService.h" |
|
||||||
#include "webapi/controllers/StoreController.h" |
#include "webapi/controllers/StoreController.h" |
||||||
#include "webapi/controllers/AuthController.h" |
|
||||||
#include "infrastructure/http/HttpServer.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 <iostream> |
||||||
#include <filesystem> |
#include <filesystem> |
||||||
#include <memory> |
#include <memory> |
||||||
|
|
||||||
namespace nxl::autostore { |
namespace nxl::autostore { |
||||||
|
|
||||||
using namespace infrastructure; |
AutoStore::AutoStore(std::string_view dataPath, int port, std::string_view host) |
||||||
using namespace application; |
: dataPath(dataPath), port(port), host(host), initialized(false), |
||||||
|
serverRunning(false) |
||||||
AutoStore::AutoStore(Config config, ILoggerPtr logger) |
|
||||||
: config{std::move(config)}, log{std::move(logger)} |
|
||||||
{} |
{} |
||||||
|
|
||||||
AutoStore::~AutoStore() |
AutoStore::~AutoStore() |
||||||
{ |
{ |
||||||
if (httpServer && httpServer->isRunning()) { |
if (serverRunning) { |
||||||
stop(); |
stop(); |
||||||
} |
} |
||||||
} |
} |
||||||
|
|
||||||
bool AutoStore::initialize() |
bool AutoStore::initialize() |
||||||
{ |
{ |
||||||
try { |
std::cout << "Initializing AutoStore with data path: " << dataPath |
||||||
std::filesystem::create_directories(config.dataPath); |
<< ", host: " << host << ", port: " << port << std::endl; |
||||||
|
|
||||||
// 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
|
try { |
||||||
storeController = std::make_unique<webapi::StoreController>( |
std::filesystem::create_directories(dataPath); |
||||||
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); |
diContainer = std::make_unique<di::DiContainer>(); |
||||||
log->info("AutoStore initialized successfully, handling expired items..."); |
storeController = std::make_unique<webapi::StoreController>(*diContainer); |
||||||
|
|
||||||
|
initialized = true; |
||||||
|
std::cout << "AutoStore initialized successfully" << std::endl; |
||||||
return true; |
return true; |
||||||
} catch (const std::exception& e) { |
} catch (const std::exception& e) { |
||||||
log->error("Failed to initialize AutoStore: %s", e.what()); |
std::cerr << "Failed to initialize AutoStore: " << e.what() << std::endl; |
||||||
return false; |
return false; |
||||||
} |
} |
||||||
} |
} |
||||||
|
|
||||||
bool AutoStore::start() |
bool AutoStore::start() |
||||||
{ |
{ |
||||||
log->info("Starting AutoStore services..."); |
if (!initialized) { |
||||||
|
std::cerr << "AutoStore not initialized. Call initialize() first." |
||||||
|
<< std::endl; |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
std::cout << "Starting AutoStore services..." << std::endl; |
||||||
|
|
||||||
try { |
try { |
||||||
taskScheduler->schedule( |
auto& httpServer = diContainer->resolveRef<infrastructure::HttpServer>(); |
||||||
[this]() { |
storeController->registerRoutes(httpServer.getServer()); |
||||||
application::HandleExpiredItems{*itemRepository, *clock, *orderService} |
|
||||||
.execute(); |
if (!httpServer.start(port, host)) { |
||||||
}, |
std::cerr << "Failed to start HTTP server" << std::endl; |
||||||
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; |
return false; |
||||||
} |
} |
||||||
|
|
||||||
log->info("AutoStore services started successfully"); |
serverRunning = true; |
||||||
log->info("HTTP server listening on http://%s:%d", config.host, |
std::cout << "AutoStore services started successfully" << std::endl; |
||||||
config.port); |
std::cout << "HTTP server listening on http://" << host << ":" << port |
||||||
log->info("API endpoint: POST http://%s:%d/api/v1", config.host, |
<< std::endl; |
||||||
config.port); |
std::cout << "API endpoint: POST http://" << host << ":" << port |
||||||
|
<< "/api/items" << std::endl; |
||||||
|
|
||||||
return true; |
return true; |
||||||
} catch (const std::exception& e) { |
} catch (const std::exception& e) { |
||||||
log->error("Failed to start AutoStore services: %s", e.what()); |
std::cerr << "Failed to start AutoStore services: " << e.what() |
||||||
|
<< std::endl; |
||||||
return false; |
return false; |
||||||
} |
} |
||||||
} |
} |
||||||
|
|
||||||
void AutoStore::stop() |
void AutoStore::stop() |
||||||
{ |
{ |
||||||
log->info("Stopping AutoStore services..."); |
if (!serverRunning) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
if (httpServer && httpServer->isRunning()) { |
std::cout << "Stopping AutoStore services..." << std::endl; |
||||||
httpServer->stop(); |
|
||||||
|
if (diContainer) { |
||||||
|
auto& httpServer = diContainer->resolveRef<infrastructure::HttpServer>(); |
||||||
|
httpServer.stop(); |
||||||
} |
} |
||||||
log->info("AutoStore services stopped"); |
|
||||||
|
serverRunning = false; |
||||||
|
std::cout << "AutoStore services stopped" << std::endl; |
||||||
} |
} |
||||||
|
|
||||||
} // namespace nxl::autostore
|
} // namespace nxl::autostore
|
||||||
@ -0,0 +1,45 @@ |
|||||||
|
#include "DiContainer.h" |
||||||
|
#include "infrastructure/repositories/FileItemRepository.h" |
||||||
|
#include "infrastructure/adapters/SystemClock.h" |
||||||
|
#include "infrastructure/http/HttpOrderService.h" |
||||||
|
#include "infrastructure/http/HttpServer.h" |
||||||
|
#include "application/commands/AddItem.h" |
||||||
|
#include "webapi/controllers/StoreController.h" |
||||||
|
#include <filesystem> |
||||||
|
|
||||||
|
namespace nxl::autostore::di { |
||||||
|
|
||||||
|
DiContainer::DiContainer() |
||||||
|
{ |
||||||
|
registerDependencies(); |
||||||
|
} |
||||||
|
|
||||||
|
void DiContainer::registerDependencies() |
||||||
|
{ |
||||||
|
// Register shared references
|
||||||
|
|
||||||
|
container.register_type<dingo::scope<dingo::shared>, |
||||||
|
dingo::storage<infrastructure::FileItemRepository>, |
||||||
|
dingo::interface<application::IItemRepository>>(); |
||||||
|
|
||||||
|
container.register_type<dingo::scope<dingo::shared>, |
||||||
|
dingo::storage<infrastructure::SystemClock>, |
||||||
|
dingo::interface<application::IClock>>(); |
||||||
|
|
||||||
|
container.register_type<dingo::scope<dingo::shared>, |
||||||
|
dingo::storage<infrastructure::HttpOrderService>, |
||||||
|
dingo::interface<application::IOrderService>>(); |
||||||
|
|
||||||
|
container.register_type<dingo::scope<dingo::shared>, |
||||||
|
dingo::storage<infrastructure::HttpServer>>(); |
||||||
|
|
||||||
|
container.register_indexed_type<dingo::scope<dingo::shared>, |
||||||
|
dingo::storage<application::AddItem>, |
||||||
|
dingo::interface<application::AddItem>>( |
||||||
|
std::string("AddItem")); |
||||||
|
|
||||||
|
// test:
|
||||||
|
auto uc = container.resolve<application::AddItem>( |
||||||
|
std::string("AddItem")); // throws on start
|
||||||
|
} |
||||||
|
} // namespace nxl::autostore::di
|
||||||
@ -0,0 +1,102 @@ |
|||||||
|
#pragma once |
||||||
|
|
||||||
|
#include <dingo/container.h> |
||||||
|
#include <dingo/factory/constructor.h> |
||||||
|
#include <dingo/storage/external.h> |
||||||
|
#include <dingo/storage/shared.h> |
||||||
|
#include <dingo/index/unordered_map.h> |
||||||
|
|
||||||
|
#include <memory> |
||||||
|
#include <string> |
||||||
|
#include <filesystem> |
||||||
|
#include <tuple> |
||||||
|
|
||||||
|
// Forward declarations
|
||||||
|
namespace nxl::autostore { |
||||||
|
class AutoStore; |
||||||
|
} |
||||||
|
|
||||||
|
namespace nxl::autostore::infrastructure { |
||||||
|
class FileItemRepository; |
||||||
|
class SystemClock; |
||||||
|
class HttpOrderService; |
||||||
|
class HttpServer; |
||||||
|
} // namespace nxl::autostore::infrastructure
|
||||||
|
|
||||||
|
namespace nxl::autostore::application { |
||||||
|
class AddItem; |
||||||
|
} |
||||||
|
|
||||||
|
namespace nxl::autostore::webapi { |
||||||
|
class StoreController; |
||||||
|
} |
||||||
|
|
||||||
|
namespace nxl::autostore::di { |
||||||
|
|
||||||
|
// Declare traits with std::string based index for named resolution
|
||||||
|
struct container_traits : dingo::dynamic_container_traits |
||||||
|
{ |
||||||
|
using index_definition_type = |
||||||
|
std::tuple<std::tuple<std::string, dingo::index_type::unordered_map>>; |
||||||
|
}; |
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Dependency Injection Container for AutoStore application |
||||||
|
* |
||||||
|
* This class wraps the dingo container and provides a simplified interface |
||||||
|
* for registering and resolving dependencies in the AutoStore application. |
||||||
|
*/ |
||||||
|
class DiContainer |
||||||
|
{ |
||||||
|
public: |
||||||
|
/**
|
||||||
|
* @brief Construct a new DiContainer object |
||||||
|
*/ |
||||||
|
DiContainer(); |
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Destroy the DiContainer object |
||||||
|
*/ |
||||||
|
~DiContainer() = default; |
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Register all application dependencies |
||||||
|
* |
||||||
|
* @param dataPath Path to the data directory |
||||||
|
* @param port HTTP server port |
||||||
|
* @param host HTTP server host |
||||||
|
*/ |
||||||
|
void registerDependencies(); |
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Resolve a dependency by type |
||||||
|
* |
||||||
|
* @tparam T Type to resolve |
||||||
|
* @return Instance of the resolved type |
||||||
|
*/ |
||||||
|
template <typename T> T resolve() { return container.resolve<T>(); } |
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Resolve a dependency by type as a shared pointer |
||||||
|
* |
||||||
|
* @tparam T Type to resolve |
||||||
|
* @return Shared pointer to the resolved type |
||||||
|
*/ |
||||||
|
template <typename T> std::shared_ptr<T> resolveShared() |
||||||
|
{ |
||||||
|
return container.resolve<std::shared_ptr<T>>(); |
||||||
|
} |
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Resolve a dependency by type as a reference |
||||||
|
* |
||||||
|
* @tparam T Type to resolve |
||||||
|
* @return Reference to the resolved type |
||||||
|
*/ |
||||||
|
template <typename T> T& resolveRef() { return container.resolve<T&>(); } |
||||||
|
|
||||||
|
private: |
||||||
|
dingo::container<container_traits> container; |
||||||
|
}; |
||||||
|
|
||||||
|
} // namespace nxl::autostore::di
|
||||||
@ -1,19 +0,0 @@ |
|||||||
#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
|
|
||||||
@ -1,21 +0,0 @@ |
|||||||
#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
|
|
||||||
@ -1,29 +0,0 @@ |
|||||||
#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
|
|
||||||
@ -1,30 +0,0 @@ |
|||||||
#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
|
|
||||||
@ -1,20 +0,0 @@ |
|||||||
#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
|
|
||||||
@ -1,20 +0,0 @@ |
|||||||
#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
|
|
||||||
@ -1,16 +0,0 @@ |
|||||||
#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
|
|
||||||
@ -1,20 +0,0 @@ |
|||||||
#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,14 @@ |
|||||||
|
#pragma once |
||||||
|
|
||||||
|
#include <chrono> |
||||||
|
|
||||||
|
namespace nxl::autostore::application { |
||||||
|
|
||||||
|
class IClock |
||||||
|
{ |
||||||
|
public: |
||||||
|
virtual ~IClock() = default; |
||||||
|
virtual std::chrono::system_clock::time_point getCurrentTime() const = 0; |
||||||
|
}; |
||||||
|
|
||||||
|
} // namespace nxl::autostore::application
|
||||||
@ -1,28 +0,0 @@ |
|||||||
#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
|
|
||||||
@ -1,18 +0,0 @@ |
|||||||
#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,23 @@ |
|||||||
|
#pragma once |
||||||
|
|
||||||
|
#include "domain/entities/User.h" |
||||||
|
#include <optional> |
||||||
|
#include <string> |
||||||
|
#include <string_view> |
||||||
|
#include <vector> |
||||||
|
|
||||||
|
namespace nxl::autostore::application { |
||||||
|
|
||||||
|
class IUserRepository |
||||||
|
{ |
||||||
|
public: |
||||||
|
virtual ~IUserRepository() = default; |
||||||
|
virtual void save(const domain::User& user) = 0; |
||||||
|
virtual std::optional<domain::User> findById(std::string_view id) = 0; |
||||||
|
virtual std::optional<domain::User> |
||||||
|
findByUsername(std::string_view username) = 0; |
||||||
|
virtual std::vector<domain::User> findAll() = 0; |
||||||
|
virtual void remove(std::string_view id) = 0; |
||||||
|
}; |
||||||
|
|
||||||
|
} // namespace nxl::autostore::application
|
||||||
@ -0,0 +1,34 @@ |
|||||||
|
#pragma once |
||||||
|
|
||||||
|
#include <functional> |
||||||
|
#include <string> |
||||||
|
|
||||||
|
namespace nxl::autostore::application { |
||||||
|
|
||||||
|
struct OpResult |
||||||
|
{ |
||||||
|
bool success; |
||||||
|
std::string message; |
||||||
|
|
||||||
|
bool operator==(const OpResult& other) const |
||||||
|
{ |
||||||
|
return success == other.success && message == other.message; |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
struct ErrorResult : public OpResult |
||||||
|
{ |
||||||
|
ErrorResult(std::string message) : OpResult({false, message}) {} |
||||||
|
}; |
||||||
|
|
||||||
|
struct SuccessResult : public OpResult |
||||||
|
{ |
||||||
|
SuccessResult(std::string message) : OpResult({true, message}) {} |
||||||
|
}; |
||||||
|
|
||||||
|
using BoolPresenter = std::function<void(bool)>; |
||||||
|
using IntPresenter = std::function<void(int)>; |
||||||
|
using DoublePresenter = std::function<void(double)>; |
||||||
|
using StringPresenter = std::function<void(std::string)>; |
||||||
|
|
||||||
|
} // namespace nxl::autostore::application
|
||||||
@ -0,0 +1,10 @@ |
|||||||
|
#pragma once |
||||||
|
|
||||||
|
#include "domain/entities/Item.h" |
||||||
|
#include <functional> |
||||||
|
|
||||||
|
namespace nxl::autostore::application { |
||||||
|
|
||||||
|
using ItemPresenter = std::function<void(const domain::Item& item)>; |
||||||
|
|
||||||
|
} // namespace nxl::autostore::application
|
||||||
@ -1,21 +0,0 @@ |
|||||||
#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
|
|
||||||
@ -1,23 +0,0 @@ |
|||||||
#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
|
|
||||||
@ -1,14 +0,0 @@ |
|||||||
#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
|
|
||||||
@ -1,21 +0,0 @@ |
|||||||
#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
|
|
||||||
@ -1,229 +0,0 @@ |
|||||||
#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
|
|
||||||
@ -1,85 +0,0 @@ |
|||||||
#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
|
|
||||||
@ -1,141 +0,0 @@ |
|||||||
#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
|
|
||||||
@ -1,198 +0,0 @@ |
|||||||
#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
|
|
||||||
@ -1,31 +1,18 @@ |
|||||||
#pragma once |
#pragma once |
||||||
|
|
||||||
#include "domain/entities/Item.h" |
#include "domain/entities/Item.h" |
||||||
#include "domain/helpers/Specification.h" |
|
||||||
#include <chrono> |
#include <chrono> |
||||||
|
|
||||||
namespace nxl::autostore::domain { |
namespace nxl::autostore::domain { |
||||||
|
|
||||||
using ItemExpirationSpec = std::unique_ptr<nxl::helpers::ISpecificationExpr>; |
|
||||||
|
|
||||||
class ItemExpirationPolicy |
class ItemExpirationPolicy |
||||||
{ |
{ |
||||||
public: |
public: |
||||||
using TimePoint = std::chrono::system_clock::time_point; |
bool isExpired(const Item& item, |
||||||
constexpr static const char* FIELD_EXP_DATE{"expiration_date"}; |
const std::chrono::system_clock::time_point& currentTime) const |
||||||
|
|
||||||
bool isExpired(const Item& item, const TimePoint& currentTime) const |
|
||||||
{ |
{ |
||||||
return item.expirationDate <= currentTime; |
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
|
} // namespace nxl::autostore::domain
|
||||||
@ -1,55 +0,0 @@ |
|||||||
#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
|
|
||||||
@ -1,33 +0,0 @@ |
|||||||
#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,17 @@ |
|||||||
|
#pragma once |
||||||
|
|
||||||
|
#include "application/interfaces/IClock.h" |
||||||
|
#include <chrono> |
||||||
|
|
||||||
|
namespace nxl::autostore::infrastructure { |
||||||
|
|
||||||
|
class SystemClock : public application::IClock |
||||||
|
{ |
||||||
|
public: |
||||||
|
std::chrono::system_clock::time_point getCurrentTime() const override |
||||||
|
{ |
||||||
|
return std::chrono::system_clock::now(); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
} // namespace nxl::autostore::infrastructure
|
||||||
@ -1,43 +0,0 @@ |
|||||||
#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
|
|
||||||
@ -1,27 +0,0 @@ |
|||||||
#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
|
|
||||||
@ -1,22 +0,0 @@ |
|||||||
#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
|
|
||||||
@ -1,15 +0,0 @@ |
|||||||
#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
|
|
||||||
@ -1,96 +0,0 @@ |
|||||||
#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
|
|
||||||
@ -1,31 +0,0 @@ |
|||||||
#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
|
|
||||||
@ -1,37 +0,0 @@ |
|||||||
#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
|
|
||||||
@ -1,20 +0,0 @@ |
|||||||
#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
|
|
||||||
@ -1,364 +1,36 @@ |
|||||||
/**
|
|
||||||
* 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 "HttpOrderService.h" |
||||||
#include "autostore/Version.h" |
|
||||||
#include <httplib.h> |
|
||||||
#include <stdexcept> |
#include <stdexcept> |
||||||
#include <regex> |
#include <iostream> |
||||||
#include <thread> |
|
||||||
#include <queue> |
|
||||||
#include <mutex> |
|
||||||
#include <condition_variable> |
|
||||||
#include <atomic> |
|
||||||
#include <chrono> |
|
||||||
#include <memory> |
|
||||||
#include <unordered_map> |
|
||||||
|
|
||||||
namespace nxl::autostore::infrastructure { |
namespace nxl::autostore::infrastructure { |
||||||
|
|
||||||
namespace { |
HttpOrderService::HttpOrderService(const std::string& baseUrl) |
||||||
|
: baseUrl(baseUrl) |
||||||
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) |
void HttpOrderService::orderItem(const domain::Item& item) |
||||||
{ |
{ |
||||||
if (item.orderUrl.empty()) { |
if (item.orderUrl.empty()) { |
||||||
throw std::runtime_error("Order URL is empty for item: " + item.name); |
throw std::runtime_error("Order URL is empty for item: " + item.name); |
||||||
} |
} |
||||||
|
|
||||||
if (item.orderUrl.find("://") == std::string::npos) { |
std::string payload = |
||||||
throw std::runtime_error("Invalid URL format for item: " + item.name |
R"({"itemName": ")" + item.name + R"(", "itemId": ")" + item.id + "\"}"; |
||||||
+ " (missing protocol)"); |
sendPostRequest(item.orderUrl, payload); |
||||||
} |
} |
||||||
|
|
||||||
|
void HttpOrderService::sendPostRequest(const std::string& url, |
||||||
|
const std::string& payload) |
||||||
|
{ |
||||||
|
// In a real implementation, this would use an HTTP client library
|
||||||
|
// For now, we'll simulate the HTTP call
|
||||||
|
std::cout << "POST request to: " << url << std::endl; |
||||||
|
std::cout << "Payload: " << payload << std::endl; |
||||||
|
|
||||||
std::string payload = createOrderPayload(item); |
// Simulate HTTP error handling
|
||||||
impl->enqueueOrder(item.orderUrl, std::move(payload)); |
if (url.find("error") != std::string::npos) { |
||||||
|
throw std::runtime_error("Failed to send order request to: " + url); |
||||||
|
} |
||||||
} |
} |
||||||
|
|
||||||
} // namespace nxl::autostore::infrastructure
|
} // namespace nxl::autostore::infrastructure
|
||||||
@ -0,0 +1,130 @@ |
|||||||
|
#include "infrastructure/repositories/FileUserRepository.h" |
||||||
|
#include "nlohmann/json.hpp" |
||||||
|
#include <fstream> |
||||||
|
#include <algorithm> |
||||||
|
|
||||||
|
namespace nxl::autostore::infrastructure { |
||||||
|
|
||||||
|
namespace { |
||||||
|
|
||||||
|
// Helper functions for JSON serialization
|
||||||
|
inline void userToJson(nlohmann::json& j, const domain::User& u) |
||||||
|
{ |
||||||
|
j = nlohmann::json{ |
||||||
|
{"id", u.id}, {"username", u.username}, {"passwordHash", u.passwordHash}}; |
||||||
|
} |
||||||
|
|
||||||
|
inline void jsonToUser(const nlohmann::json& j, domain::User& u) |
||||||
|
{ |
||||||
|
j.at("id").get_to(u.id); |
||||||
|
j.at("username").get_to(u.username); |
||||||
|
j.at("passwordHash").get_to(u.passwordHash); |
||||||
|
} |
||||||
|
|
||||||
|
// Helper functions for vector serialization
|
||||||
|
inline nlohmann::json usersToJson(const std::vector<domain::User>& users) |
||||||
|
{ |
||||||
|
nlohmann::json j = nlohmann::json::array(); |
||||||
|
for (const auto& user : users) { |
||||||
|
nlohmann::json userJson; |
||||||
|
userToJson(userJson, user); |
||||||
|
j.push_back(userJson); |
||||||
|
} |
||||||
|
return j; |
||||||
|
} |
||||||
|
|
||||||
|
inline std::vector<domain::User> jsonToUsers(const nlohmann::json& j) |
||||||
|
{ |
||||||
|
std::vector<domain::User> users; |
||||||
|
for (const auto& userJson : j) { |
||||||
|
domain::User user; |
||||||
|
jsonToUser(userJson, user); |
||||||
|
users.push_back(user); |
||||||
|
} |
||||||
|
return users; |
||||||
|
} |
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
FileUserRepository::FileUserRepository(std::string_view dbPath) : dbPath(dbPath) |
||||||
|
{ |
||||||
|
load(); |
||||||
|
} |
||||||
|
|
||||||
|
void FileUserRepository::save(const domain::User& user) |
||||||
|
{ |
||||||
|
std::lock_guard<std::mutex> lock(mtx); |
||||||
|
auto it = |
||||||
|
std::find_if(users.begin(), users.end(), |
||||||
|
[&](const domain::User& u) { return u.id == user.id; }); |
||||||
|
|
||||||
|
if (it != users.end()) { |
||||||
|
*it = user; |
||||||
|
} else { |
||||||
|
users.push_back(user); |
||||||
|
} |
||||||
|
persist(); |
||||||
|
} |
||||||
|
|
||||||
|
std::optional<domain::User> FileUserRepository::findById(std::string_view id) |
||||||
|
{ |
||||||
|
std::lock_guard<std::mutex> lock(mtx); |
||||||
|
auto it = std::find_if(users.begin(), users.end(), |
||||||
|
[&](const domain::User& u) { return u.id == id; }); |
||||||
|
|
||||||
|
if (it != users.end()) { |
||||||
|
return *it; |
||||||
|
} |
||||||
|
return std::nullopt; |
||||||
|
} |
||||||
|
|
||||||
|
std::optional<domain::User> |
||||||
|
FileUserRepository::findByUsername(std::string_view username) |
||||||
|
{ |
||||||
|
std::lock_guard<std::mutex> lock(mtx); |
||||||
|
auto it = |
||||||
|
std::find_if(users.begin(), users.end(), |
||||||
|
[&](const domain::User& u) { return u.username == username; }); |
||||||
|
|
||||||
|
if (it != users.end()) { |
||||||
|
return *it; |
||||||
|
} |
||||||
|
return std::nullopt; |
||||||
|
} |
||||||
|
|
||||||
|
std::vector<domain::User> FileUserRepository::findAll() |
||||||
|
{ |
||||||
|
std::lock_guard<std::mutex> lock(mtx); |
||||||
|
return users; |
||||||
|
} |
||||||
|
|
||||||
|
void FileUserRepository::remove(std::string_view id) |
||||||
|
{ |
||||||
|
std::lock_guard<std::mutex> lock(mtx); |
||||||
|
users.erase(std::remove_if(users.begin(), users.end(), |
||||||
|
[&](const domain::User& u) { return u.id == id; }), |
||||||
|
users.end()); |
||||||
|
persist(); |
||||||
|
} |
||||||
|
|
||||||
|
void FileUserRepository::load() |
||||||
|
{ |
||||||
|
std::lock_guard<std::mutex> lock(mtx); |
||||||
|
std::ifstream file(dbPath); |
||||||
|
if (file.is_open()) { |
||||||
|
nlohmann::json j; |
||||||
|
file >> j; |
||||||
|
users = jsonToUsers(j); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
void FileUserRepository::persist() |
||||||
|
{ |
||||||
|
std::ofstream file(dbPath); |
||||||
|
if (file.is_open()) { |
||||||
|
nlohmann::json j = usersToJson(users); |
||||||
|
file << j.dump(4); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} // namespace nxl::autostore::infrastructure
|
||||||
@ -0,0 +1,30 @@ |
|||||||
|
#pragma once |
||||||
|
|
||||||
|
#include "application/interfaces/IUserRepository.h" |
||||||
|
#include <string> |
||||||
|
#include <vector> |
||||||
|
#include <mutex> |
||||||
|
|
||||||
|
namespace nxl::autostore::infrastructure { |
||||||
|
|
||||||
|
class FileUserRepository : public application::IUserRepository |
||||||
|
{ |
||||||
|
public: |
||||||
|
explicit FileUserRepository(std::string_view dbPath); |
||||||
|
void save(const domain::User& user) override; |
||||||
|
std::optional<domain::User> findById(std::string_view id) override; |
||||||
|
std::optional<domain::User> |
||||||
|
findByUsername(std::string_view username) override; |
||||||
|
std::vector<domain::User> findAll() override; |
||||||
|
void remove(std::string_view id) override; |
||||||
|
|
||||||
|
private: |
||||||
|
void load(); |
||||||
|
void persist(); |
||||||
|
|
||||||
|
std::string dbPath; |
||||||
|
std::vector<domain::User> users; |
||||||
|
std::mutex mtx; |
||||||
|
}; |
||||||
|
|
||||||
|
} // namespace nxl::autostore::infrastructure
|
||||||
@ -1,58 +0,0 @@ |
|||||||
#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
|
|
||||||
@ -1,26 +0,0 @@ |
|||||||
#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
|
|
||||||
@ -1,23 +0,0 @@ |
|||||||
#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
|
|
||||||
@ -1,86 +0,0 @@ |
|||||||
#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
|
|
||||||
@ -1,40 +1,21 @@ |
|||||||
#pragma once |
#pragma once |
||||||
|
|
||||||
#include "webapi/controllers/BaseController.h" |
#include "DiContainer.h" |
||||||
#include "application/commands/AddItem.h" |
#include <httplib.h> // TODO: forward declaration |
||||||
#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 { |
namespace nxl::autostore::webapi { |
||||||
|
|
||||||
class StoreController : public BaseController |
class StoreController |
||||||
{ |
{ |
||||||
public: |
public: |
||||||
struct Context |
StoreController(di::DiContainer& diContainer); |
||||||
{ |
|
||||||
application::AddItem addItemUc; |
|
||||||
application::ListItems listItemsUc; |
|
||||||
application::GetItem getItemUc; |
|
||||||
application::DeleteItem deleteItemUc; |
|
||||||
application::IAuthService& authService; |
|
||||||
}; |
|
||||||
|
|
||||||
StoreController(Context&& context); |
void registerRoutes(httplib::Server& server); |
||||||
|
|
||||||
protected: |
|
||||||
std::vector<RouteConfig> getRoutes() const override; |
|
||||||
|
|
||||||
private: |
private: |
||||||
void addItem(const httplib::Request& req, httplib::Response& res); |
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; |
di::DiContainer& diContainer; |
||||||
}; |
}; |
||||||
|
|
||||||
} // namespace nxl::autostore::webapi
|
} // namespace nxl::autostore::webapi
|
||||||
@ -1,57 +0,0 @@ |
|||||||
#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,312 @@ |
|||||||
|
#include "infrastructure/repositories/FileUserRepository.h" |
||||||
|
#include "domain/entities/User.h" |
||||||
|
#include <catch2/catch_test_macros.hpp> |
||||||
|
#include <catch2/matchers/catch_matchers_string.hpp> |
||||||
|
#include <filesystem> |
||||||
|
#include <fstream> |
||||||
|
#include <optional> |
||||||
|
|
||||||
|
using namespace nxl::autostore; |
||||||
|
using Catch::Matchers::Equals; |
||||||
|
|
||||||
|
namespace Test { |
||||||
|
// Constants for magic strings and numbers
|
||||||
|
constexpr const char* TEST_USER_ID_1 = "user123"; |
||||||
|
constexpr const char* TEST_USER_ID_2 = "user456"; |
||||||
|
constexpr const char* TEST_USERNAME_1 = "testuser"; |
||||||
|
constexpr const char* TEST_USERNAME_2 = "anotheruser"; |
||||||
|
constexpr const char* TEST_PASSWORD_HASH_1 = "hashedpassword123"; |
||||||
|
constexpr const char* TEST_PASSWORD_HASH_2 = "hashedpassword456"; |
||||||
|
constexpr const char* NON_EXISTENT_ID = "nonexistent"; |
||||||
|
constexpr const char* NON_EXISTENT_USERNAME = "nonexistentuser"; |
||||||
|
constexpr const char* TEST_DIR_NAME = "autostore_test"; |
||||||
|
constexpr const char* TEST_DB_FILE_NAME = "test_users.json"; |
||||||
|
|
||||||
|
// Helper function to create a test user with default values
|
||||||
|
domain::User |
||||||
|
createTestUser(const std::string& id = TEST_USER_ID_1, |
||||||
|
const std::string& username = TEST_USERNAME_1, |
||||||
|
const std::string& passwordHash = TEST_PASSWORD_HASH_1) |
||||||
|
{ |
||||||
|
domain::User user; |
||||||
|
user.id = id; |
||||||
|
user.username = username; |
||||||
|
user.passwordHash = passwordHash; |
||||||
|
return user; |
||||||
|
} |
||||||
|
|
||||||
|
// Helper function to create a second test user
|
||||||
|
domain::User createSecondTestUser() |
||||||
|
{ |
||||||
|
return createTestUser(TEST_USER_ID_2, TEST_USERNAME_2, TEST_PASSWORD_HASH_2); |
||||||
|
} |
||||||
|
|
||||||
|
// Helper function to set up test environment
|
||||||
|
std::string setupTestEnvironment() |
||||||
|
{ |
||||||
|
std::filesystem::path testDir = |
||||||
|
std::filesystem::temp_directory_path() / TEST_DIR_NAME; |
||||||
|
std::filesystem::create_directories(testDir); |
||||||
|
std::string testDbPath = (testDir / TEST_DB_FILE_NAME).string(); |
||||||
|
|
||||||
|
// Clean up any existing test file
|
||||||
|
if (std::filesystem::exists(testDbPath)) { |
||||||
|
std::filesystem::remove(testDbPath); |
||||||
|
} |
||||||
|
|
||||||
|
return testDbPath; |
||||||
|
} |
||||||
|
|
||||||
|
// Helper function to clean up test environment
|
||||||
|
void cleanupTestEnvironment() |
||||||
|
{ |
||||||
|
std::filesystem::path testDir = |
||||||
|
std::filesystem::temp_directory_path() / TEST_DIR_NAME; |
||||||
|
if (std::filesystem::exists(testDir)) { |
||||||
|
std::filesystem::remove_all(testDir); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Helper function to verify user properties match expected values
|
||||||
|
void verifyUserProperties(const domain::User& user, |
||||||
|
const std::string& expectedId, |
||||||
|
const std::string& expectedUsername, |
||||||
|
const std::string& expectedPasswordHash) |
||||||
|
{ |
||||||
|
REQUIRE(user.id == expectedId); |
||||||
|
REQUIRE(user.username == expectedUsername); |
||||||
|
REQUIRE(user.passwordHash == expectedPasswordHash); |
||||||
|
} |
||||||
|
|
||||||
|
// Helper function to verify user properties match default test user values
|
||||||
|
void verifyDefaultTestUser(const domain::User& user) |
||||||
|
{ |
||||||
|
verifyUserProperties(user, TEST_USER_ID_1, TEST_USERNAME_1, |
||||||
|
TEST_PASSWORD_HASH_1); |
||||||
|
} |
||||||
|
|
||||||
|
// Helper function to verify user properties match second test user values
|
||||||
|
void verifySecondTestUser(const domain::User& user) |
||||||
|
{ |
||||||
|
verifyUserProperties(user, TEST_USER_ID_2, TEST_USERNAME_2, |
||||||
|
TEST_PASSWORD_HASH_2); |
||||||
|
} |
||||||
|
} // namespace Test
|
||||||
|
|
||||||
|
TEST_CASE("FileUserRepository Integration Tests", |
||||||
|
"[integration][FileUserRepository]") |
||||||
|
{ |
||||||
|
// Setup test environment
|
||||||
|
std::string testDbPath = Test::setupTestEnvironment(); |
||||||
|
|
||||||
|
SECTION("when a new user is saved then it can be found by id") |
||||||
|
{ |
||||||
|
// Given
|
||||||
|
infrastructure::FileUserRepository repository(testDbPath); |
||||||
|
domain::User testUser = Test::createTestUser(); |
||||||
|
|
||||||
|
// When
|
||||||
|
repository.save(testUser); |
||||||
|
|
||||||
|
// Then
|
||||||
|
auto foundUser = repository.findById(Test::TEST_USER_ID_1); |
||||||
|
REQUIRE(foundUser.has_value()); |
||||||
|
Test::verifyDefaultTestUser(*foundUser); |
||||||
|
} |
||||||
|
|
||||||
|
SECTION("when a new user is saved then it can be found by username") |
||||||
|
{ |
||||||
|
// Given
|
||||||
|
infrastructure::FileUserRepository repository(testDbPath); |
||||||
|
domain::User testUser = Test::createTestUser(); |
||||||
|
|
||||||
|
// When
|
||||||
|
repository.save(testUser); |
||||||
|
|
||||||
|
// Then
|
||||||
|
auto foundUser = repository.findByUsername(Test::TEST_USERNAME_1); |
||||||
|
REQUIRE(foundUser.has_value()); |
||||||
|
Test::verifyDefaultTestUser(*foundUser); |
||||||
|
} |
||||||
|
|
||||||
|
SECTION("when multiple users are saved then findAll returns all users") |
||||||
|
{ |
||||||
|
// Given
|
||||||
|
infrastructure::FileUserRepository repository(testDbPath); |
||||||
|
domain::User firstUser = Test::createTestUser(); |
||||||
|
domain::User secondUser = Test::createSecondTestUser(); |
||||||
|
|
||||||
|
// When
|
||||||
|
repository.save(firstUser); |
||||||
|
repository.save(secondUser); |
||||||
|
|
||||||
|
// Then
|
||||||
|
auto allUsers = repository.findAll(); |
||||||
|
REQUIRE(allUsers.size() == 2); |
||||||
|
|
||||||
|
// Verify both users are present (order doesn't matter)
|
||||||
|
bool foundFirst = false; |
||||||
|
bool foundSecond = false; |
||||||
|
|
||||||
|
for (const auto& user : allUsers) { |
||||||
|
if (user.id == Test::TEST_USER_ID_1) { |
||||||
|
Test::verifyDefaultTestUser(user); |
||||||
|
foundFirst = true; |
||||||
|
} else if (user.id == Test::TEST_USER_ID_2) { |
||||||
|
Test::verifySecondTestUser(user); |
||||||
|
foundSecond = true; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
REQUIRE(foundFirst); |
||||||
|
REQUIRE(foundSecond); |
||||||
|
} |
||||||
|
|
||||||
|
SECTION("when an existing user is saved then it is updated") |
||||||
|
{ |
||||||
|
// Given
|
||||||
|
infrastructure::FileUserRepository repository(testDbPath); |
||||||
|
domain::User testUser = Test::createTestUser(); |
||||||
|
repository.save(testUser); |
||||||
|
|
||||||
|
// When
|
||||||
|
testUser.username = "updatedusername"; |
||||||
|
testUser.passwordHash = "updatedpasswordhash"; |
||||||
|
repository.save(testUser); |
||||||
|
|
||||||
|
// Then
|
||||||
|
auto foundUser = repository.findById(Test::TEST_USER_ID_1); |
||||||
|
REQUIRE(foundUser.has_value()); |
||||||
|
REQUIRE(foundUser->id == Test::TEST_USER_ID_1); |
||||||
|
REQUIRE(foundUser->username == "updatedusername"); |
||||||
|
REQUIRE(foundUser->passwordHash == "updatedpasswordhash"); |
||||||
|
} |
||||||
|
|
||||||
|
SECTION("when a user is removed then it cannot be found by id") |
||||||
|
{ |
||||||
|
// Given
|
||||||
|
infrastructure::FileUserRepository repository(testDbPath); |
||||||
|
domain::User testUser = Test::createTestUser(); |
||||||
|
repository.save(testUser); |
||||||
|
|
||||||
|
// When
|
||||||
|
repository.remove(Test::TEST_USER_ID_1); |
||||||
|
|
||||||
|
// Then
|
||||||
|
auto foundUser = repository.findById(Test::TEST_USER_ID_1); |
||||||
|
REQUIRE_FALSE(foundUser.has_value()); |
||||||
|
} |
||||||
|
|
||||||
|
SECTION("when a user is removed then it cannot be found by username") |
||||||
|
{ |
||||||
|
// Given
|
||||||
|
infrastructure::FileUserRepository repository(testDbPath); |
||||||
|
domain::User testUser = Test::createTestUser(); |
||||||
|
repository.save(testUser); |
||||||
|
|
||||||
|
// When
|
||||||
|
repository.remove(Test::TEST_USER_ID_1); |
||||||
|
|
||||||
|
// Then
|
||||||
|
auto foundUser = repository.findByUsername(Test::TEST_USERNAME_1); |
||||||
|
REQUIRE_FALSE(foundUser.has_value()); |
||||||
|
} |
||||||
|
|
||||||
|
SECTION("when a user is removed then it is not in findAll") |
||||||
|
{ |
||||||
|
// Given
|
||||||
|
infrastructure::FileUserRepository repository(testDbPath); |
||||||
|
domain::User firstUser = Test::createTestUser(); |
||||||
|
domain::User secondUser = Test::createSecondTestUser(); |
||||||
|
repository.save(firstUser); |
||||||
|
repository.save(secondUser); |
||||||
|
|
||||||
|
// When
|
||||||
|
repository.remove(Test::TEST_USER_ID_1); |
||||||
|
|
||||||
|
// Then
|
||||||
|
auto allUsers = repository.findAll(); |
||||||
|
REQUIRE(allUsers.size() == 1); |
||||||
|
Test::verifySecondTestUser(allUsers[0]); |
||||||
|
} |
||||||
|
|
||||||
|
SECTION( |
||||||
|
"when findById is called with non-existent id then it returns nullopt") |
||||||
|
{ |
||||||
|
// Given
|
||||||
|
infrastructure::FileUserRepository repository(testDbPath); |
||||||
|
|
||||||
|
// When
|
||||||
|
auto foundUser = repository.findById(Test::NON_EXISTENT_ID); |
||||||
|
|
||||||
|
// Then
|
||||||
|
REQUIRE_FALSE(foundUser.has_value()); |
||||||
|
} |
||||||
|
|
||||||
|
SECTION("when findByUsername is called with non-existent username then it " |
||||||
|
"returns nullopt") |
||||||
|
{ |
||||||
|
// Given
|
||||||
|
infrastructure::FileUserRepository repository(testDbPath); |
||||||
|
|
||||||
|
// When
|
||||||
|
auto foundUser = repository.findByUsername(Test::NON_EXISTENT_USERNAME); |
||||||
|
|
||||||
|
// Then
|
||||||
|
REQUIRE_FALSE(foundUser.has_value()); |
||||||
|
} |
||||||
|
|
||||||
|
SECTION("when remove is called with non-existent id then it does nothing") |
||||||
|
{ |
||||||
|
// Given
|
||||||
|
infrastructure::FileUserRepository repository(testDbPath); |
||||||
|
domain::User testUser = Test::createTestUser(); |
||||||
|
repository.save(testUser); |
||||||
|
|
||||||
|
// When
|
||||||
|
repository.remove(Test::NON_EXISTENT_ID); |
||||||
|
|
||||||
|
// Then
|
||||||
|
auto allUsers = repository.findAll(); |
||||||
|
REQUIRE(allUsers.size() == 1); |
||||||
|
Test::verifyDefaultTestUser(allUsers[0]); |
||||||
|
} |
||||||
|
|
||||||
|
SECTION( |
||||||
|
"when repository is created with existing data file then it loads the data") |
||||||
|
{ |
||||||
|
// Given
|
||||||
|
{ |
||||||
|
infrastructure::FileUserRepository firstRepository(testDbPath); |
||||||
|
domain::User testUser = Test::createTestUser(); |
||||||
|
firstRepository.save(testUser); |
||||||
|
} |
||||||
|
|
||||||
|
// When
|
||||||
|
infrastructure::FileUserRepository secondRepository(testDbPath); |
||||||
|
|
||||||
|
// Then
|
||||||
|
auto foundUser = secondRepository.findById(Test::TEST_USER_ID_1); |
||||||
|
REQUIRE(foundUser.has_value()); |
||||||
|
Test::verifyDefaultTestUser(*foundUser); |
||||||
|
} |
||||||
|
|
||||||
|
SECTION("when repository is created with non-existent data file then it " |
||||||
|
"starts empty") |
||||||
|
{ |
||||||
|
// Given
|
||||||
|
std::filesystem::path testDir = |
||||||
|
std::filesystem::temp_directory_path() / Test::TEST_DIR_NAME; |
||||||
|
std::string nonExistentDbPath = (testDir / "nonexistent.json").string(); |
||||||
|
|
||||||
|
// When
|
||||||
|
infrastructure::FileUserRepository repository(nonExistentDbPath); |
||||||
|
|
||||||
|
// Then
|
||||||
|
auto allUsers = repository.findAll(); |
||||||
|
REQUIRE(allUsers.empty()); |
||||||
|
} |
||||||
|
|
||||||
|
// Clean up test environment
|
||||||
|
Test::cleanupTestEnvironment(); |
||||||
|
} |
||||||
@ -1,19 +0,0 @@ |
|||||||
#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
|
|
||||||
@ -1,24 +0,0 @@ |
|||||||
#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
|
|
||||||
@ -1,14 +0,0 @@ |
|||||||
#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
|
|
||||||
@ -1,24 +0,0 @@ |
|||||||
#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
|
|
||||||
@ -1,16 +0,0 @@ |
|||||||
#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
|
|
||||||
@ -1,51 +0,0 @@ |
|||||||
#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
|
|
||||||
@ -1,223 +0,0 @@ |
|||||||
#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); |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,692 +0,0 @@ |
|||||||
#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')"); |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,436 +0,0 @@ |
|||||||
#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); |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,31 +0,0 @@ |
|||||||
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"] |
|
||||||
@ -1,25 +0,0 @@ |
|||||||
{ |
|
||||||
"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" |
|
||||||
} |
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue