Compare commits

...

16 Commits

  1. 9
      .gitignore
  2. 2
      LICENSE
  3. 154
      README.md
  4. 83
      cpp17/.clang-format
  5. 254
      cpp17/.clang-tidy
  6. 5
      cpp17/.devcontainer/Dockerfile
  7. 20
      cpp17/.devcontainer/devcontainer.json
  8. 12
      cpp17/.devcontainer/docker-compose.yml
  9. 14
      cpp17/CMakeLists.txt
  10. 33
      cpp17/CMakePresets.json
  11. 44
      cpp17/README.md
  12. 36
      cpp17/app/CMakeLists.txt
  13. 13
      cpp17/app/defaults/users.json
  14. 71
      cpp17/app/src/App.cpp
  15. 31
      cpp17/app/src/App.h
  16. 10
      cpp17/app/src/Main.cpp
  17. 48
      cpp17/app/src/OsHelpers.h
  18. 49
      cpp17/app/src/SpdLogger.h
  19. 58
      cpp17/doc/add-item-sequence.md
  20. 82
      cpp17/doc/architecture-overview.md
  21. 4
      cpp17/docker/.dockerignore
  22. 28
      cpp17/docker/Dockerfile
  23. 10
      cpp17/docker/docker-compose.yml
  24. 62
      cpp17/lib/CMakeLists.txt
  25. 67
      cpp17/lib/include/autostore/AutoStore.h
  26. 98
      cpp17/lib/include/autostore/ILogger.h
  27. 132
      cpp17/lib/src/AutoStore.cpp
  28. 21
      cpp17/lib/src/Version.h.in
  29. 27
      cpp17/lib/src/application/commands/AddItem.cpp
  30. 27
      cpp17/lib/src/application/commands/AddItem.h
  31. 19
      cpp17/lib/src/application/commands/DeleteItem.cpp
  32. 21
      cpp17/lib/src/application/commands/DeleteItem.h
  33. 29
      cpp17/lib/src/application/commands/HandleExpiredItems.cpp
  34. 30
      cpp17/lib/src/application/commands/HandleExpiredItems.h
  35. 20
      cpp17/lib/src/application/commands/LoginUser.cpp
  36. 20
      cpp17/lib/src/application/commands/LoginUser.h
  37. 16
      cpp17/lib/src/application/exceptions/AutoStoreExceptions.h
  38. 21
      cpp17/lib/src/application/interfaces/IAuthService.h
  39. 20
      cpp17/lib/src/application/interfaces/IBlocker.h
  40. 27
      cpp17/lib/src/application/interfaces/IItemRepository.h
  41. 14
      cpp17/lib/src/application/interfaces/IOrderService.h
  42. 28
      cpp17/lib/src/application/interfaces/IThreadManager.h
  43. 18
      cpp17/lib/src/application/interfaces/ITimeProvider.h
  44. 21
      cpp17/lib/src/application/queries/GetItem.cpp
  45. 23
      cpp17/lib/src/application/queries/GetItem.h
  46. 14
      cpp17/lib/src/application/queries/ListItems.cpp
  47. 21
      cpp17/lib/src/application/queries/ListItems.h
  48. 229
      cpp17/lib/src/application/services/TaskScheduler.cpp
  49. 85
      cpp17/lib/src/application/services/TaskScheduler.h
  50. 20
      cpp17/lib/src/domain/entities/Item.h
  51. 17
      cpp17/lib/src/domain/entities/User.h
  52. 141
      cpp17/lib/src/domain/helpers/Specification.cpp
  53. 198
      cpp17/lib/src/domain/helpers/Specification.h
  54. 31
      cpp17/lib/src/domain/polices/ItemExpirationPolicy.h
  55. 55
      cpp17/lib/src/infrastructure/adapters/CvBlocker.cpp
  56. 33
      cpp17/lib/src/infrastructure/adapters/CvBlocker.h
  57. 43
      cpp17/lib/src/infrastructure/adapters/SystemThreadManager.cpp
  58. 27
      cpp17/lib/src/infrastructure/adapters/SystemThreadManager.h
  59. 22
      cpp17/lib/src/infrastructure/adapters/SystemTimeProvider.cpp
  60. 15
      cpp17/lib/src/infrastructure/adapters/SystemTimeProvider.h
  61. 96
      cpp17/lib/src/infrastructure/auth/FileJwtAuthService.cpp
  62. 31
      cpp17/lib/src/infrastructure/auth/FileJwtAuthService.h
  63. 32
      cpp17/lib/src/infrastructure/helpers/Jsend.cpp
  64. 20
      cpp17/lib/src/infrastructure/helpers/Jsend.h
  65. 76
      cpp17/lib/src/infrastructure/helpers/JsonItem.cpp
  66. 22
      cpp17/lib/src/infrastructure/helpers/JsonItem.h
  67. 37
      cpp17/lib/src/infrastructure/http/HttpJwtMiddleware.cpp
  68. 20
      cpp17/lib/src/infrastructure/http/HttpJwtMiddleware.h
  69. 364
      cpp17/lib/src/infrastructure/http/HttpOrderService.cpp
  70. 24
      cpp17/lib/src/infrastructure/http/HttpOrderService.h
  71. 123
      cpp17/lib/src/infrastructure/http/HttpServer.cpp
  72. 32
      cpp17/lib/src/infrastructure/http/HttpServer.h
  73. 132
      cpp17/lib/src/infrastructure/repositories/FileItemRepository.cpp
  74. 32
      cpp17/lib/src/infrastructure/repositories/FileItemRepository.h
  75. 58
      cpp17/lib/src/webapi/controllers/AuthController.cpp
  76. 26
      cpp17/lib/src/webapi/controllers/AuthController.h
  77. 23
      cpp17/lib/src/webapi/controllers/BaseController.cpp
  78. 86
      cpp17/lib/src/webapi/controllers/BaseController.h
  79. 128
      cpp17/lib/src/webapi/controllers/StoreController.cpp
  80. 40
      cpp17/lib/src/webapi/controllers/StoreController.h
  81. 38
      cpp17/tests/CMakeLists.txt
  82. 57
      cpp17/tests/helpers/AddItemTestHelpers.h
  83. 392
      cpp17/tests/integration/FileItemRepository.test.cpp
  84. 19
      cpp17/tests/mocks/MockBlocker.h
  85. 24
      cpp17/tests/mocks/MockItemRepository.h
  86. 14
      cpp17/tests/mocks/MockOrderService.h
  87. 24
      cpp17/tests/mocks/MockThreadManager.h
  88. 16
      cpp17/tests/mocks/MockTimeProvider.h
  89. 51
      cpp17/tests/mocks/TestLogger.h
  90. 223
      cpp17/tests/unit/AddItem.test.cpp
  91. 692
      cpp17/tests/unit/Specification.test.cpp
  92. 436
      cpp17/tests/unit/TaskScheduler.test.cpp
  93. 14
      cpp17/vcpkg.json
  94. 31
      golang/.devcontainer/Dockerfile
  95. 25
      golang/.devcontainer/devcontainer.json
  96. 29
      golang/.devcontainer/docker-compose.yml
  97. 183
      golang/SPEC_DETAILS.md
  98. 94
      golang/cmd/main.go
  99. 4
      golang/docker/.dockerignore
  100. 33
      golang/docker/Dockerfile
  101. Some files were not shown because too many files have changed in this diff Show More

9
.gitignore vendored

@ -1,6 +1,10 @@
# ---> C++ # ---> C++
# Prerequisites # Prerequisites
*.d *.d
build
build-*
volumes
tmp
# Compiled Object files # Compiled Object files
*.slo *.slo
@ -17,10 +21,6 @@
*.dylib *.dylib
*.dll *.dll
# Fortran module files
*.mod
*.smod
# Compiled Static libraries # Compiled Static libraries
*.lai *.lai
*.la *.la
@ -236,3 +236,4 @@ gradle-app.setting
hs_err_pid* hs_err_pid*
replay_pid* replay_pid*
reference-*

2
LICENSE

@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) <year> <copyright holders> Copyright (c) 2025 chodak166
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

154
README.md

@ -4,7 +4,7 @@ This repository hosts multiple implementations of the same back-end application.
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. Following principles such as **SOLID** and maintainable architectural patterns (**Clean, Hexagonal, Onion, or even DDD**) is recommended to clearly showcase the strengths and idioms of each technology.
Some over-engineering is acceptable to demonstrate architectural features, but please keep implementations readable and avoid excessive complexity (e.g., skip event sourcing or strict CQRS unless intentionally focusing on those patterns for comparison). Some over-engineering is acceptable to demonstrate architectural features, but please keep implementations readable and avoid excessive complexity (e.g., skip event sourcing or atomic transactions unless intentionally focusing on those patterns for comparison).
--- ---
@ -19,10 +19,11 @@ A system to store items with expiration dates. When items expire, new ones are a
3. **When an item expires, a new item of the same type is automatically ordered.** 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.** 4. **Expired items can be added to the store, triggering immediate ordering.**
5. **Every item belongs to a user.** 5. **Every item belongs to a user.**
6. **Only the item's owner can manage it.**
#### Application Requirements #### Application Requirements
1. **Users can register and log in to obtain a JWT.** 1. **Users can log in to obtain a JWT.**
2. **Authenticated users manage their personal collection of items via an HTTP API.** 2. **Authenticated users manage their personal collection of items via an HTTP API.**
3. **Each item has an associated "order URL".** 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.** 4. **When an item expires, the system must notify the "order URL" with an HTTP POST request.**
@ -30,17 +31,20 @@ A system to store items with expiration dates. When items expire, new ones are a
6. **Upon startup, the system must verify expiration dates for all items.** 6. **Upon startup, the system must verify expiration dates for all items.**
7. **Persistent storage must be used (file, database, etc.).** 7. **Persistent storage must be used (file, database, etc.).**
**Note:** For simplicity, user CRUD is skipped. Integrate with an OP (OpenID Provider) service like Keycloak, Authentic, or Zitadel, or mock authentication with a simple Docker service. Alternatively, simply authenticate a predefined user and return a JWT on login.
--- ---
## Layer Boundaries ## Layer Boundaries
| Layer | Responsibility | Internal Dependencies | External Dependencies | | Layer | Responsibility | Internal Dependencies | External Dependencies |
|------------------|--------------------------------------------------------------- |----------------------|-----------------------| |-------------------|--------------------------------------------------------------- |-----------------------|-----------------------|
| **Domain** | Entities, value objects, domain services (pure business logic) | None | None (language only) | | **Domain** | Entities, value objects, domain services (pure business logic) | None | None (language only) |
| **Application** | Use cases, orchestration, DTOs, infrastructure interfaces | Domain | None or minimal | | **Application** | Use cases, orchestration, DTOs, infrastructure interfaces | Domain | None or minimal |
| **Infrastructure**| Implementations (repositories, HTTP, auth), background jobs | Application | Any (framework/lib) | | **Infrastructure**| Implementations (repositories, HTTP, auth), background jobs | Application | Any (framework/lib) |
| **Presentation** | API controllers, DTOs, auth middleware | Application | UI/web/CLI/others | | **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.| | **Assembly** | Main app, DI, startup logic, job scheduling | Any layer | DI container, config, framework, etc.|
--- ---
@ -48,61 +52,105 @@ A system to store items with expiration dates. When items expire, new ones are a
```plaintext ```plaintext
AutoStore/ AutoStore/
├── App ├── App # app assembly
│ ├── Main │ ├── Main
│ ├── AppConfig │ ├── AppConfig
│ └── ... │ └── ...
├── Extern ├── Extern
│ ├── <jwt-lib, http-client, etc.> │ ├── <package manager files>
│ └── <...downloaded libraries and git submodules> │ └── <...downloaded libraries and git submodules>
└── Src ├── Src # internal/lib/src
├── Domain/ │ ├── Domain/
│ ├── Entities/ │ │ ├── Entities/
│ │ ├── User │ │ │ ├── User
│ │ └── Item │ │ │ └── Item
│ └── Services/ │ │ └── Specifications/
│ └── ExpirationPolicy │ │ └── ItemExpirationSpec # domain knowledge (from domain experts)
├── Application/ │ ├── Application/
│ ├── UseCases/ │ │ ├── Commands/ # use cases
│ │ ├── RegisterUser │ │ │ ├── Login
│ │ ├── LoginUser │ │ │ ├── AddItem
│ │ ├── AddItem │ │ │ ├── DeleteItem
│ │ ├── GetItem │ │ │ └── HandleExpiredItems
│ │ ├── DeleteItem │ │ ├── Queries/ # use cases (read only)
│ │ └── HandleExpiredItems │ │ │ ├── GetItem
│ ├── Interfaces/ │ │ │ └── ListItems
│ │ ├── IUserRepository │ │ ├── Interfaces/
│ │ ├── IItemRepository │ │ │ ├── IUserRepository
│ │ ├── IAuthService │ │ │ ├── IItemRepository
│ │ └── IClock │ │ │ ├── IAuthService
│ ├── Dto/ │ │ │ └── IDateProvider
│ └── Services/ │ │ ├── Dto/ # data transfer objects (fields mappings, validation, etc.)
├── Infrastructure/ │ │ └── Services/
│ ├── Repositories/ │ │ ├── UserInitializationService
│ │ ├── FileUserRepository │ │ └── ExpirationScheduler
│ │ └── FileItemRepository │ ├── Infrastructure/
│ ├── Adapters/ │ │ ├── Repositories/
│ │ ├── JwtAuthAdapter │ │ │ ├── FileUserRepository
│ │ ├── OrderUrlHttpClient │ │ │ └── FileItemRepository
│ │ ├── SystemClockImpl │ │ ├── Adapters/
│ │ └── <... some extern lib adapters> │ │ │ ├── JwtAuthAdapter
│ └── Helpers/ │ │ │ ├── OrderUrlHttpClient
│ └── <... DRY helpers> │ │ │ ├── SystemDateProvider
└── WebApi/ │ │ │ └── <... some extern lib adapters>
├── Controllers/ │ │ └── Helpers/
│ ├── StoreController │ │ └── <... DRY helpers>
│ └── UserController │ ├── Cli # presentation, optional command line use case caller
└── Auth/ │ └── WebApi/ # presentation, REST (controllers, middlewares, etc.)
└── JwtMiddleware │ ├── Controllers/
│ │ ├── StoreController
│ │ └── UserController
│ └── Auth/
│ └── JwtMiddleware
└── Tests
├── Unit/
└── Integration/
``` ```
---
## Domain Knowledge and Repository Queries
Business rules like expiration checks (`expirationDate <= currentDate`) represent domain knowledge that **must have a single source of truth**. This logic might evolve (e.g., to `<= currentDate - N days` or vary by item type) and should never be duplicated across the codebase.
While simple predicates (like `findWhere(predicate<bool>(Item))`) work for in-memory repositories, SQL-based repositories need to translate these rules into efficient WHERE clauses. One solution (not too over-engineered) would be to pass DTO or value object ready to be put into queries (e.g., `fetchExpiredItems(calculatedConditionFields)`).
But for fun and possible UI search, consider implementing a specification pattern with a simple condition abstraction that exposes the business rule as composable conditions (field, operator, value).
This allows domain services to define rules once, use cases to apply them consistently, and repositories to translate them into optimal queries by interpreting the conditions according to their storage mechanism. Avoid duplicating the business logic in repository implementations - instead, let repositories consume the specification and build their queries accordingly. This aims to overcome to-repo-and-back drawbacks depicted in *Evans, Eric (2003). Domain Driven Design. Final Manuscript*.
---
## Build and Run ## Build and Run
Ideally, each implementation should include a `<impl>/docker/docker-compose.yml` file so that you can simply run: Each implementation should include a `<impl>/docker/docker-compose.yml` file so that you can simply run:
```bash ```bash
docker compose up cd docker && docker compose up --build
``` ```
to build and run the application. to build, test and run the application.
Otherwise, please provide a `<impl>/README.md` file with setup and running instructions. Otherwise, please provide a `<impl>/README.md` file with setup and running instructions.
## API Endpoints
See `openapi.yaml` file for suggested API (test it with Tavern, Postman etc.).
Here's a summary of example API endpoints:
| Endpoint | Method | Description |
|-------------------------|--------|--------------------------------------|
| `/login` | POST | Authenticate user and get JWT token |
| `/items` | GET | Get user's items |
| `/items` | POST | Create new item |
| `/items/{id}` | GET | Get item by ID |
| `/items/{id}` | PUT | Update item details |
| `/items/{id}` | DELETE | Delete item |
Suggested base URL is `http://localhost:50080/api/v1/`.
## Testing
- Each implementation should include its own **unit tests** and **integration tests**, which must run automatically during the Docker image build.
- Implementation-independent functional tests are provided in `testing/tavern/`
- Tavern API tests (requests and assertions) must pass for every implementation to ensure consistent behavior across all technology stacks.
- For debugging and verifying the automatic ordering feature, use the helper service in `testing/http-echo-server/` which provides a simple Docker Compose setup that listens on port 8888 and logs all incoming POST requests, allowing you to observe when expired items trigger order notifications.

83
cpp17/.clang-format

@ -0,0 +1,83 @@
---
Language: Cpp
AccessModifierOffset: -2
AlignAfterOpenBracket: Align
AlignConsecutiveAssignments: false
AlignConsecutiveDeclarations: false
AlignEscapedNewlines: Right
AlignOperands: true
AlignTrailingComments: true
AllowAllArgumentsOnNextLine: true
AllowAllConstructorInitializersOnNextLine: true
AllowAllParametersOfDeclarationOnNextLine: true
AllowShortBlocksOnASingleLine: false
AllowShortCaseLabelsOnASingleLine: false
AllowShortFunctionsOnASingleLine: Inline
AllowShortIfStatementsOnASingleLine: false
AllowShortLoopsOnASingleLine: false
AlwaysBreakAfterDefinitionReturnType: None
AlwaysBreakAfterReturnType: None
AlwaysBreakBeforeMultilineStrings: false
AlwaysBreakTemplateDeclarations: MultiLine
BinPackArguments: true
BinPackParameters: true
BreakBeforeBinaryOperators: NonAssignment
BreakBeforeBraces: Custom
BraceWrapping:
AfterClass: true
AfterControlStatement: false
AfterEnum: false
AfterFunction: true
AfterNamespace: false
AfterStruct: true
AfterUnion: false
AfterExternBlock: false
BeforeCatch: false
BeforeElse: false
IndentBraces: false
SplitEmptyFunction: false
SplitEmptyRecord: false
SplitEmptyNamespace: false
BreakBeforeInheritanceComma: false
BreakInheritanceList: BeforeColon
ColumnLimit: 80
CompactNamespaces: false
ConstructorInitializerIndentWidth: 2
ContinuationIndentWidth: 2
Cpp11BracedListStyle: true
DerivePointerAlignment: false
DisableFormat: false
ExperimentalAutoDetectBinPacking: false
FixNamespaceComments: true
IndentCaseLabels: true
IndentPPDirectives: None
IndentWidth: 2
IndentWrappedFunctionNames: false
KeepEmptyLinesAtTheStartOfBlocks: false
# LambdaBodyIndentation: Signature
MaxEmptyLinesToKeep: 1
NamespaceIndentation: None
PointerAlignment: Left
ReflowComments: true
SortIncludes: false
SortUsingDeclarations: false
SpaceAfterCStyleCast: false
SpaceAfterLogicalNot: false
SpaceAfterTemplateKeyword: true
# SpaceAroundPointerQualifiers: Default
SpaceBeforeAssignmentOperators: true
SpaceBeforeCpp11BracedList: false
SpaceBeforeCtorInitializerColon: true
SpaceBeforeInheritanceColon: true
SpaceBeforeParens: ControlStatements
SpaceBeforeRangeBasedForLoopColon: true
SpaceInEmptyParentheses: false
SpacesBeforeTrailingComments: 1
SpacesInAngles: false
SpacesInCStyleCastParentheses: false
SpacesInConditionalStatement: false
SpacesInContainerLiterals: true
SpacesInParentheses: false
SpacesInSquareBrackets: false
TabWidth: 8
UseTab: Never

254
cpp17/.clang-tidy

@ -0,0 +1,254 @@
---
Checks: >-
-*,
bugprone-assert-side-effect,
bugprone-bad-signal-to-kill-thread,
bugprone-bool-pointer-implicit-conversion,
bugprone-branch-clone,
bugprone-copy-constructor-init,
bugprone-dangling-handle,
bugprone-dynamic-static-initializers,
bugprone-exception-escape,
bugprone-forward-declaration-namespace,
bugprone-forwarding-reference-overload,
bugprone-inaccurate-erase,
bugprone-incorrect-roundings,
bugprone-infinite-loop,
bugprone-integer-division,
bugprone-macro-parentheses,
bugprone-misplaced-operator-in-strlen-in-alloc,
bugprone-misplaced-pointer-arithmetic-in-alloc,
bugprone-misplaced-widening-cast,
bugprone-move-forwarding-reference,
bugprone-multiple-statement-macro,
bugprone-not-null-terminated-result,
bugprone-parent-virtual-call,
bugprone-posix-return,
bugprone-signed-char-misuse,
bugprone-sizeof-container,
bugprone-sizeof-expression,
bugprone-spuriously-wake-up-functions,
bugprone-string-constructor,
bugprone-string-integer-assignment,
bugprone-string-literal-with-embedded-nul,
bugprone-suspicious-enum-usage,
bugprone-suspicious-include,
bugprone-suspicious-memset-usage,
bugprone-suspicious-missing-comma,
bugprone-suspicious-semicolon,
bugprone-suspicious-string-compare,
bugprone-swapped-arguments,
bugprone-terminating-continue,
bugprone-throw-keyword-missing,
bugprone-too-small-loop-variable,
bugprone-undefined-memory-manipulation,
bugprone-undelegated-constructor,
bugprone-unhandled-self-assignment,
bugprone-unhandled-self-assignment,
bugprone-unused-raii,
bugprone-use-after-move,
cert-dcl21-cpp,
cert-dcl50-cpp,
cert-dcl58-cpp,
cert-env33-c,
cert-err34-c,
cert-err52-cpp,
cert-err58-cpp,
cert-err60-cpp,
cert-flp30-c,
cert-mem57-cpp,
cert-msc50-cpp,
cert-msc51-cpp,
cert-oop57-cpp,
cert-oop58-cpp,
clang-analyzer-core.CallAndMessage,
clang-analyzer-core.DivideZero,
clang-analyzer-core.*,
clang-analyzer-cplusplus.*,
clang-analyzer-deadcode.*,
clang-analyzer-nullability.*,
clang-analyzer-optin.*,
clang-analyzer-valist.*,
clang-analyzer-security.*,
cppcoreguidelines-avoid-goto,
cppcoreguidelines-avoid-non-const-global-variables,
cppcoreguidelines-init-variables,
cppcoreguidelines-interfaces-global-init,
cppcoreguidelines-macro-usage,
cppcoreguidelines-narrowing-conversions,
cppcoreguidelines-no-malloc,
cppcoreguidelines-owning-memory,
cppcoreguidelines-pro-bounds-array-to-pointer-decay,
cppcoreguidelines-pro-bounds-constant-array-index,
cppcoreguidelines-pro-bounds-pointer-arithmetic,
cppcoreguidelines-pro-type-const-cast,
cppcoreguidelines-pro-type-cstyle-cast,
cppcoreguidelines-pro-type-member-init,
cppcoreguidelines-pro-type-reinterpret-cast,
cppcoreguidelines-pro-type-static-cast-downcast,
cppcoreguidelines-pro-type-union-access,
cppcoreguidelines-pro-type-vararg,
cppcoreguidelines-slicing,
cppcoreguidelines-special-member-functions,
google-build-namespaces,
google-default-arguments,
google-explicit-constructor,
google-build-using-namespace,
google-global-names-in-headers,
google-readability-casting,
google-runtime-int,
google-runtime-operator,
hicpp-exception-baseclass,
hicpp-multiway-paths-covered,
hicpp-no-assembler,
hicpp-signed-bitwise,
llvm-namespace-comment,
misc-definitions-in-headers,
misc-misplaced-const,
misc-new-delete-overloads,
misc-no-recursion,
misc-non-copyable-objects,
misc-non-private-member-variables-in-classes,
misc-redundant-expression,
misc-static-assert,
misc-throw-by-value-catch-by-reference,
misc-unconventional-assign-operator,
misc-uniqueptr-reset-release,
misc-unused-parameters,
misc-unused-using-decls,
misc-unused-alias-decls,
modernize-avoid-bind,
modernize-avoid-c-arrays,
modernize-concat-nested-namespaces,
modernize-deprecated-headers,
modernize-deprecated-ios-base-aliases,
modernize-loop-convert,
modernize-make-shared,
modernize-make-unique,
modernize-raw-string-literal,
modernize-redundant-void-arg,
modernize-replace-auto-ptr,
modernize-replace-disallow-copy-and-assign-macro,
modernize-replace-random-shuffle,
modernize-return-braced-init-list,
modernize-shrink-to-fit,
modernize-unary-static-assert,
modernize-use-auto,
modernize-use-bool-literals,
modernize-use-default-member-init,
modernize-use-emplace,
modernize-use-equals-default,
modernize-use-equals-delete,
modernize-use-nodiscard,
modernize-use-noexcept,
modernize-use-nullptr,
modernize-use-override,
modernize-use-transparent-functors,
modernize-use-uncaught-exceptions,
modernize-use-using,
performance-faster-string-find,
performance-for-range-copy,
performance-implicit-conversion-in-loop,
performance-inefficient-algorithm,
performance-inefficient-string-concatenation,
performance-inefficient-vector-operation,
performance-move-const-arg,
performance-move-constructor-init,
performance-no-automatic-move,
performance-noexcept-move-constructor,
performance-trivially-destructible,
performance-type-promotion-in-math-fn,
performance-unnecessary-copy-initialization,
performance-unnecessary-value-param,
readability-avoid-const-params-in-decls,
readability-braces-around-statements,
readability-const-return-type,
readability-container-size-empty,
readability-delete-null-pointer,
readability-deleted-default,
readability-else-after-return,
readability-function-size,
readability-identifier-naming,
readability-implicit-bool-conversion,
readability-inconsistent-declaration-parameter-name,
readability-isolate-declaration,
readability-magic-numbers,
readability-make-member-function-const,
readability-misleading-indentation,
readability-misplaced-array-index,
readability-named-parameter,
readability-non-const-parameter,
readability-redundant-control-flow,
readability-redundant-declaration,
readability-redundant-function-ptr-dereference,
readability-redundant-member-init,
readability-redundant-preprocessor,
readability-redundant-smartptr-get,
readability-redundant-string-cstr,
readability-redundant-string-init,
readability-simplify-boolean-expr,
readability-simplify-subscript-expr,
readability-static-accessed-through-instance,
readability-static-definition-in-anonymous-namespace,
readability-string-compare,
readability-uniqueptr-delete-release,
readability-uppercase-literal-suffix,
readability-use-anyofallof
WarningsAsErrors: ''
HeaderFilterRegex: '.*'
AnalyzeTemporaryDtors: false
FormatStyle: none
CheckOptions:
- key: cppcoreguidelines-special-member-functions.AllowSoleDefaultDtor
value: 1
- key: modernize-use-nullptr.NullMacros
value: 'NULL'
- key: readability-function-size.LineThreshold
value: 50
- key: readability-function-size.StatementThreshold
value: 800
- key: readability-function-size.BranchThreshold
value: 10
- key: readability-function-size.ParameterThreshold
value: 6
- key: readability-function-size.NestingThreshold
value: 15
- key: readability-function-size.VariableThreshold
value: 10
- key: readability-identifier-naming.ClassCase
value: CamelCase
- key: readability-identifier-naming.MemberCase
value: camelBack
- key: readability-identifier-naming.ClassMemberCase
value: camelBack
- key: readability-identifier-naming.ClassMethodCase
value: camelBack
- key: readability-identifier-naming.MethodCase
value: camelBack
- key: readability-identifier-naming.ConstantCase
value: UPPER_CASE
- key: readability-identifier-naming.LocalConstantCase
value: camelBack
- key: readability-identifier-naming.NamespaceCase
value: lower_case
- key: readability-identifier-naming.ParameterCase
value: camelBack
- key: readability-identifier-naming.EnumCase
value: CamelCase
- key: readability-identifier-naming.EnumConstantCase
value: CamelCase
- key: readability-identifier-naming.FunctionCase
value: camelBack
- key: misc-non-private-member-variables-in-classes.IgnoreClassesWithAllMemberVariablesBeingPublic
value: 1
...

5
cpp17/.devcontainer/Dockerfile

@ -0,0 +1,5 @@
FROM kuyoh/vcpkg:2025.06.13-ubuntu24.04
RUN apt update -y && apt install -y gdb
RUN chown -R 1000:1000 /opt/vcpkg
WORKDIR /workspace
CMD ["bash"]

20
cpp17/.devcontainer/devcontainer.json

@ -0,0 +1,20 @@
{
"name": "AutoStore dev container",
"dockerComposeFile": "./docker-compose.yml",
"service": "app",
"workspaceFolder": "/workspace",
"customizations": {
"vscode": {
"settings": {
"terminal.integrated.defaultProfile.linux": "bash",
"cmake.useCMakePresets": "always"
},
"extensions": [
"ms-vscode.cmake-tools",
"fredericbonnet.cmake-test-adapter",
"twxs.cmake",
"ms-vscode.cpptools-extension-pack"
]
}
}
}

12
cpp17/.devcontainer/docker-compose.yml

@ -0,0 +1,12 @@
version: "3.9"
services:
app:
image: dev-cpp-vcpkg-img
build:
context: ..
dockerfile: .devcontainer/Dockerfile
volumes:
- ../:/workspace:cached
- ./volumes/vscode-server:/home/ubuntu/.vscode-server
command: ["sleep", "infinity"]
user: "1000:1000"

14
cpp17/CMakeLists.txt

@ -0,0 +1,14 @@
cmake_minimum_required(VERSION 3.20)
project(AutoStore VERSION 1.0.0 LANGUAGES CXX)
set(PROJECT_ROOT ${PROJECT_SOURCE_DIR})
set(CTEST_OUTPUT_ON_FAILURE ON)
enable_testing(true)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/bin)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/lib)
add_subdirectory(lib)
add_subdirectory(app)
add_subdirectory(tests)

33
cpp17/CMakePresets.json

@ -0,0 +1,33 @@
{
"version": 3,
"configurePresets": [
{
"name": "debug",
"toolchainFile": "${env:VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"CMAKE_EXPORT_COMPILE_COMMANDS": "TRUE"
}
},
{
"name": "release",
"toolchainFile": "${env:VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
"CMAKE_EXPORT_COMPILE_COMMANDS": "TRUE"
}
}
],
"buildPresets": [
{
"name": "debug",
"configurePreset": "debug",
"jobs": 8
},
{
"name": "release",
"configurePreset": "release",
"jobs": 8
}
]
}

44
cpp17/README.md

@ -0,0 +1,44 @@
# Ovierview
Read top-level `README.md` for more information on this repository.
# Authentication
No external service is used. JWT tokens are created and verified using `jwt-cpp` library.
Default, pre-defined user databse is a simple json file (`app/defaults/users.json`).
# Build and Run
```bash
cd docker
docker compose build
docker compose up
```
Note: do not use this for development. See `.devcontainer` directory for development setup.
For non-container development, see Dockerfile and replicate the steps. Simple `build-and-test.sh`
script would look like this:
```bash
#/bin/bash
OUT_DIR=./out
DEBUG_DIR=$OUT_DIR/build/debug
cmake -DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_EXPORT_COMPILE_COMMANDS=TRUE \
-DCMAKE_TOOLCHAIN_FILE=/opt/vcpkg/scripts/buildsystems/vcpkg.cmake \
-S /workspace -B $DEBUG_DIR
cd "$DEBUG_DIR"
cmake --build . -- -j8
ctest --output-on-failure .
```
# Testing
Unit tests are added to ctest and executed on docker build. Execute `ctest .` in build dir to run it.
Use top-level testing/tavern scripts to run functional tests.

36
cpp17/app/CMakeLists.txt

@ -0,0 +1,36 @@
cmake_minimum_required(VERSION 3.20)
project(AutoStoreApp LANGUAGES CXX)
set(TARGET_NAME AutoStore)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(spdlog CONFIG REQUIRED)
set(SOURCES
src/Main.cpp
src/App.cpp
src/App.h
)
set (LIBRARIES
AutoStoreLib
spdlog::spdlog
)
add_executable(${TARGET_NAME} ${SOURCES})
target_include_directories(${TARGET_NAME}
PRIVATE
${CMAKE_BINARY_DIR}
)
# Create data directory and copy defalut users.json for development
add_custom_command(
TARGET ${TARGET_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/data" && ${CMAKE_COMMAND} -E copy
"${CMAKE_CURRENT_LIST_DIR}/defaults/users.json"
"${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/data/users.json"
)
target_link_libraries(${TARGET_NAME} PRIVATE ${LIBRARIES})

13
cpp17/app/defaults/users.json

@ -0,0 +1,13 @@
[
{
"username": "admin",
"password": "8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918",
"id": "1000"
},
{
"username": "user",
"password": "04f8996da763b7a969b1028ee3007569eaf3a635486ddab211d512c85b9df8fb",
"id": "1001"
}
]

71
cpp17/app/src/App.cpp

@ -0,0 +1,71 @@
#include "App.h"
#include "SpdLogger.h"
#include "OsHelpers.h"
#include <iostream>
#include <filesystem>
namespace nxl {
using nxl::autostore::AutoStore;
std::condition_variable App::exitCv;
std::mutex App::mtx;
bool App::shouldExit = false;
nxl::autostore::ILoggerPtr log{nullptr};
App::App(int argc, char** argv)
{
signal(SIGINT, App::handleSignal);
signal(SIGTERM, App::handleSignal);
auto spdLogger = spdlog::stdout_color_mt("console");
spdLogger->set_pattern("[%Y-%m-%d %H:%M:%S] [%^%l%$] %v");
spdLogger->set_level(spdlog::level::debug);
log = logger = std::make_shared<SpdLogger>(spdLogger, 9);
autoStore = std::make_unique<AutoStore>(
AutoStore::Config{
.dataPath = os::getApplicationDirectory() + "/data",
.host = "0.0.0.0",
.port = 50080,
},
logger);
if (!autoStore->initialize()) {
std::cerr << "Failed to initialize AutoStore" << std::endl;
throw std::runtime_error("Failed to initialize AutoStore");
}
}
App::~App() = default;
int App::exec()
{
if (!autoStore->start()) {
std::cerr << "Failed to start AutoStore services" << std::endl;
return 1;
}
logger->info("AutoStore is running. Press Ctrl+C to stop.");
std::unique_lock<std::mutex> lock(mtx);
exitCv.wait(lock, [] { return shouldExit; });
autoStore->stop();
return 0;
}
void App::handleSignal(int signum)
{
if (log) {
log->info("Caught signal %d. Graceful shutdown.", signum);
}
{
std::lock_guard<std::mutex> lock(mtx);
shouldExit = true;
}
exitCv.notify_one();
}
} // namespace nxl

31
cpp17/app/src/App.h

@ -0,0 +1,31 @@
#pragma once
#include <atomic>
#include <condition_variable>
#include <csignal>
#include <mutex>
#include <thread>
#include <memory>
#include <autostore/AutoStore.h>
#include <autostore/ILogger.h>
namespace nxl {
class App
{
public:
App(int argc, char** argv);
~App();
int exec();
private:
static void handleSignal(int signum);
static std::condition_variable exitCv;
static std::mutex mtx;
static bool shouldExit;
std::unique_ptr<nxl::autostore::AutoStore> autoStore;
autostore::ILoggerPtr logger;
};
} // namespace nxl

10
cpp17/app/src/Main.cpp

@ -0,0 +1,10 @@
#include "App.h"
#include "autostore/Version.h"
#include <iostream>
int main(int argc, char** argv)
{
std::cout << "AutoStore v" << nxl::getVersionString() << std::endl;
nxl::App app(argc, argv);
return app.exec();
}

48
cpp17/app/src/OsHelpers.h

@ -0,0 +1,48 @@
#pragma once
#include <string>
#ifdef _WIN32
#include <windows.h>
#ifndef PATH_MAX
#define PATH_MAX 4096
#endif
namespace nxl::os {
std::string getApplicationDirectory()
{
char exePath[PATH_MAX];
GetModuleFileNameA(NULL, exePath, PATH_MAX);
std::string exeDir = std::string(dirname(exePath));
return exeDir;
}
} // namespace nxl::os
#else
#include <sys/types.h>
#include <unistd.h>
#include <libgen.h>
namespace nxl::os {
std::string getApplicationDirectory()
{
char result[PATH_MAX] = {0};
std::string path;
ssize_t count = readlink("/proc/self/exe", result, PATH_MAX);
if (count != -1) {
result[count] = '\0';
path = dirname(result);
} else {
path = "./";
}
return path;
}
} // namespace nxl::os
#endif

49
cpp17/app/src/SpdLogger.h

@ -0,0 +1,49 @@
#pragma once
#include <autostore/ILogger.h>
#include <spdlog/spdlog.h>
#include <spdlog/sinks/stdout_color_sinks.h>
class SpdLogger : public nxl::autostore::ILogger
{
public:
explicit SpdLogger(std::shared_ptr<spdlog::logger> logger, int8_t vlevel)
: logger{std::move(logger)}, vlevel{vlevel}
{}
protected:
void log(LogLevel level, std::string_view message) override
{
switch (level) {
case LogLevel::Info:
logger->info(message);
break;
case LogLevel::Warning:
logger->warn(message);
break;
case LogLevel::Error:
logger->error(message);
break;
case LogLevel::Debug:
logger->debug(message);
break;
}
}
void vlog(int8_t level, std::string_view message) override
{
if (level > vlevel) {
return;
}
logger->log(spdlog::level::info,
"[V:" + std::to_string(level) + "] " + std::string(message));
}
std::shared_ptr<spdlog::logger> getLogger() const { return logger; }
void setVLevel(int8_t level) { vlevel = level; }
private:
int8_t vlevel{-1};
std::shared_ptr<spdlog::logger> logger;
};

58
cpp17/doc/add-item-sequence.md

@ -0,0 +1,58 @@
# Add item sequence
```mermaid
sequenceDiagram
participant Client as HTTP Client
participant Middleware as HttpJwtMiddleware
participant Server as HttpServer
participant Controller as StoreController
participant AuthService as IAuthService
participant UseCase as AddItem
participant Clock as ITimeProvider
participant Policy as ItemExpirationPolicy
participant OrderService as IOrderService
participant HttpClient as HttpOrderService
participant Repo as IItemRepository
Client->>Server: POST /items with JWT
Server->>Middleware: Validate token
Middleware-->>Server: token valid
Server->>Controller: Forward request
Controller->>AuthService: Extract user ID from token
AuthService-->>Controller: User ID
Controller->>Controller: Parse request body to Item
Controller->>UseCase: execute(item)
UseCase->>Clock: now()
Clock-->>UseCase: current time
UseCase->>Policy: isExpired(item, currentTime)
Policy-->>UseCase: boolean
alt Item is expired
UseCase->>OrderService: orderItem(item)
OrderService->>HttpClient: POST to order URL
HttpClient-->>OrderService: Response
OrderService-->>UseCase: void
end
UseCase->>Repo: save(item)
Repo->>Repo: Persist to file storage
Repo-->>UseCase: Item ID
UseCase-->>Controller: Item ID
Controller->>Controller: Build success response
Controller-->>Client: 201 Created with Item ID
alt Error occurs
UseCase-->>Controller: Exception
Controller->>Controller: Build error response
Controller-->>Client: 4xx/5xx error
end
alt Authentication fails
Middleware-->>Client: 401 Unauthorized
end
```

82
cpp17/doc/architecture-overview.md

@ -0,0 +1,82 @@
# AutoStore Architecture Overview
## Layer Boundaries
```mermaid
graph TB
subgraph PL[Presentation Layer]
A[StoreController]
B[AuthController]
end
subgraph AL[Application Layer]
E[AddItem Use Case]
F[DeleteItem Use Case]
G[LoginUser Use Case]
H[GetItem Use Case]
I[ListItems Use Case]
J[HandleExpiredItems Use Case]
K[TaskScheduler]
L[IItemRepository]
M[IAuthService]
N[IOrderService]
O[ITimeProvider]
P[IThreadManager]
end
subgraph DL[Domain Layer]
Q[Item]
R[User]
S[ItemExpirationPolicy]
end
subgraph IL[Infrastructure Layer]
C[HttpServer]
D[HttpJwtMiddleware]
T[FileItemRepository]
U[FileJwtAuthService]
V[HttpOrderService]
W[SystemTimeProvider]
X[SystemThreadManager]
Y[CvBlocker]
end
```
## Component Dependencies
```mermaid
graph LR
StoreController --> AddItem
StoreController --> DeleteItem
StoreController --> GetItem
StoreController --> ListItems
StoreController --> IAuthService
AuthController --> LoginUser
AddItem --> IItemRepository
AddItem --> ITimeProvider
AddItem --> IOrderService
AddItem --> ItemExpirationPolicy
DeleteItem --> IItemRepository
LoginUser --> IAuthService
GetItem --> IItemRepository
ListItems --> IItemRepository
HandleExpiredItems --> IItemRepository
HandleExpiredItems --> ITimeProvider
HandleExpiredItems --> IOrderService
HandleExpiredItems --> ItemExpirationPolicy
TaskScheduler --> ITimeProvider
TaskScheduler --> IThreadManager
IItemRepository --> Item
IAuthService --> User
IOrderService --> Item
ItemExpirationPolicy --> Item
ItemExpirationPolicy --> ITimeProvider

4
cpp17/docker/.dockerignore

@ -0,0 +1,4 @@
.devcontainer
.out
build
build-*

28
cpp17/docker/Dockerfile

@ -0,0 +1,28 @@
FROM kuyoh/vcpkg:2025.06.13-ubuntu24.04 AS builder
WORKDIR /workspace
COPY ../CMakeLists.txt .
COPY ../vcpkg.json .
RUN vcpkg install
# Cche stays valid if only code changes
COPY .. .
RUN cmake -DCMAKE_TOOLCHAIN_FILE:STRING=${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake \
-DCMAKE_EXPORT_COMPILE_COMMANDS:BOOL=TRUE -DCMAKE_BUILD_TYPE:STRING=Release \
-H/workspace -B/workspace/build -G Ninja
RUN cmake --build /workspace/build --config Release --target all -j 8 --
# run tests
RUN cd /workspace/build && ctest --output-on-failure .
FROM ubuntu:24.04 AS runtime
WORKDIR /app
COPY --from=builder /workspace/build/bin/AutoStore ./AutoStore
COPY --from=builder /workspace/build/bin/data ./data
CMD ["./AutoStore"]

10
cpp17/docker/docker-compose.yml

@ -0,0 +1,10 @@
version: "3.9"
services:
app:
build:
context: ..
dockerfile: docker/Dockerfile
image: autostore-build-cpp-vcpkg-img
container_name: autostore-build-cpp-vcpkg
ports:
- 50080:50080

62
cpp17/lib/CMakeLists.txt

@ -0,0 +1,62 @@
cmake_minimum_required(VERSION 3.20)
project(AutoStoreLib LANGUAGES CXX VERSION 0.1.0)
set(TARGET_NAME AutoStoreLib)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Find dependencies
find_package(httplib CONFIG REQUIRED)
find_package(nlohmann_json CONFIG REQUIRED)
find_package(jwt-cpp CONFIG REQUIRED)
configure_file(src/Version.h.in ${CMAKE_BINARY_DIR}/autostore/Version.h)
add_library(${TARGET_NAME} STATIC
src/domain/helpers/Specification.cpp
src/application/queries/GetItem.cpp
src/application/queries/ListItems.cpp
src/application/commands/AddItem.cpp
src/application/commands/HandleExpiredItems.cpp
src/application/commands/DeleteItem.cpp
src/application/commands/LoginUser.cpp
src/infrastructure/repositories/FileItemRepository.cpp
src/infrastructure/http/HttpServer.cpp
src/infrastructure/http/HttpJwtMiddleware.cpp
src/infrastructure/http/HttpOrderService.cpp
src/infrastructure/helpers/Jsend.cpp
src/infrastructure/helpers/JsonItem.cpp
src/infrastructure/auth/FileJwtAuthService.cpp
src/application/services/TaskScheduler.cpp
src/infrastructure/adapters/CvBlocker.cpp
src/infrastructure/adapters/SystemThreadManager.cpp
src/infrastructure/adapters/SystemTimeProvider.cpp
src/webapi/controllers/BaseController.cpp
src/webapi/controllers/StoreController.cpp
src/webapi/controllers/AuthController.cpp
src/AutoStore.cpp
)
target_include_directories(${TARGET_NAME}
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include/autostore
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src
${CMAKE_BINARY_DIR}
)
target_sources(${TARGET_NAME}
PUBLIC
FILE_SET HEADERS
BASE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/include
FILES
include/autostore/AutoStore.h
include/autostore/ILogger.h
)
target_link_libraries(${TARGET_NAME}
PUBLIC
httplib::httplib
nlohmann_json::nlohmann_json
jwt-cpp::jwt-cpp
)

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

@ -0,0 +1,67 @@
#pragma once
#include "autostore/ILogger.h"
#include <memory>
#include <string>
#include <string_view>
#include <thread>
namespace nxl::autostore {
namespace application {
class IItemRepository;
class ITimeProvider;
class IOrderService;
class IAuthService;
class IThreadManager;
class TaskScheduler;
} // namespace application
namespace infrastructure {
class HttpServer;
} // namespace infrastructure
namespace webapi {
class StoreController;
class AuthController;
} // namespace webapi
namespace application {
class AddItem;
class LoginUser;
} // namespace application
class AutoStore
{
public:
struct Config
{
std::string dataPath;
std::string host{"0.0.0.0"};
uint16_t port{50080};
};
AutoStore(Config config, ILoggerPtr logger);
~AutoStore();
bool initialize();
bool start();
void stop();
private:
Config config;
ILoggerPtr log;
std::unique_ptr<infrastructure::HttpServer> httpServer;
std::unique_ptr<application::TaskScheduler> taskScheduler;
std::unique_ptr<webapi::StoreController> storeController;
std::unique_ptr<webapi::AuthController> authController;
std::unique_ptr<application::IItemRepository> itemRepository;
std::unique_ptr<application::ITimeProvider> clock;
std::unique_ptr<application::IOrderService> orderService;
std::unique_ptr<application::IAuthService> authService;
std::unique_ptr<application::ITimeProvider> timeProvider;
std::unique_ptr<application::IThreadManager> threadManager;
};
} // namespace nxl::autostore

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

@ -0,0 +1,98 @@
#pragma once
#include <memory>
#include <string>
#include <utility>
#include <cstdint>
#include <string_view>
#include <cstdio>
namespace nxl::autostore {
template <typename T> auto to_printf_arg(T&& arg)
{
if constexpr (std::is_same_v<std::decay_t<T>, std::string_view>) {
return 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

132
cpp17/lib/src/AutoStore.cpp

@ -0,0 +1,132 @@
#include "AutoStore.h"
#include "infrastructure/repositories/FileItemRepository.h"
#include "infrastructure/adapters/SystemTimeProvider.h"
#include "infrastructure/http/HttpOrderService.h"
#include "infrastructure/auth/FileJwtAuthService.h"
#include "webapi/controllers/StoreController.h"
#include "webapi/controllers/AuthController.h"
#include "infrastructure/http/HttpServer.h"
#include "application/services/TaskScheduler.h"
#include "application/commands/HandleExpiredItems.h"
#include "infrastructure/adapters/SystemTimeProvider.h"
#include "infrastructure/adapters/SystemThreadManager.h"
#include "infrastructure/adapters/CvBlocker.h"
#include <iostream>
#include <filesystem>
#include <memory>
namespace nxl::autostore {
using namespace infrastructure;
using namespace application;
AutoStore::AutoStore(Config config, ILoggerPtr logger)
: config{std::move(config)}, log{std::move(logger)}
{}
AutoStore::~AutoStore()
{
if (httpServer && httpServer->isRunning()) {
stop();
}
}
bool AutoStore::initialize()
{
try {
std::filesystem::create_directories(config.dataPath);
// Initialize repositories and services
std::string itemsDbPath =
std::filesystem::path(config.dataPath) / "items.json";
itemRepository = std::make_unique<FileItemRepository>(itemsDbPath);
clock = std::make_unique<SystemTimeProvider>();
orderService = std::make_unique<HttpOrderService>(log);
// Initialize auth service
std::string usersDbPath =
std::filesystem::path(config.dataPath) / "users.json";
authService = std::make_unique<FileJwtAuthService>(usersDbPath);
// Initialize dependencies for task scheduler
timeProvider = std::make_unique<SystemTimeProvider>();
threadManager = std::make_unique<SystemThreadManager>();
auto blocker = std::make_unique<CvBlocker>();
// Initialize task scheduler (for handling expired items)
taskScheduler = std::make_unique<TaskScheduler>(
log, *timeProvider, *threadManager, std::move(blocker));
// Initialize HTTP server
httpServer = std::make_unique<HttpServer>(log, *authService);
// Initialize store controller
storeController = std::make_unique<webapi::StoreController>(
webapi::StoreController::Context{
application::AddItem{*itemRepository, *clock, *orderService},
application::ListItems{*itemRepository},
application::GetItem{*itemRepository},
application::DeleteItem{*itemRepository}, *authService});
// Initialize auth controller
authController = std::make_unique<webapi::AuthController>(
webapi::AuthController::Context{application::LoginUser{*authService}});
log->info("Data path: %s", config.dataPath);
log->info("AutoStore initialized successfully, handling expired items...");
return true;
} catch (const std::exception& e) {
log->error("Failed to initialize AutoStore: %s", e.what());
return false;
}
}
bool AutoStore::start()
{
log->info("Starting AutoStore services...");
try {
taskScheduler->schedule(
[this]() {
application::HandleExpiredItems{*itemRepository, *clock, *orderService}
.execute();
},
00, 00, 00, // midnight (00:00:00)
TaskScheduler::RunMode::Forever | TaskScheduler::RunMode::OnStart);
taskScheduler->start();
storeController->registerRoutes(httpServer->getServer());
authController->registerRoutes(httpServer->getServer());
if (!httpServer->start(config.port, config.host)) {
log->error("Failed to start HTTP server");
return false;
}
log->info("AutoStore services started successfully");
log->info("HTTP server listening on http://%s:%d", config.host,
config.port);
log->info("API endpoint: POST http://%s:%d/api/v1", config.host,
config.port);
return true;
} catch (const std::exception& e) {
log->error("Failed to start AutoStore services: %s", e.what());
return false;
}
}
void AutoStore::stop()
{
log->info("Stopping AutoStore services...");
if (httpServer && httpServer->isRunning()) {
httpServer->stop();
}
log->info("AutoStore services stopped");
}
} // namespace nxl::autostore

21
cpp17/lib/src/Version.h.in

@ -0,0 +1,21 @@
#ifndef VERSION_H_IN
#define VERSION_H_IN
#include <string>
namespace nxl {
static constexpr int VERSION_MAJOR = ${PROJECT_VERSION_MAJOR};
static constexpr int VERSION_MINOR = ${PROJECT_VERSION_MINOR};
static constexpr int VERSION_PATCH = ${PROJECT_VERSION_PATCH};
static constexpr char VERSION_SUFFIX[] = "${PROJECT_VERSION_SUFFIX}";
inline std::string getVersionString()
{
return std::to_string(VERSION_MAJOR) + "." + std::to_string(VERSION_MINOR)
+ "." + std::to_string(VERSION_PATCH) + VERSION_SUFFIX;
}
} // namespace nxl
#endif // VERSION_H_IN

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

@ -0,0 +1,27 @@
#include "AddItem.h"
#include <stdexcept>
namespace nxl::autostore::application {
AddItem::AddItem(IItemRepository& itemRepository, ITimeProvider& clock,
IOrderService& orderService)
: itemRepository(itemRepository), clock(clock), orderService(orderService)
{}
domain::Item::Id_t AddItem::execute(domain::Item&& item)
{
if (item.userId == domain::User::NULL_ID) {
throw std::runtime_error("User ID not provided");
}
const auto currentTime = clock.now();
if (expirationPolicy.isExpired(item, currentTime)) {
item.id = domain::Item::NULL_ID;
orderService.orderItem(item);
return item.id; // TODO: test that
}
return itemRepository.save(item);
}
} // namespace nxl::autostore::application

27
cpp17/lib/src/application/commands/AddItem.h

@ -0,0 +1,27 @@
#pragma once
#include "domain/entities/Item.h"
#include "domain/polices/ItemExpirationPolicy.h"
#include "application/interfaces/IItemRepository.h"
#include "application/interfaces/ITimeProvider.h"
#include "application/interfaces/IOrderService.h"
namespace nxl::autostore::application {
class AddItem
{
public:
virtual ~AddItem() = default;
AddItem(IItemRepository& itemRepository, ITimeProvider& clock,
IOrderService& orderService);
domain::Item::Id_t execute(domain::Item&& item);
private:
IItemRepository& itemRepository;
ITimeProvider& clock;
IOrderService& orderService;
domain::ItemExpirationPolicy expirationPolicy;
};
} // namespace nxl::autostore::application

19
cpp17/lib/src/application/commands/DeleteItem.cpp

@ -0,0 +1,19 @@
#include "DeleteItem.h"
#include "application/exceptions/AutoStoreExceptions.h"
namespace nxl::autostore::application {
DeleteItem::DeleteItem(IItemRepository& itemRepository)
: itemRepository(itemRepository)
{}
void DeleteItem::execute(domain::Item::Id_t id, domain::User::Id_t ownerId)
{
auto item = itemRepository.findById(id);
if (!item || item->userId != ownerId) {
throw ItemNotFoundException("Item not found");
}
itemRepository.remove(id);
}
} // namespace nxl::autostore::application

21
cpp17/lib/src/application/commands/DeleteItem.h

@ -0,0 +1,21 @@
#pragma once
#include "domain/entities/Item.h"
#include "application/interfaces/IItemRepository.h"
#include <string_view>
namespace nxl::autostore::application {
class DeleteItem
{
public:
virtual ~DeleteItem() = default;
explicit DeleteItem(IItemRepository& itemRepository);
void execute(domain::Item::Id_t id, domain::User::Id_t ownerId);
private:
IItemRepository& itemRepository;
};
} // namespace nxl::autostore::application

29
cpp17/lib/src/application/commands/HandleExpiredItems.cpp

@ -0,0 +1,29 @@
#include "HandleExpiredItems.h"
#include <stdexcept>
namespace nxl::autostore::application {
HandleExpiredItems::HandleExpiredItems(IItemRepository& itemRepository,
ITimeProvider& clock,
IOrderService& orderService)
: itemRepository(itemRepository), clock(clock), orderService(orderService)
{}
uint16_t HandleExpiredItems::execute()
{
const auto currentTime = clock.now();
auto items = itemRepository.findWhere([&](const domain::Item& i) {
return expirationPolicy.isExpired(i, currentTime);
});
// remove expired one and order a new one
for (auto& item : items) {
itemRepository.remove(item.id);
orderService.orderItem(item);
}
return items.size();
}
} // namespace nxl::autostore::application

30
cpp17/lib/src/application/commands/HandleExpiredItems.h

@ -0,0 +1,30 @@
#pragma once
#include "domain/entities/Item.h"
#include "domain/polices/ItemExpirationPolicy.h"
#include "application/interfaces/IItemRepository.h"
#include "application/interfaces/ITimeProvider.h"
#include "application/interfaces/IOrderService.h"
namespace nxl::autostore::application {
class HandleExpiredItems
{
public:
virtual ~HandleExpiredItems() = default;
HandleExpiredItems(IItemRepository& itemRepository, ITimeProvider& clock,
IOrderService& orderService);
/**
* @returns number of expired items
*/
uint16_t execute();
private:
IItemRepository& itemRepository;
ITimeProvider& clock;
IOrderService& orderService;
domain::ItemExpirationPolicy expirationPolicy;
};
} // namespace nxl::autostore::application

20
cpp17/lib/src/application/commands/LoginUser.cpp

@ -0,0 +1,20 @@
#include "LoginUser.h"
#include <stdexcept>
namespace nxl::autostore::application {
LoginUser::LoginUser(IAuthService& authService) : authService(authService) {}
std::string LoginUser::execute(std::string_view username,
std::string_view password)
{
auto userId = authService.authenticateUser(username, password);
if (!userId) {
throw std::runtime_error("Invalid username or password");
}
// Generate a token for the authenticated user
return authService.generateToken(*userId);
}
} // namespace nxl::autostore::application

20
cpp17/lib/src/application/commands/LoginUser.h

@ -0,0 +1,20 @@
#pragma once
#include "domain/entities/User.h"
#include "application/interfaces/IAuthService.h"
namespace nxl::autostore::application {
class LoginUser
{
public:
virtual ~LoginUser() = default;
LoginUser(IAuthService& authService);
std::string execute(std::string_view username, std::string_view password);
private:
IAuthService& authService;
};
} // namespace nxl::autostore::application

16
cpp17/lib/src/application/exceptions/AutoStoreExceptions.h

@ -0,0 +1,16 @@
#pragma once
#include <stdexcept>
#include <string>
namespace nxl::autostore::application {
class ItemNotFoundException : public std::runtime_error
{
public:
explicit ItemNotFoundException(const std::string& message)
: std::runtime_error(message)
{}
};
} // namespace nxl::autostore::application

21
cpp17/lib/src/application/interfaces/IAuthService.h

@ -0,0 +1,21 @@
#pragma once
#include "domain/entities/User.h"
#include <string>
#include <string_view>
#include <optional>
namespace nxl::autostore::application {
class IAuthService
{
public:
virtual ~IAuthService() = default;
virtual std::optional<domain::User::Id_t>
authenticateUser(std::string_view username, std::string_view password) = 0;
virtual std::string generateToken(std::string_view userId) = 0;
virtual std::optional<domain::User::Id_t>
extractUserId(std::string_view token) = 0;
};
} // namespace nxl::autostore::application

20
cpp17/lib/src/application/interfaces/IBlocker.h

@ -0,0 +1,20 @@
#pragma once
#include <chrono>
namespace nxl::autostore::application {
class IBlocker
{
public:
using TimePoint = std::chrono::system_clock::time_point;
virtual ~IBlocker() = default;
virtual void block() = 0;
virtual void blockFor(const std::chrono::milliseconds& duration) = 0;
virtual void blockUntil(const TimePoint& timePoint) = 0;
virtual void notify() = 0;
virtual bool isBlocked() = 0;
virtual bool wasNotified() = 0;
};
} // namespace nxl::autostore::application

27
cpp17/lib/src/application/interfaces/IItemRepository.h

@ -0,0 +1,27 @@
#pragma once
#include "domain/entities/Item.h"
#include "domain/polices/ItemExpirationPolicy.h"
#include <optional>
#include <functional>
#include <string>
#include <string_view>
#include <vector>
namespace nxl::autostore::application {
class IItemRepository
{
public:
virtual ~IItemRepository() = default;
virtual domain::Item::Id_t save(const domain::Item& item) = 0;
virtual std::optional<domain::Item> findById(domain::Item::Id_t id) = 0;
virtual std::vector<domain::Item> findByOwner(domain::User::Id_t ownerId) = 0;
virtual std::vector<domain::Item>
findWhere(std::function<bool(const domain::Item&)> predicate) = 0;
virtual std::vector<domain::Item>
findWhere(const domain::ItemExpirationSpec& spec) = 0;
virtual void remove(domain::Item::Id_t id) = 0;
};
} // namespace nxl::autostore::application

14
cpp17/lib/src/application/interfaces/IOrderService.h

@ -0,0 +1,14 @@
#pragma once
#include "domain/entities/Item.h"
namespace nxl::autostore::application {
class IOrderService
{
public:
virtual ~IOrderService() = default;
virtual void orderItem(const domain::Item& item) = 0;
};
} // namespace nxl::autostore::application

28
cpp17/lib/src/application/interfaces/IThreadManager.h

@ -0,0 +1,28 @@
#pragma once
#include <functional>
#include <thread>
namespace nxl::autostore::application {
class IThreadManager
{
public:
class ThreadHandle
{
public:
virtual ~ThreadHandle() = default;
virtual void join() = 0;
virtual bool joinable() const = 0;
};
using ThreadHandlePtr = std::unique_ptr<ThreadHandle>;
virtual ~IThreadManager() = default;
virtual ThreadHandlePtr createThread(std::function<void()> func) = 0;
virtual std::thread::id getCurrentThreadId() const = 0;
virtual void sleep(const std::chrono::milliseconds& duration) = 0;
};
} // namespace nxl::autostore::application

18
cpp17/lib/src/application/interfaces/ITimeProvider.h

@ -0,0 +1,18 @@
#pragma once
#include <chrono>
namespace nxl::autostore::application {
class ITimeProvider
{
public:
using Clock = std::chrono::system_clock;
virtual ~ITimeProvider() = default;
virtual Clock::time_point now() const = 0;
virtual std::tm to_tm(const Clock::time_point& timePoint) const = 0;
virtual Clock::time_point from_tm(const std::tm& tm) const = 0;
};
} // namespace nxl::autostore::application

21
cpp17/lib/src/application/queries/GetItem.cpp

@ -0,0 +1,21 @@
#include "GetItem.h"
#include "../exceptions/AutoStoreExceptions.h"
namespace nxl::autostore::application {
GetItem::GetItem(IItemRepository& itemRepository)
: itemRepository(itemRepository)
{}
std::optional<domain::Item> GetItem::execute(domain::Item::Id_t id,
domain::User::Id_t ownerId)
{
auto item = itemRepository.findById(id);
// act as not found when ownerId does not match for security
if (!item || item->userId != ownerId) {
throw ItemNotFoundException("Item not found");
}
return item;
}
} // namespace nxl::autostore::application

23
cpp17/lib/src/application/queries/GetItem.h

@ -0,0 +1,23 @@
#pragma once
#include "domain/entities/Item.h"
#include "application/interfaces/IItemRepository.h"
#include <optional>
#include <string_view>
namespace nxl::autostore::application {
class GetItem
{
public:
virtual ~GetItem() = default;
explicit GetItem(IItemRepository& itemRepository);
std::optional<domain::Item> execute(domain::Item::Id_t id,
domain::User::Id_t ownerId);
private:
IItemRepository& itemRepository;
};
} // namespace nxl::autostore::application

14
cpp17/lib/src/application/queries/ListItems.cpp

@ -0,0 +1,14 @@
#include "ListItems.h"
namespace nxl::autostore::application {
ListItems::ListItems(IItemRepository& itemRepository)
: itemRepository(itemRepository)
{}
std::vector<domain::Item> ListItems::execute(domain::User::Id_t ownerId)
{
return itemRepository.findByOwner(ownerId);
}
} // namespace nxl::autostore::application

21
cpp17/lib/src/application/queries/ListItems.h

@ -0,0 +1,21 @@
#pragma once
#include "domain/entities/Item.h"
#include "application/interfaces/IItemRepository.h"
#include <vector>
namespace nxl::autostore::application {
class ListItems
{
public:
virtual ~ListItems() = default;
explicit ListItems(IItemRepository& itemRepository);
std::vector<domain::Item> execute(domain::User::Id_t ownerId);
private:
IItemRepository& itemRepository;
};
} // namespace nxl::autostore::application

229
cpp17/lib/src/application/services/TaskScheduler.cpp

@ -0,0 +1,229 @@
#include "TaskScheduler.h"
#include <stdexcept>
namespace nxl::autostore::application {
namespace {
using Clock = std::chrono::system_clock;
using TimePoint = Clock::time_point;
using Duration = Clock::duration;
using Hours = std::chrono::hours;
using Minutes = std::chrono::minutes;
using Seconds = std::chrono::seconds;
using Milliseconds = std::chrono::milliseconds;
using Days = std::chrono::duration<int, std::ratio<86400>>;
using RunMode = TaskScheduler::RunMode;
bool isValidTime(int hour, int minute, int second)
{
return (hour >= 0 && hour <= 23) && (minute >= 0 && minute <= 59)
&& (second >= 0 && second <= 59);
}
bool areModesMutuallyExclusive(RunMode mode)
{
return (static_cast<int>(mode) & static_cast<int>(RunMode::Forever))
&& (static_cast<int>(mode) & static_cast<int>(RunMode::Once));
}
TimePoint todayAt(uint8_t hour, uint8_t minute, uint8_t second,
const ITimeProvider& timeProvider)
{
auto now = timeProvider.now();
auto midnight =
std::chrono::time_point_cast<Duration>(std::chrono::floor<Days>(now));
auto offset = Hours{hour} + Minutes{minute} + Seconds{second};
return midnight + offset;
}
bool shouldExecuteOnStart(const TaskScheduler::ScheduledTask& task)
{
return (static_cast<int>(task.mode) & static_cast<int>(RunMode::OnStart))
&& !task.executed;
}
TimePoint calculateNextExecutionTime(const TaskScheduler::ScheduledTask& task,
const ITimeProvider& timeProvider,
TimePoint now)
{
auto taskTime = todayAt(task.hour, task.minute, task.second, timeProvider);
if (taskTime <= now) {
if (static_cast<int>(task.mode) & static_cast<int>(RunMode::Forever)) {
taskTime += Hours(24);
} else if ((static_cast<int>(task.mode) & static_cast<int>(RunMode::Once))
&& task.executed) {
return TimePoint{};
}
}
return taskTime;
}
bool shouldExecuteBasedOnTime(const TaskScheduler::ScheduledTask& task,
TimePoint now)
{
if (task.nextExecution == TimePoint{}) {
return false;
}
return task.nextExecution <= now;
}
void executeTask(TaskScheduler::ScheduledTask& task, ILogger& logger,
const ITimeProvider& timeProvider, TimePoint& nextWakeupTime)
{
try {
task.function();
task.executed = true;
logger.info("Task executed successfully");
if (static_cast<int>(task.mode) & static_cast<int>(RunMode::Forever)) {
auto nextTaskTime =
todayAt(task.hour, task.minute, task.second, timeProvider);
nextTaskTime += Hours(24);
task.nextExecution = nextTaskTime;
if (nextTaskTime < nextWakeupTime) {
nextWakeupTime = nextTaskTime;
}
}
} catch (const std::exception& e) {
logger.error("Task execution failed: %s", e.what());
}
}
void processTasks(std::vector<TaskScheduler::ScheduledTask>& tasks,
ILogger& logger, const ITimeProvider& timeProvider,
std::atomic<bool>& stopRequested, TimePoint now,
bool& hasOnStartTask, TimePoint& nextWakeupTime)
{
for (auto& task : tasks) {
if (stopRequested) {
break;
}
bool executeNow = false;
if (shouldExecuteOnStart(task)) {
executeNow = true;
hasOnStartTask = true;
} else if ((static_cast<int>(task.mode) & static_cast<int>(RunMode::Once))
|| (static_cast<int>(task.mode)
& static_cast<int>(RunMode::Forever))) {
if (task.nextExecution == TimePoint{}) {
auto taskTime = calculateNextExecutionTime(task, timeProvider, now);
if ((static_cast<int>(task.mode) & static_cast<int>(RunMode::Once))
&& taskTime == TimePoint{} && !task.executed) {
executeNow = true;
} else if (taskTime != TimePoint{}) {
task.nextExecution = taskTime;
}
}
if (!executeNow && shouldExecuteBasedOnTime(task, now)) {
executeNow = true;
}
if (!executeNow && task.nextExecution < nextWakeupTime) {
nextWakeupTime = task.nextExecution;
}
}
if (executeNow) {
executeTask(task, logger, timeProvider, nextWakeupTime);
}
}
}
void waitForNextTask(IBlocker& blocker, std::atomic<bool>& stopRequested,
TimePoint now, TimePoint nextWakeupTime)
{
if (!stopRequested && nextWakeupTime > now) {
auto waitDuration =
std::chrono::duration_cast<Milliseconds>(nextWakeupTime - now);
auto maxWait = Minutes(1);
if (waitDuration > maxWait) {
waitDuration = maxWait;
}
blocker.blockFor(waitDuration);
}
}
} // namespace
TaskScheduler::TaskScheduler(ILoggerPtr logger, ITimeProvider& timeProvider,
IThreadManager& threadManager,
std::unique_ptr<IBlocker> blocker)
: logger{std::move(logger)}, timeProvider{timeProvider},
threadManager{threadManager}, blocker{std::move(blocker)}
{}
void TaskScheduler::schedule(TaskFunction task, int hour, int minute,
int second, RunMode mode)
{
if (!isValidTime(hour, minute, second)) {
throw std::invalid_argument("Invalid time parameters");
}
if (areModesMutuallyExclusive(mode)) {
throw std::invalid_argument(
"Forever and Once modes are mutually exclusive");
}
std::lock_guard<std::mutex> lock(tasksMutex);
tasks.emplace_back(std::move(task), hour, minute, second, mode);
}
void TaskScheduler::start()
{
if (running) {
return;
}
running = true;
stopRequested = false;
threadHandle = threadManager.createThread([this]() {
logger->info("TaskScheduler thread started");
while (!stopRequested) {
auto now = timeProvider.now();
bool shouldExecuteOnStart = false;
auto nextWakeupTime = now + Hours(24);
{
std::lock_guard<std::mutex> lock(tasksMutex);
processTasks(tasks, *logger, timeProvider, stopRequested, now,
shouldExecuteOnStart, nextWakeupTime);
}
if (shouldExecuteOnStart) {
continue;
}
waitForNextTask(*blocker, stopRequested, now, nextWakeupTime);
}
running = false;
logger->info("TaskScheduler thread stopped");
});
}
void TaskScheduler::stop()
{
if (!running) {
return;
}
stopRequested = true;
blocker->notify();
if (threadHandle && threadHandle->joinable()) {
threadHandle->join();
}
}
} // namespace nxl::autostore::application

85
cpp17/lib/src/application/services/TaskScheduler.h

@ -0,0 +1,85 @@
#pragma once
#include "application/interfaces/ITimeProvider.h"
#include "application/interfaces/IThreadManager.h"
#include "application/interfaces/IBlocker.h"
#include <autostore/ILogger.h>
#include <functional>
#include <chrono>
#include <vector>
#include <atomic>
#include <memory>
#include <mutex>
namespace nxl::autostore::application {
class TaskScheduler
{
public:
enum class RunMode {
OnStart = 1,
Forever = 2,
OnStartThenForever = 3, // OnStart | Forever
Once = 4,
OnStartThenOnce = 5 // OnStart | Once
};
using TaskFunction = std::function<void()>;
using TimePoint = application::ITimeProvider::Clock::time_point;
using ThreadHandle = application::IThreadManager::ThreadHandle;
struct ScheduledTask
{
TaskFunction function;
int hour;
int minute;
int second;
RunMode mode;
bool executed = false;
TimePoint nextExecution{};
ScheduledTask(TaskFunction t, int h, int m, int s, RunMode md)
: function(std::move(t)), hour(h), minute(m), second(s), mode(md)
{}
};
TaskScheduler(ILoggerPtr logger, ITimeProvider& timeProvider,
IThreadManager& threadManager,
std::unique_ptr<IBlocker> blocker);
TaskScheduler(const TaskScheduler&) = delete;
TaskScheduler& operator=(const TaskScheduler&) = delete;
virtual ~TaskScheduler() = default;
void schedule(TaskFunction task, int hour, int minute, int second,
RunMode mode);
void start();
void stop();
private:
ILoggerPtr logger;
ITimeProvider& timeProvider;
IThreadManager& threadManager;
std::unique_ptr<IBlocker> blocker;
std::vector<ScheduledTask> tasks;
std::mutex tasksMutex;
std::atomic<bool> running{false};
std::atomic<bool> stopRequested{false};
std::unique_ptr<ThreadHandle> threadHandle;
};
constexpr TaskScheduler::RunMode operator|(TaskScheduler::RunMode a,
TaskScheduler::RunMode b)
{
return static_cast<TaskScheduler::RunMode>(static_cast<uint8_t>(a)
| static_cast<uint8_t>(b));
}
constexpr uint8_t operator&(TaskScheduler::RunMode a, TaskScheduler::RunMode b)
{
return static_cast<uint8_t>(a) & static_cast<uint8_t>(b);
}
} // namespace nxl::autostore::application

20
cpp17/lib/src/domain/entities/Item.h

@ -0,0 +1,20 @@
#pragma once
#include "User.h"
#include <string>
#include <chrono>
namespace nxl::autostore::domain {
struct Item
{
using Id_t = std::string;
inline const static Id_t NULL_ID{""};
Id_t id;
std::string name;
std::chrono::system_clock::time_point expirationDate;
std::string orderUrl;
User::Id_t userId;
};
} // namespace nxl::autostore::domain

17
cpp17/lib/src/domain/entities/User.h

@ -0,0 +1,17 @@
#pragma once
#include <string>
namespace nxl::autostore::domain {
struct User
{
using Id_t = std::string;
inline static const Id_t NULL_ID{""};
Id_t id;
std::string username;
std::string passwordHash;
};
} // namespace nxl::autostore::domain

141
cpp17/lib/src/domain/helpers/Specification.cpp

@ -0,0 +1,141 @@
#include "Specification.h"
#include <stdexcept>
namespace nxl::helpers {
// Condition render implementation
std::string Condition::render(const Renderer& renderer) const
{
std::string opStr;
switch (op) {
case ComparisonOp::EQ:
opStr = renderer.opEq;
break;
case ComparisonOp::NE:
opStr = renderer.opNe;
break;
case ComparisonOp::LT:
opStr = renderer.opLt;
break;
case ComparisonOp::LE:
opStr = renderer.opLe;
break;
case ComparisonOp::GT:
opStr = renderer.opGt;
break;
case ComparisonOp::GE:
opStr = renderer.opGe;
break;
case ComparisonOp::LIKE:
opStr = renderer.opLike;
break;
case ComparisonOp::IS_NULL:
opStr = renderer.opIsNull;
break;
case ComparisonOp::IS_NOT_NULL:
opStr = renderer.opIsNotNull;
break;
}
std::string result = field + " " + opStr;
if (value) {
result += " " + renderer.formatValue(*value);
}
return result;
}
// ConditionGroup render implementation
std::string ConditionGroup::render(const Renderer& renderer) const
{
if (children.empty())
return "";
std::string opStr = (op == LogicalOp::AND) ? renderer.opAnd : renderer.opOr;
std::string result = renderer.groupStart;
bool first = true;
for (const auto& child : children) {
if (!first)
result += " " + opStr + " ";
result += child->render(renderer);
first = false;
}
result += renderer.groupEnd;
return result;
}
// SpecificationBuilder implementation
SpecificationBuilder::SpecificationBuilder()
{
root = std::make_unique<ConditionGroup>(LogicalOp::AND);
current = root.get();
}
SpecificationBuilder& SpecificationBuilder::field(const std::string& name)
{
lastField = name;
return *this;
}
SpecificationBuilder& SpecificationBuilder::like(std::string pattern)
{
addCondition(ComparisonOp::LIKE, std::move(pattern));
return *this;
}
SpecificationBuilder& SpecificationBuilder::isNull()
{
addCondition(ComparisonOp::IS_NULL);
return *this;
}
SpecificationBuilder& SpecificationBuilder::isNotNull()
{
addCondition(ComparisonOp::IS_NOT_NULL);
return *this;
}
SpecificationBuilder& SpecificationBuilder::andGroup()
{
startGroup(LogicalOp::AND);
return *this;
}
SpecificationBuilder& SpecificationBuilder::orGroup()
{
startGroup(LogicalOp::OR);
return *this;
}
SpecificationBuilder& SpecificationBuilder::endGroup()
{
if (stack.empty())
return *this;
current = stack.back();
stack.pop_back();
return *this;
}
std::unique_ptr<ISpecificationExpr> SpecificationBuilder::build()
{
return std::move(root);
}
void SpecificationBuilder::startGroup(LogicalOp opType)
{
auto newGroup = std::make_unique<ConditionGroup>(opType);
auto* newGroupPtr = newGroup.get();
current->add(std::move(newGroup));
stack.push_back(current);
current = newGroupPtr;
}
// Helper function
SpecificationBuilder makeSpecification()
{
return SpecificationBuilder();
}
} // namespace nxl::helpers

198
cpp17/lib/src/domain/helpers/Specification.h

@ -0,0 +1,198 @@
#pragma once
#include <string>
#include <vector>
#include <memory>
#include <variant>
#include <optional>
#include <functional>
#include <any>
#include <stdexcept>
namespace nxl::helpers {
class ISpecificationExpr;
class Condition;
class ConditionGroup;
enum class ComparisonOp { EQ, NE, LT, LE, GT, GE, LIKE, IS_NULL, IS_NOT_NULL };
enum class LogicalOp { AND, OR };
class ISpecificationExpr
{
public:
virtual ~ISpecificationExpr() = default;
virtual std::string render(const struct Renderer& renderer) const = 0;
};
class ConditionValue
{
public:
template <typename T> ConditionValue(T value) : value_(std::move(value)) {}
const std::any& get() const { return value_; }
template <typename T> const T& as() const
{
return std::any_cast<const T&>(value_);
}
template <typename T> bool is() const { return value_.type() == typeid(T); }
private:
std::any value_;
};
class Condition : public ISpecificationExpr
{
std::string field;
ComparisonOp op;
std::optional<ConditionValue> value; // Optional for operators like IS_NULL
public:
Condition(std::string f, ComparisonOp o, std::optional<ConditionValue> v = {})
: field(std::move(f)), op(o), value(std::move(v))
{}
const std::string& getField() const { return field; }
ComparisonOp getOp() const { return op; }
const std::optional<ConditionValue>& getValue() const { return value; }
std::string render(const struct Renderer& renderer) const override;
};
// Logical group of conditions
class ConditionGroup : public ISpecificationExpr
{
LogicalOp op;
std::vector<std::unique_ptr<ISpecificationExpr>> children;
public:
ConditionGroup(LogicalOp o) : op(o) {}
void add(std::unique_ptr<ISpecificationExpr> expr)
{
children.push_back(std::move(expr));
}
LogicalOp getOp() const { return op; }
const std::vector<std::unique_ptr<ISpecificationExpr>>& getChildren() const
{
return children;
}
std::string render(const struct Renderer& renderer) const override;
};
// Renderer interface - defines how to render operators and grouping
struct Renderer
{
std::string opEq;
std::string opNe;
std::string opLt;
std::string opLe;
std::string opGt;
std::string opGe;
std::string opLike;
std::string opIsNull;
std::string opIsNotNull;
std::string opAnd;
std::string opOr;
std::string groupStart;
std::string groupEnd;
// Value formatting function
std::function<std::string(const ConditionValue&)> formatValue;
};
// Fluent builder for specifications
class SpecificationBuilder
{
std::unique_ptr<ConditionGroup> root;
ConditionGroup* current;
std::vector<ConditionGroup*> stack;
std::string lastField;
public:
SpecificationBuilder();
// Set the field for the next condition
SpecificationBuilder& field(const std::string& name);
template <typename T> SpecificationBuilder& equals(T value)
{
addCondition(ComparisonOp::EQ, std::move(value));
return *this;
}
template <typename T> SpecificationBuilder& notEquals(T value)
{
addCondition(ComparisonOp::NE, std::move(value));
return *this;
}
template <typename T> SpecificationBuilder& lessThan(T value)
{
addCondition(ComparisonOp::LT, std::move(value));
return *this;
}
template <typename T> SpecificationBuilder& lessOrEqual(T value)
{
addCondition(ComparisonOp::LE, std::move(value));
return *this;
}
template <typename T> SpecificationBuilder& greaterThan(T value)
{
addCondition(ComparisonOp::GT, std::move(value));
return *this;
}
template <typename T> SpecificationBuilder& greaterOrEqual(T value)
{
addCondition(ComparisonOp::GE, std::move(value));
return *this;
}
SpecificationBuilder& like(std::string pattern);
SpecificationBuilder& isNull();
SpecificationBuilder& isNotNull();
SpecificationBuilder& andGroup();
SpecificationBuilder& orGroup();
SpecificationBuilder& endGroup();
std::unique_ptr<ISpecificationExpr> build();
private:
template <typename T> void addCondition(ComparisonOp op, T value)
{
if (lastField.empty()) {
throw std::runtime_error("No field specified for condition");
}
auto condition = std::make_unique<Condition>(
lastField, op, ConditionValue(std::move(value)));
current->add(std::move(condition));
lastField.clear();
}
void addCondition(ComparisonOp op)
{
if (lastField.empty()) {
throw std::runtime_error("No field specified for condition");
}
auto condition = std::make_unique<Condition>(lastField, op, std::nullopt);
current->add(std::move(condition));
lastField.clear();
}
void startGroup(LogicalOp opType);
};
// Helper function to create a specification builder
SpecificationBuilder makeSpecification();
} // namespace nxl::helpers

31
cpp17/lib/src/domain/polices/ItemExpirationPolicy.h

@ -0,0 +1,31 @@
#pragma once
#include "domain/entities/Item.h"
#include "domain/helpers/Specification.h"
#include <chrono>
namespace nxl::autostore::domain {
using ItemExpirationSpec = std::unique_ptr<nxl::helpers::ISpecificationExpr>;
class ItemExpirationPolicy
{
public:
using TimePoint = std::chrono::system_clock::time_point;
constexpr static const char* FIELD_EXP_DATE{"expiration_date"};
bool isExpired(const Item& item, const TimePoint& currentTime) const
{
return item.expirationDate <= currentTime;
}
ItemExpirationSpec getExpiredSpecification(const TimePoint& currentTime) const
{
return nxl::helpers::SpecificationBuilder()
.field(FIELD_EXP_DATE)
.lessOrEqual(currentTime)
.build();
}
};
} // namespace nxl::autostore::domain

55
cpp17/lib/src/infrastructure/adapters/CvBlocker.cpp

@ -0,0 +1,55 @@
#include "CvBlocker.h"
namespace nxl::autostore::infrastructure {
void CvBlocker::block()
{
notified = false;
std::unique_lock<std::mutex> lock(mutex);
blocked = true;
conditionVar.wait(lock);
blocked = false;
}
void CvBlocker::blockFor(const std::chrono::milliseconds& duration)
{
notified = false;
std::unique_lock<std::mutex> lock(mutex);
blocked = true;
conditionVar.wait_for(lock, duration);
blocked = false;
}
void CvBlocker::blockUntil(const TimePoint& timePoint)
{
blockUntilTimePoint(timePoint);
}
void CvBlocker::notify()
{
notified = true;
conditionVar.notify_all();
}
bool CvBlocker::isBlocked()
{
return blocked;
}
bool CvBlocker::wasNotified()
{
return notified;
}
template <class Clock, class Duration>
void CvBlocker::blockUntilTimePoint(
const std::chrono::time_point<Clock, Duration>& timePoint)
{
notified = false;
std::unique_lock<std::mutex> lock(mutex);
blocked = true;
conditionVar.wait_until(lock, timePoint);
blocked = false;
}
} // namespace nxl::autostore::infrastructure

33
cpp17/lib/src/infrastructure/adapters/CvBlocker.h

@ -0,0 +1,33 @@
#pragma once
#include "application/interfaces/IBlocker.h"
#include <condition_variable>
#include <mutex>
#include <atomic>
namespace nxl::autostore::infrastructure {
class CvBlocker : public application::IBlocker
{
public:
~CvBlocker() override = default;
void block() override;
void blockFor(const std::chrono::milliseconds& duration) override;
void blockUntil(const TimePoint& timePoint) override;
void notify() override;
bool isBlocked() override;
bool wasNotified() override;
private:
template <class Clock, class Duration>
void blockUntilTimePoint(
const std::chrono::time_point<Clock, Duration>& timePoint);
private:
std::condition_variable conditionVar;
std::mutex mutex;
std::atomic<bool> notified{false};
std::atomic<bool> blocked{false};
};
} // namespace nxl::autostore::infrastructure

43
cpp17/lib/src/infrastructure/adapters/SystemThreadManager.cpp

@ -0,0 +1,43 @@
#include "SystemThreadManager.h"
namespace nxl::autostore::infrastructure {
SystemThreadManager::SystemThreadHandle::SystemThreadHandle(
std::thread&& thread)
: thread{std::move(thread)}
{}
SystemThreadManager::SystemThreadHandle::~SystemThreadHandle()
{
if (thread.joinable()) {
thread.join();
}
}
void SystemThreadManager::SystemThreadHandle::join()
{
thread.join();
}
bool SystemThreadManager::SystemThreadHandle::joinable() const
{
return thread.joinable();
}
application::IThreadManager::ThreadHandlePtr
SystemThreadManager::createThread(std::function<void()> func)
{
return std::make_unique<SystemThreadHandle>(std::thread(func));
}
std::thread::id SystemThreadManager::getCurrentThreadId() const
{
return std::this_thread::get_id();
}
void SystemThreadManager::sleep(const std::chrono::milliseconds& duration)
{
std::this_thread::sleep_for(duration);
}
} // namespace nxl::autostore::infrastructure

27
cpp17/lib/src/infrastructure/adapters/SystemThreadManager.h

@ -0,0 +1,27 @@
#pragma once
#include "application/interfaces/IThreadManager.h"
namespace nxl::autostore::infrastructure {
class SystemThreadManager : public application::IThreadManager
{
public:
class SystemThreadHandle : public ThreadHandle
{
public:
explicit SystemThreadHandle(std::thread&& thread);
~SystemThreadHandle() override;
void join() override;
bool joinable() const override;
private:
std::thread thread;
};
ThreadHandlePtr createThread(std::function<void()> func) override;
std::thread::id getCurrentThreadId() const override;
void sleep(const std::chrono::milliseconds& duration) override;
};
} // namespace nxl::autostore::infrastructure

22
cpp17/lib/src/infrastructure/adapters/SystemTimeProvider.cpp

@ -0,0 +1,22 @@
#include "SystemTimeProvider.h"
namespace nxl::autostore::infrastructure {
SystemTimeProvider::Clock::time_point SystemTimeProvider::now() const
{
return Clock::now();
}
std::tm SystemTimeProvider::to_tm(const Clock::time_point& timePoint) const
{
auto time_t_now = Clock::to_time_t(timePoint);
return *std::localtime(&time_t_now);
}
SystemTimeProvider::Clock::time_point
SystemTimeProvider::from_tm(const std::tm& tm) const
{
return Clock::from_time_t(std::mktime(const_cast<std::tm*>(&tm)));
}
} // namespace nxl::autostore::infrastructure

15
cpp17/lib/src/infrastructure/adapters/SystemTimeProvider.h

@ -0,0 +1,15 @@
#pragma once
#include "application/interfaces/ITimeProvider.h"
namespace nxl::autostore::infrastructure {
class SystemTimeProvider : public application::ITimeProvider
{
public:
Clock::time_point now() const override;
std::tm to_tm(const Clock::time_point& timePoint) const override;
Clock::time_point from_tm(const std::tm& tm) const override;
};
} // namespace nxl::autostore::infrastructure

96
cpp17/lib/src/infrastructure/auth/FileJwtAuthService.cpp

@ -0,0 +1,96 @@
#include "FileJwtAuthService.h"
#include <jwt-cpp/jwt.h>
#include <fstream>
#include <nlohmann/json.hpp>
#include <picosha2.h>
namespace nxl::autostore::infrastructure {
namespace {
// hardcoded secret key for demo purposes
constexpr const char* secretKey{"secret-key"};
} // namespace
/**
* @note Normally that would be generated by the OP
*/
std::string FileJwtAuthService::generateToken(std::string_view userId)
{
auto token =
jwt::create()
.set_issuer("autostore")
.set_type("JWS")
.set_payload_claim("sub", jwt::claim(std::string(userId)))
.set_expires_at(std::chrono::system_clock::now() + std::chrono::hours(24))
.sign(jwt::algorithm::hs256{secretKey});
return token;
}
std::optional<domain::User::Id_t>
FileJwtAuthService::extractUserId(std::string_view token)
{
// Check cache first
std::string tokenStr(token);
auto cacheIt = uidCache.find(tokenStr);
if (cacheIt != uidCache.end()) {
return cacheIt->second;
}
try {
auto decoded = jwt::decode(tokenStr);
auto verifier = jwt::verify()
.allow_algorithm(jwt::algorithm::hs256{secretKey})
.with_issuer("autostore");
verifier.verify(decoded);
auto subClaim = decoded.get_payload_claim("sub");
auto userId = subClaim.as_string();
if (uidCache.size() >= MAX_UID_CACHE_SIZE) {
// Remove the oldest entry (first element) to make space
uidCache.erase(uidCache.begin());
}
uidCache[tokenStr] = userId;
return userId;
} catch (const std::exception& e) {
return std::nullopt;
}
}
/**
* @note Normally that wouldn't be this app's concern
*/
std::optional<domain::User::Id_t>
FileJwtAuthService::authenticateUser(std::string_view username,
std::string_view password)
{
try {
std::string passwordHash;
picosha2::hash256_hex_string(password, passwordHash);
std::ifstream file(dbPath);
if (!file.is_open()) {
return std::nullopt;
}
nlohmann::json usersJson;
file >> usersJson;
for (const auto& userJson : usersJson) {
if (userJson["username"] == username
&& userJson["password"] == passwordHash) {
return userJson["id"].get<std::string>();
}
}
return std::nullopt;
} catch (const std::exception& e) {
return std::nullopt;
}
}
} // namespace nxl::autostore::infrastructure

31
cpp17/lib/src/infrastructure/auth/FileJwtAuthService.h

@ -0,0 +1,31 @@
#pragma once
#include "application/interfaces/IAuthService.h"
#include <unordered_map>
#include <string>
#include <string_view>
#include <optional>
namespace nxl::autostore::infrastructure {
class FileJwtAuthService : public application::IAuthService
{
public:
FileJwtAuthService(std::string dbPath) : dbPath{std::move(dbPath)} {}
std::string generateToken(std::string_view userId) override;
std::optional<domain::User::Id_t>
extractUserId(std::string_view token) override;
std::optional<domain::User::Id_t>
authenticateUser(std::string_view username,
std::string_view password) override;
private:
static constexpr size_t MAX_UID_CACHE_SIZE = 1024;
std::string dbPath;
std::unordered_map<std::string, domain::User::Id_t> uidCache;
};
} // namespace nxl::autostore::infrastructure

32
cpp17/lib/src/infrastructure/helpers/Jsend.cpp

@ -0,0 +1,32 @@
#include "infrastructure/helpers/Jsend.h"
namespace nxl::autostore::infrastructure {
std::string Jsend::success(const nlohmann::json& data)
{
nlohmann::json response;
response["status"] = "success";
if (!data.is_null()) {
response["data"] = data;
}
return response.dump();
}
std::string Jsend::error(std::string_view message, int code,
const nlohmann::json& data)
{
nlohmann::json response;
response["status"] = "error";
response["message"] = message;
response["code"] = code;
if (!data.is_null()) {
response["data"] = data;
}
return response.dump();
}
} // namespace nxl::autostore::infrastructure

20
cpp17/lib/src/infrastructure/helpers/Jsend.h

@ -0,0 +1,20 @@
#pragma once
#include "nlohmann/json.hpp"
#include <string>
namespace nxl::autostore::infrastructure {
class Jsend
{
public:
static std::string success(const nlohmann::json& data = nullptr);
static std::string error(std::string_view message, int code = 500,
const nlohmann::json& data = nullptr);
private:
Jsend() = delete;
~Jsend() = delete;
};
} // namespace nxl::autostore::infrastructure

76
cpp17/lib/src/infrastructure/helpers/JsonItem.cpp

@ -0,0 +1,76 @@
#include "infrastructure/helpers/JsonItem.h"
#include <chrono>
#include <ctime>
#include <stdexcept>
#include <type_traits>
namespace nxl::autostore::infrastructure {
domain::Item JsonItem::fromJson(std::string_view jsonBody)
{
auto json = nlohmann::json::parse(jsonBody);
return fromJsonObj(json);
}
domain::Item JsonItem::fromJsonObj(const nlohmann::json& j)
{
domain::Item item;
item.id = j.value("id", "");
item.name = j.value("name", "");
item.orderUrl = j.value("orderUrl", "");
item.userId = j.value("userId", domain::User::NULL_ID);
if (j["expirationDate"].is_number()) {
// Handle numeric timestamp
time_t timestamp = j["expirationDate"];
item.expirationDate = std::chrono::system_clock::from_time_t(timestamp);
} else if (j["expirationDate"].is_string()) {
// Handle ISO 8601 string format
std::string dateStr = j["expirationDate"];
std::tm tm = {};
std::istringstream ss(dateStr);
// Parse the ISO 8601 format
ss >> std::get_time(&tm, "%Y-%m-%dT%H:%M:%S");
if (ss.fail()) {
throw std::runtime_error(
"Invalid format for expirationDate string. Expected ISO 8601 format "
"(YYYY-MM-DDTHH:MM:SS).");
}
// Convert to time_t
time_t timestamp = std::mktime(&tm);
if (timestamp == -1) {
throw std::runtime_error(
"Failed to convert expirationDate to timestamp.");
}
item.expirationDate = std::chrono::system_clock::from_time_t(timestamp);
} else {
throw std::runtime_error("Invalid type for expirationDate. Expected number "
"(Unix timestamp) or string (ISO 8601 format).");
}
if (item.name.empty()) {
throw std::runtime_error("Item name is required");
}
return item;
}
std::string JsonItem::toJson(const domain::Item& item)
{
return toJsonObj(item).dump();
}
nlohmann::json JsonItem::toJsonObj(const domain::Item& item)
{
nlohmann::json j;
j["id"] = item.id;
j["name"] = item.name;
j["expirationDate"] =
std::chrono::system_clock::to_time_t(item.expirationDate);
j["orderUrl"] = item.orderUrl;
j["userId"] = item.userId;
return j;
}
} // namespace nxl::autostore::infrastructure

22
cpp17/lib/src/infrastructure/helpers/JsonItem.h

@ -0,0 +1,22 @@
#pragma once
#include "domain/entities/Item.h"
#include "nlohmann/json.hpp"
#include <string>
namespace nxl::autostore::infrastructure {
class JsonItem
{
public:
static domain::Item fromJson(std::string_view jsonBody);
static std::string toJson(const domain::Item& item);
static nlohmann::json toJsonObj(const domain::Item& item);
static domain::Item fromJsonObj(const nlohmann::json& j);
private:
JsonItem() = delete;
~JsonItem() = delete;
};
} // namespace nxl::autostore::infrastructure

37
cpp17/lib/src/infrastructure/http/HttpJwtMiddleware.cpp

@ -0,0 +1,37 @@
#include "HttpJwtMiddleware.h"
namespace nxl::autostore::infrastructure {
httplib::Server::HandlerResponse HttpJwtMiddleware::authenticate(
const httplib::Request& req, httplib::Response& res,
application::IAuthService& authService, ILoggerPtr log)
{
log->v(1, "Pre-request handler: %s", req.path);
// Skip authentication for login endpoint
if (req.path == "/api/v1/login") {
log->v(1, "Skipping authentication for login endpoint");
return httplib::Server::HandlerResponse::Unhandled;
}
auto it = req.headers.find("Authorization");
if (it != req.headers.end()) {
auto authHeader = it->second;
if (authHeader.find("Bearer ") == 0) {
auto token = authHeader.substr(7); // Remove "Bearer " prefix
if (authService.extractUserId(token)) {
log->v(1, "Authorized request");
return httplib::Server::HandlerResponse::Unhandled;
}
}
}
log->v(1, "Unauthorized request");
res.status = 401;
res.set_content(
R"({"status":"error","message":"Unauthorized - Invalid or missing token"})",
"application/json");
return httplib::Server::HandlerResponse::Handled;
}
} // namespace nxl::autostore::infrastructure

20
cpp17/lib/src/infrastructure/http/HttpJwtMiddleware.h

@ -0,0 +1,20 @@
#pragma once
#include "application/interfaces/IAuthService.h"
#include "autostore/ILogger.h"
#include <httplib.h>
namespace nxl::autostore::infrastructure {
class HttpJwtMiddleware
{
public:
HttpJwtMiddleware() = default;
~HttpJwtMiddleware() = default;
static httplib::Server::HandlerResponse
authenticate(const httplib::Request& req, httplib::Response& res,
application::IAuthService& authService, ILoggerPtr logger);
};
} // namespace nxl::autostore::infrastructure

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

@ -0,0 +1,364 @@
/**
* HTTP-based order service implementation with retry logic and
* connection pooling
*
* FLOW OVERVIEW:
* 1. orderItem() validates input and enqueues order request
* 2. Background worker thread processes queue sequentially (FIFO)
* 3. Failed requests are retried with exponential backoff (1s, 2s, 4s...)
* 4. HTTP clients are cached per host and auto-cleaned when unused
* 5. Service shuts down gracefully, completing queued orders
*
* IMPORTANT LIMITATIONS:
* - Uses single worker thread - retries of failed requests will block
* processing of new orders until retry delay expires
* - Not suitable for time-critical operations due to sequential processing
* - Designed for fire-and-forget order notifications, not real-time
* transactions
*/
#include "HttpOrderService.h"
#include "autostore/Version.h"
#include <httplib.h>
#include <stdexcept>
#include <regex>
#include <thread>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <atomic>
#include <chrono>
#include <memory>
#include <unordered_map>
namespace nxl::autostore::infrastructure {
namespace {
constexpr int MAX_RETRIES = 3;
constexpr int CONNECTION_TIMEOUT_SECONDS = 5;
constexpr int READ_TIMEOUT_SECONDS = 5;
constexpr int WRITE_TIMEOUT_SECONDS = 5;
constexpr char CONTENT_TYPE_JSON[] = "application/json";
std::pair<std::string, std::string> parseUrl(const std::string& url)
{
static const std::regex url_regex(
R"(^(https?:\/\/)?([^\/:]+)(?::(\d+))?(\/[^\?]*)?(\?.*)?$)");
std::smatch matches;
if (!std::regex_match(url, matches, url_regex) || matches.size() < 5) {
throw std::runtime_error("Invalid URL format: " + url);
}
std::string host = matches[2].str();
std::string port = matches[3].str();
std::string path = matches[4].str();
std::string query = matches[5].str();
if (!port.empty()) {
host += ":" + port;
}
if (path.empty()) {
path = "/";
}
path += query;
return {host, path};
}
std::string createOrderPayload(const domain::Item& item)
{
// Escape JSON special characters in strings
auto escapeJson = [](const std::string& str) {
std::string escaped;
escaped.reserve(str.size() + 10); // Reserve extra space for escapes
for (char c : str) {
switch (c) {
case '"':
escaped += "\\\"";
break;
case '\\':
escaped += "\\\\";
break;
case '\b':
escaped += "\\b";
break;
case '\f':
escaped += "\\f";
break;
case '\n':
escaped += "\\n";
break;
case '\r':
escaped += "\\r";
break;
case '\t':
escaped += "\\t";
break;
default:
escaped += c;
break;
}
}
return escaped;
};
return R"({"itemName": ")" + escapeJson(item.name) + R"(", "itemId": ")"
+ escapeJson(item.id) + "\"}";
}
} // namespace
struct OrderRequest
{
std::string url;
std::string payload;
int retryCount = 0;
std::chrono::system_clock::time_point nextAttemptTime;
OrderRequest() = default;
OrderRequest(std::string url, std::string payload, int rc = 0,
std::chrono::system_clock::time_point nat =
std::chrono::system_clock::now())
: url{std::move(url)}, payload{std::move(payload)}, retryCount{rc},
nextAttemptTime(nat)
{}
};
class HttpOrderService::Impl
{
public:
explicit Impl(ILoggerPtr logger)
: log{std::move(logger)}, shutdownRequested{false}
{
if (!log) {
throw std::invalid_argument("Logger cannot be null");
}
userAgent = "Autostore/" + nxl::getVersionString();
workerThread = std::thread(&Impl::processQueue, this);
}
~Impl()
{
shutdown();
if (workerThread.joinable()) {
workerThread.join();
}
}
void enqueueOrder(const std::string& url, std::string payload)
{
{
std::lock_guard<std::mutex> lock(queueMutex);
if (shutdownRequested) {
throw std::runtime_error(
"Service is shutting down, cannot enqueue new orders");
}
orderQueue.emplace(url, std::move(payload));
}
queueCondition.notify_one();
}
private:
void shutdown()
{
{
std::lock_guard<std::mutex> lock(queueMutex);
shutdownRequested = true;
}
queueCondition.notify_one();
}
bool shouldShutdown() const
{
return shutdownRequested && orderQueue.empty();
}
bool isRequestReady(const OrderRequest& request) const
{
return request.nextAttemptTime <= std::chrono::system_clock::now();
}
void processQueue()
{
while (true) {
std::unique_lock<std::mutex> lock(queueMutex);
// Wait for orders or shutdown signal
queueCondition.wait(
lock, [this] { return !orderQueue.empty() || shutdownRequested; });
if (shouldShutdown()) {
break;
}
if (orderQueue.empty()) {
continue;
}
// Check if the front request is ready to be processed
if (!isRequestReady(orderQueue.front())) {
// Wait until the next attempt time
auto waitTime =
orderQueue.front().nextAttemptTime - std::chrono::system_clock::now();
if (waitTime > std::chrono::milliseconds(0)) {
queueCondition.wait_for(lock, waitTime);
}
continue;
}
// Extract request for processing
OrderRequest request = std::move(orderQueue.front());
orderQueue.pop();
// Release lock before processing to avoid blocking other operations
lock.unlock();
processRequest(request);
}
}
void processRequest(OrderRequest& request)
{
try {
sendPostRequest(request.url, request.payload);
log->i("Order request sent successfully to: %s", request.url.c_str());
} catch (const std::exception& e) {
log->e("Failed to send order request to %s: %s", request.url.c_str(),
e.what());
handleFailedRequest(request);
}
}
void handleFailedRequest(OrderRequest& request)
{
if (request.retryCount < MAX_RETRIES) {
request.retryCount++;
// Exponential backoff: 1s, 2s, 4s, 8s...
auto delay = std::chrono::seconds(1 << (request.retryCount - 1));
request.nextAttemptTime = std::chrono::system_clock::now() + delay;
log->w("Retrying order request to %s (attempt %d/%d) in %ld seconds",
request.url.c_str(), request.retryCount, MAX_RETRIES,
delay.count());
{
std::lock_guard<std::mutex> lock(queueMutex);
if (!shutdownRequested) {
orderQueue.push(std::move(request));
}
}
queueCondition.notify_one();
} else {
log->e("Max retries exceeded for order request to: %s",
request.url.c_str());
}
}
std::shared_ptr<httplib::Client> getOrCreateClient(const std::string& host)
{
std::lock_guard<std::mutex> lock(clientsMutex);
auto it = clients.find(host);
if (it != clients.end()) {
// Check if client is still valid
auto client = it->second.lock();
if (client) {
return client;
} else {
// Remove expired weak_ptr
clients.erase(it);
}
}
// Create new client
auto client = std::make_shared<httplib::Client>(host);
configureClient(*client);
clients[host] = client;
return client;
}
void configureClient(httplib::Client& client)
{
client.set_connection_timeout(CONNECTION_TIMEOUT_SECONDS, 0);
client.set_read_timeout(READ_TIMEOUT_SECONDS, 0);
client.set_write_timeout(WRITE_TIMEOUT_SECONDS, 0);
// Enable keep-alive for better performance
client.set_keep_alive(true);
// Set reasonable limits
client.set_compress(true);
}
void sendPostRequest(const std::string& url, const std::string& payload)
{
auto [host, path] = parseUrl(url);
auto client = getOrCreateClient(host);
httplib::Headers headers = {{"Content-Type", CONTENT_TYPE_JSON},
{"User-Agent", userAgent},
{"Accept", CONTENT_TYPE_JSON}};
log->i("Sending POST request to: %s%s", host.c_str(), path.c_str());
log->v(1, "Payload: %s", payload.c_str());
auto res = client->Post(path, headers, payload, CONTENT_TYPE_JSON);
if (!res) {
throw std::runtime_error("Failed to connect to: " + host);
}
log->v(2, "Response status: %d", res->status);
log->v(3, "Response body: %s", res->body.c_str());
if (res->status < 200 || res->status >= 300) {
std::string error_msg =
"HTTP request failed with status: " + std::to_string(res->status)
+ " for URL: " + url;
if (!res->body.empty()) {
error_msg += " Response: " + res->body;
}
throw std::runtime_error(error_msg);
}
}
ILoggerPtr log;
std::queue<OrderRequest> orderQueue;
std::mutex queueMutex;
std::condition_variable queueCondition;
std::thread workerThread;
std::atomic<bool> shutdownRequested;
// Use weak_ptr to allow automatic cleanup of unused clients
std::unordered_map<std::string, std::weak_ptr<httplib::Client>> clients;
std::mutex clientsMutex;
std::string userAgent;
};
HttpOrderService::HttpOrderService(ILoggerPtr logger)
: impl{std::make_unique<Impl>(std::move(logger))}
{}
HttpOrderService::~HttpOrderService() = default;
void HttpOrderService::orderItem(const domain::Item& item)
{
if (item.orderUrl.empty()) {
throw std::runtime_error("Order URL is empty for item: " + item.name);
}
if (item.orderUrl.find("://") == std::string::npos) {
throw std::runtime_error("Invalid URL format for item: " + item.name
+ " (missing protocol)");
}
std::string payload = createOrderPayload(item);
impl->enqueueOrder(item.orderUrl, std::move(payload));
}
} // namespace nxl::autostore::infrastructure

24
cpp17/lib/src/infrastructure/http/HttpOrderService.h

@ -0,0 +1,24 @@
#pragma once
#include "application/interfaces/IOrderService.h"
#include "domain/entities/Item.h"
#include "autostore/ILogger.h"
namespace nxl::autostore::infrastructure {
class HttpOrderService : public application::IOrderService
{
public:
explicit HttpOrderService(ILoggerPtr logger);
virtual ~HttpOrderService();
void orderItem(const domain::Item& item) override;
private:
ILoggerPtr log;
void sendPostRequest(std::string_view url, std::string_view payload);
class Impl;
std::unique_ptr<Impl> impl;
};
} // namespace nxl::autostore::infrastructure

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

@ -0,0 +1,123 @@
#include "infrastructure/http/HttpServer.h"
#include "infrastructure/http/HttpJwtMiddleware.h"
#include <iostream>
#include <future>
namespace nxl::autostore::infrastructure {
namespace {
constexpr std::chrono::seconds defaultStartupTimeout{5};
}
HttpServer::HttpServer(ILoggerPtr logger,
application::IAuthService& authService)
: log{std::move(logger)}, authService{authService}
{}
HttpServer::~HttpServer()
{
if (running) {
stop();
}
}
bool HttpServer::start(int port, std::string_view host)
{
if (running) {
return true;
}
log->info("Starting HTTP server on %s:%d...", host.data(), port);
std::promise<bool> startupPromise;
std::future<bool> startupFuture = startupPromise.get_future();
serverThread = std::thread([host, port, this, &startupPromise]() {
log->v(1, "Server thread started, binding to %s:%d...", host, port);
try {
// Try to bind to the port
if (!server.bind_to_port(host.data(), port)) {
startupPromise.set_value(false);
return;
}
server.set_logger([this](const httplib::Request& req, const auto& res) {
log->v(1, "Request: %s\n%s", req.path, req.body);
log->v(1, "Response: %d, %s", res.status, res.body);
});
// JWT authentication middleware
server.set_pre_request_handler(
[this](const httplib::Request& req,
httplib::Response& res) -> httplib::Server::HandlerResponse {
return HttpJwtMiddleware::authenticate(req, res, authService, log);
});
// Signal that the server has bound to the port
startupPromise.set_value(true);
log->info("Server thread listening on %s:%d", host, port);
bool listenResult = server.listen_after_bind();
log->info("Server stopped, listen result: %d", listenResult);
} catch (const std::exception& e) {
log->error("Server thread exception: %s", e.what());
startupPromise.set_exception(std::current_exception());
} catch (...) {
log->error("Server thread unknown exception");
startupPromise.set_exception(std::current_exception());
}
});
// Wait for the server to start (with a timeout)
if (startupFuture.wait_for(defaultStartupTimeout)
== std::future_status::timeout) {
log->error("Failed to start HTTP server - timeout");
return false;
}
try {
// Check if the server bound to the port successfully
if (!startupFuture.get()) {
log->error("Failed to start HTTP server - could not bind to port");
return false;
}
} catch (const std::exception& e) {
log->error("Failed to start HTTP server - %s", e.what());
return false;
}
running = true;
log->info("HTTP server is running");
return true;
}
void HttpServer::stop()
{
if (!running) {
return;
}
log->info("Stopping HTTP server...");
server.stop();
if (serverThread.joinable()) {
log->info("Waiting for server thread to join...");
serverThread.join();
}
running = false;
log->info("HTTP server stopped");
}
bool HttpServer::isRunning() const
{
return running;
}
httplib::Server& HttpServer::getServer()
{
return server;
}
} // namespace nxl::autostore::infrastructure

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

@ -0,0 +1,32 @@
#pragma once
#include "autostore/ILogger.h"
#include "application/interfaces/IAuthService.h"
#include <httplib.h>
#include <memory>
#include <string>
#include <thread>
namespace nxl::autostore::infrastructure {
class HttpServer
{
public:
HttpServer(ILoggerPtr logger, application::IAuthService& authService);
~HttpServer();
bool start(int port = 50080, std::string_view host = "0.0.0.0");
void stop();
bool isRunning() const;
httplib::Server& getServer();
private:
ILoggerPtr log;
application::IAuthService& authService;
bool running{false};
httplib::Server server;
std::thread serverThread;
};
} // namespace nxl::autostore::infrastructure

132
cpp17/lib/src/infrastructure/repositories/FileItemRepository.cpp

@ -0,0 +1,132 @@
#include "infrastructure/repositories/FileItemRepository.h"
#include "infrastructure/helpers/JsonItem.h"
#include <fstream>
#include <algorithm>
#include <chrono>
#include <ctime>
#include <iterator>
#include "FileItemRepository.h"
namespace nxl::autostore::infrastructure {
namespace {
// Helper functions for vector serialization
inline nlohmann::json itemsToJson(const std::vector<domain::Item>& items)
{
nlohmann::json j = nlohmann::json::array();
for (const auto& item : items) {
j.push_back(infrastructure::JsonItem::toJsonObj(item));
}
return j;
}
inline std::vector<domain::Item> jsonToItems(const nlohmann::json& j)
{
std::vector<domain::Item> items;
for (const auto& itemJson : j) {
items.push_back(infrastructure::JsonItem::fromJsonObj(itemJson));
}
return items;
}
} // namespace
FileItemRepository::FileItemRepository(std::string_view dbPath) : dbPath(dbPath)
{
load();
}
domain::Item::Id_t FileItemRepository::save(const domain::Item& item)
{
std::lock_guard<std::mutex> lock(mtx);
domain::Item::Id_t id = item.id;
auto it =
std::find_if(items.begin(), items.end(),
[&](const domain::Item& i) { return i.id == item.id; });
if (it != items.end()) {
*it = item;
} else {
domain::Item newItem{item};
newItem.id = "item-"
+ std::to_string(
std::chrono::system_clock::now().time_since_epoch().count());
items.push_back(newItem);
id = newItem.id;
}
persist();
return id;
}
std::optional<domain::Item> FileItemRepository::findById(domain::Item::Id_t id)
{
std::lock_guard<std::mutex> lock(mtx);
auto it = std::find_if(items.begin(), items.end(),
[&](const domain::Item& i) { return i.id == id; });
if (it != items.end()) {
return *it;
}
return std::nullopt;
}
std::vector<domain::Item>
FileItemRepository::findByOwner(domain::User::Id_t userId)
{
std::lock_guard<std::mutex> lock(mtx);
std::vector<domain::Item> userItems;
std::copy_if(items.begin(), items.end(), std::back_inserter(userItems),
[&](const domain::Item& i) { return i.userId == userId; });
return userItems;
}
std::vector<domain::Item> FileItemRepository::findWhere(
std::function<bool(const domain::Item&)> predicate)
{
std::lock_guard<std::mutex> lock(mtx);
std::vector<domain::Item> matchedItems;
std::copy_if(items.begin(), items.end(), std::back_inserter(matchedItems),
predicate);
return matchedItems;
}
std::vector<domain::Item>
nxl::autostore::infrastructure::FileItemRepository::findWhere(
const domain::ItemExpirationSpec& spec)
{
// Not implemented since no SQL-like query language is used
throw std::runtime_error("Not implemented");
}
void FileItemRepository::remove(domain::Item::Id_t id)
{
std::lock_guard<std::mutex> lock(mtx);
items.erase(std::remove_if(items.begin(), items.end(),
[&](const domain::Item& i) { return i.id == id; }),
items.end());
persist();
}
void FileItemRepository::load()
{
std::lock_guard<std::mutex> lock(mtx);
std::ifstream file(dbPath);
if (file.is_open()) {
nlohmann::json j;
file >> j;
items = jsonToItems(j);
}
}
void FileItemRepository::persist()
{
std::ofstream file(dbPath);
if (file.is_open()) {
nlohmann::json j = itemsToJson(items);
file << j.dump(4);
}
}
} // namespace nxl::autostore::infrastructure

32
cpp17/lib/src/infrastructure/repositories/FileItemRepository.h

@ -0,0 +1,32 @@
#pragma once
#include "application/interfaces/IItemRepository.h"
#include <string>
#include <vector>
#include <mutex>
namespace nxl::autostore::infrastructure {
class FileItemRepository : public application::IItemRepository
{
public:
explicit FileItemRepository(std::string_view dbPath);
domain::Item::Id_t save(const domain::Item& item) override;
std::optional<domain::Item> findById(domain::Item::Id_t id) override;
std::vector<domain::Item> findByOwner(domain::User::Id_t userId) override;
std::vector<domain::Item>
findWhere(std::function<bool(const domain::Item&)> predicate) override;
virtual std::vector<domain::Item>
findWhere(const domain::ItemExpirationSpec& spec) override;
void remove(domain::Item::Id_t id) override;
private:
void load();
void persist();
std::string dbPath;
std::vector<domain::Item> items;
std::mutex mtx;
};
} // namespace nxl::autostore::infrastructure

58
cpp17/lib/src/webapi/controllers/AuthController.cpp

@ -0,0 +1,58 @@
#include "webapi/controllers/AuthController.h"
#include "infrastructure/helpers/JsonItem.h"
#include "infrastructure/helpers/Jsend.h"
#include "application/commands/LoginUser.h"
#include <nlohmann/json.hpp>
namespace nxl::autostore::webapi {
AuthController::AuthController(Context&& context)
: BaseController(std::move(context))
{}
std::vector<BaseController::RouteConfig> AuthController::getRoutes() const
{
return {{"/api/v1/login", "POST",
[this](const httplib::Request& req, httplib::Response& res) {
const_cast<AuthController*>(this)->loginUser(req, res);
}}};
}
void AuthController::loginUser(const httplib::Request& req,
httplib::Response& res)
{
try {
if (req.body.empty()) {
sendError(res, "Request body is empty",
httplib::StatusCode::BadRequest_400);
return;
}
auto requestBody = nlohmann::json::parse(req.body);
std::string username = requestBody.value("username", "");
std::string password = requestBody.value("password", "");
if (username.empty() || password.empty()) {
sendError(res, "Username and password are required",
httplib::StatusCode::BadRequest_400);
return;
}
try {
std::string token =
getContext<Context>().loginUserUc.execute(username, password);
nlohmann::json responseData = nlohmann::json::object();
responseData["token"] = token;
res.status = httplib::StatusCode::OK_200;
res.set_content(infrastructure::Jsend::success(responseData),
"application/json");
} catch (const std::exception& e) {
sendError(res, "Authentication failed: " + std::string(e.what()),
httplib::StatusCode::Unauthorized_401);
}
} catch (const std::exception& e) {
sendError(res, e.what(), httplib::StatusCode::BadRequest_400);
}
}
} // namespace nxl::autostore::webapi

26
cpp17/lib/src/webapi/controllers/AuthController.h

@ -0,0 +1,26 @@
#pragma once
#include "webapi/controllers/BaseController.h"
#include "application/commands/LoginUser.h"
#include <httplib.h>
namespace nxl::autostore::webapi {
class AuthController : public BaseController
{
public:
struct Context
{
application::LoginUser loginUserUc;
};
AuthController(Context&& context);
protected:
std::vector<RouteConfig> getRoutes() const override;
private:
void loginUser(const httplib::Request& req, httplib::Response& res);
};
} // namespace nxl::autostore::webapi

23
cpp17/lib/src/webapi/controllers/BaseController.cpp

@ -0,0 +1,23 @@
#include "webapi/controllers/BaseController.h"
namespace nxl::autostore::webapi {
void BaseController::registerRoutes(httplib::Server& server)
{
auto routes = getRoutes();
for (const auto& route : routes) {
if (route.method == "GET") {
server.Get(route.path, route.handler);
} else if (route.method == "POST") {
server.Post(route.path, route.handler);
} else if (route.method == "PUT") {
server.Put(route.path, route.handler);
} else if (route.method == "DELETE") {
server.Delete(route.path, route.handler);
} else if (route.method == "PATCH") {
server.Patch(route.path, route.handler);
}
}
}
} // namespace nxl::autostore::webapi

86
cpp17/lib/src/webapi/controllers/BaseController.h

@ -0,0 +1,86 @@
#pragma once
#include "infrastructure/helpers/Jsend.h"
#include <httplib.h>
#include <functional>
#include <string_view>
#include <nlohmann/json.hpp>
namespace nxl::autostore::webapi {
class BaseController
{
public:
using HttpRequestHandler =
std::function<void(const httplib::Request&, httplib::Response&)>;
struct RouteConfig
{
std::string path;
std::string method;
HttpRequestHandler handler;
};
template <typename Context>
BaseController(Context&& context)
: contextStorage(
std::make_unique<ContextHolder<Context>>(std::move(context)))
{}
virtual ~BaseController() = default;
void registerRoutes(httplib::Server& server);
protected:
virtual std::vector<RouteConfig> getRoutes() const = 0;
void sendError(httplib::Response& res, std::string_view message, int status)
{
res.status = status;
res.set_content(infrastructure::Jsend::error(message, status),
"application/json");
}
template <typename T> T& getContext()
{
return static_cast<ContextHolder<T>*>(contextStorage.get())->getContext();
}
std::string extractUserToken(const httplib::Request& req)
{
auto header = req.get_header_value("Authorization");
if (header.empty()) {
throw std::runtime_error("Authorization header is missing");
}
if (header.substr(0, 7) != "Bearer ") {
throw std::runtime_error("Authorization header is invalid");
}
return header.substr(7);
}
template <typename T, typename U>
std::optional<U> extractUserId(const httplib::Request& req)
{
auto token = extractUserToken(req);
return getContext<T>().authService.extractUserId(token);
}
private:
struct ContextHolderBase
{
virtual ~ContextHolderBase() = default;
};
template <typename T> struct ContextHolder : ContextHolderBase
{
ContextHolder(T&& ctx) : context(std::move(ctx)) {}
T& getContext() { return context; }
T context;
};
std::unique_ptr<ContextHolderBase> contextStorage;
};
} // namespace nxl::autostore::webapi

128
cpp17/lib/src/webapi/controllers/StoreController.cpp

@ -0,0 +1,128 @@
#include "webapi/controllers/StoreController.h"
#include "infrastructure/helpers/JsonItem.h"
#include "infrastructure/helpers/Jsend.h"
#include "application/commands/AddItem.h"
#include "application/queries/ListItems.h"
#include "application/queries/GetItem.h"
#include "application/commands/DeleteItem.h"
#include "application/exceptions/AutoStoreExceptions.h"
#include <optional>
namespace nxl::autostore::webapi {
StoreController::StoreController(Context&& context)
: BaseController(std::move(context))
{}
std::vector<BaseController::RouteConfig> StoreController::getRoutes() const
{
return {{"/api/v1/items", "POST",
[this](const httplib::Request& req, httplib::Response& res) {
const_cast<StoreController*>(this)->addItem(req, res);
}},
{"/api/v1/items", "GET",
[this](const httplib::Request& req, httplib::Response& res) {
const_cast<StoreController*>(this)->listItems(req, res);
}},
{"/api/v1/items/(.*)", "GET",
[this](const httplib::Request& req, httplib::Response& res) {
const_cast<StoreController*>(this)->getItem(req, res);
}},
{"/api/v1/items/(.*)", "DELETE",
[this](const httplib::Request& req, httplib::Response& res) {
const_cast<StoreController*>(this)->deleteItem(req, res);
}}};
}
void StoreController::addItem(const httplib::Request& req,
httplib::Response& res)
{
try {
auto userId = extractUserId<Context, domain::User::Id_t>(req);
assertUserId(userId);
auto item = infrastructure::JsonItem::fromJson(req.body);
item.userId = userId.value();
auto itemId = getContext<Context>().addItemUc.execute(std::move(item));
nlohmann::json responseData = nlohmann::json::object();
responseData["id"] = itemId;
res.status = httplib::StatusCode::Created_201;
res.set_content(infrastructure::Jsend::success(responseData),
"application/json");
} catch (const std::exception& e) {
sendError(res, e.what(), httplib::StatusCode::InternalServerError_500);
}
}
void StoreController::listItems(const httplib::Request& req,
httplib::Response& res)
{
try {
auto userId = extractUserId<Context, domain::User::Id_t>(req);
assertUserId(userId);
auto items = getContext<Context>().listItemsUc.execute(userId.value());
nlohmann::json responseData = nlohmann::json::array();
for (const auto& item : items) {
responseData.push_back(infrastructure::JsonItem::toJsonObj(item));
}
res.status = httplib::StatusCode::OK_200;
res.set_content(infrastructure::Jsend::success(responseData),
"application/json");
} catch (const std::exception& e) {
sendError(res, e.what(), httplib::StatusCode::InternalServerError_500);
}
}
void StoreController::getItem(const httplib::Request& req,
httplib::Response& res)
{
try {
auto itemId = req.matches[1];
auto userId = extractUserId<Context, domain::User::Id_t>(req);
assertUserId(userId);
auto item = getContext<Context>().getItemUc.execute(itemId, userId.value());
auto responseData = infrastructure::JsonItem::toJsonObj(*item);
res.status = httplib::StatusCode::OK_200;
res.set_content(infrastructure::Jsend::success(responseData),
"application/json");
} catch (const nxl::autostore::application::ItemNotFoundException& e) {
sendError(res, e.what(), httplib::StatusCode::NotFound_404);
} catch (const std::exception& e) {
sendError(res, e.what(), httplib::StatusCode::InternalServerError_500);
}
}
void StoreController::deleteItem(const httplib::Request& req,
httplib::Response& res)
{
try {
auto itemId = req.matches[1];
auto userId = extractUserId<Context, domain::User::Id_t>(req);
assertUserId(userId);
getContext<Context>().deleteItemUc.execute(itemId, userId.value());
nlohmann::json responseData = nlohmann::json::object();
responseData["message"] = "Item deleted successfully";
res.status = httplib::StatusCode::NoContent_204;
// Actually, no content should follow 204 response
// res.set_content(infrastructure::Jsend::success(responseData),
// "application/json");
} catch (const nxl::autostore::application::ItemNotFoundException& e) {
sendError(res, e.what(), httplib::StatusCode::NotFound_404);
} catch (const std::exception& e) {
sendError(res, e.what(), httplib::StatusCode::InternalServerError_500);
}
}
void StoreController::assertUserId(
std::optional<domain::User::Id_t> userId) const
{
if (!userId) {
throw std::runtime_error("User ID not found in request");
}
}
} // namespace nxl::autostore::webapi

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

@ -0,0 +1,40 @@
#pragma once
#include "webapi/controllers/BaseController.h"
#include "application/commands/AddItem.h"
#include "application/queries/ListItems.h"
#include "application/queries/GetItem.h"
#include "application/commands/DeleteItem.h"
#include "application/interfaces/IAuthService.h"
#include "infrastructure/helpers/JsonItem.h"
#include <httplib.h>
namespace nxl::autostore::webapi {
class StoreController : public BaseController
{
public:
struct Context
{
application::AddItem addItemUc;
application::ListItems listItemsUc;
application::GetItem getItemUc;
application::DeleteItem deleteItemUc;
application::IAuthService& authService;
};
StoreController(Context&& context);
protected:
std::vector<RouteConfig> getRoutes() const override;
private:
void addItem(const httplib::Request& req, httplib::Response& res);
void listItems(const httplib::Request& req, httplib::Response& res);
void getItem(const httplib::Request& req, httplib::Response& res);
void deleteItem(const httplib::Request& req, httplib::Response& res);
void assertUserId(std::optional<domain::User::Id_t> userId) const;
};
} // namespace nxl::autostore::webapi

38
cpp17/tests/CMakeLists.txt

@ -0,0 +1,38 @@
cmake_minimum_required(VERSION 3.20)
enable_testing()
find_package(Catch2 CONFIG REQUIRED)
find_package(trompeloeil CONFIG REQUIRED)
# Macro to create test executable
function(add_test_target TEST_NAME SOURCE_FILE)
add_executable(${TEST_NAME}
${SOURCE_FILE}
)
target_link_libraries(${TEST_NAME}
PRIVATE
AutoStoreLib
Catch2::Catch2WithMain
trompeloeil::trompeloeil
)
target_include_directories(${TEST_NAME}
PRIVATE
${PROJECT_SOURCE_DIR}/lib/include
${PROJECT_SOURCE_DIR}/lib/src
${PROJECT_SOURCE_DIR}/tests
)
# Add test to CTest
add_test(NAME ${TEST_NAME} COMMAND ${TEST_NAME})
endfunction()
# Create test executables
add_test_target(FileItemRepositoryTest integration/FileItemRepository.test.cpp)
# add_integration_test(FileUserRepositoryTest integration/FileUserRepository.test.cpp)
add_test_target(AddItemTest unit/AddItem.test.cpp)
add_test_target(SpecificationTest unit/Specification.test.cpp)
add_test_target(TaskSchedulerTest unit/TaskScheduler.test.cpp)

57
cpp17/tests/helpers/AddItemTestHelpers.h

@ -0,0 +1,57 @@
#pragma once
#include "domain/entities/Item.h"
#include "domain/entities/User.h"
#include <chrono>
#include <string>
namespace nxl::autostore::domain {
// Equality operator for Item to make trompeloeil work
inline bool operator==(const Item& lhs, const Item& rhs)
{
return lhs.id == rhs.id && lhs.name == rhs.name
&& lhs.orderUrl == rhs.orderUrl && lhs.userId == rhs.userId
&& lhs.expirationDate == rhs.expirationDate;
}
} // namespace nxl::autostore::domain
namespace test {
constexpr const char* TEST_ITEM_ID_1 = "item123";
constexpr const char* TEST_ITEM_ID_2 = "item456";
constexpr const char* TEST_ITEM_NAME_1 = "testitem";
constexpr const char* TEST_ORDER_URL_1 = "https://example.com/order1";
constexpr const char* TEST_USER_ID_1 = "user123";
constexpr const char* TEST_USER_ID_2 = "user456";
// Fixed test timepoint: 2020-01-01 12:00
constexpr std::chrono::system_clock::time_point TEST_TIMEPOINT_NOW =
std::chrono::system_clock::time_point(std::chrono::seconds(1577880000));
// Helper function to create a test item with default values
nxl::autostore::domain::Item
createTestItem(const std::string& id = TEST_ITEM_ID_1,
const std::string& name = TEST_ITEM_NAME_1,
const std::string& orderUrl = TEST_ORDER_URL_1,
const std::string& userId = TEST_USER_ID_1,
const std::chrono::system_clock::time_point& expirationDate =
std::chrono::system_clock::now() + std::chrono::hours(24))
{
nxl::autostore::domain::Item item;
item.id = id;
item.name = name;
item.orderUrl = orderUrl;
item.userId = userId;
item.expirationDate = expirationDate;
return item;
}
// Helper function to create an expired test item
nxl::autostore::domain::Item createExpiredTestItem()
{
return createTestItem(TEST_ITEM_ID_1, TEST_ITEM_NAME_1, TEST_ORDER_URL_1,
TEST_USER_ID_1,
TEST_TIMEPOINT_NOW - std::chrono::hours(1));
}
} // namespace test

392
cpp17/tests/integration/FileItemRepository.test.cpp

@ -0,0 +1,392 @@
#include "infrastructure/repositories/FileItemRepository.h"
#include "domain/entities/Item.h"
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_string.hpp>
#include <filesystem>
#include <fstream>
#include <optional>
#include <chrono>
using namespace nxl::autostore;
using Catch::Matchers::Equals;
namespace Test {
constexpr const char* TEST_ITEM_ID_1 = "item123";
constexpr const char* TEST_ITEM_ID_2 = "item456";
constexpr const char* TEST_ITEM_NAME_1 = "testitem";
constexpr const char* TEST_ITEM_NAME_2 = "anotheritem";
constexpr const char* TEST_ORDER_URL_1 = "https://example.com/order1";
constexpr const char* TEST_ORDER_URL_2 = "https://example.com/order2";
constexpr const char* TEST_USER_ID_1 = "user123";
constexpr const char* TEST_USER_ID_2 = "user456";
constexpr const char* NON_EXISTENT_ID = "nonexistent";
constexpr const char* NON_EXISTENT_USER_ID = "nonexistentuser";
constexpr const char* TEST_DIR_NAME = "autostore_test";
constexpr const char* TEST_DB_FILE_NAME = "test_items.json";
// Helper function to create a test item with default values
domain::Item
createTestItem(const std::string& id = TEST_ITEM_ID_1,
const std::string& name = TEST_ITEM_NAME_1,
const std::string& orderUrl = TEST_ORDER_URL_1,
const std::string& userId = TEST_USER_ID_1,
const std::chrono::system_clock::time_point& expirationDate =
std::chrono::system_clock::now() + std::chrono::hours(24))
{
domain::Item item;
item.id = id;
item.name = name;
item.orderUrl = orderUrl;
item.userId = userId;
item.expirationDate = expirationDate;
return item;
}
// Helper function to create a second test item
domain::Item createSecondTestItem()
{
return createTestItem(
TEST_ITEM_ID_2, TEST_ITEM_NAME_2, TEST_ORDER_URL_2, TEST_USER_ID_2,
std::chrono::system_clock::now() + std::chrono::hours(48));
}
// Helper function to set up test environment
std::string setupTestEnvironment()
{
std::filesystem::path testDir =
std::filesystem::temp_directory_path() / TEST_DIR_NAME;
std::filesystem::create_directories(testDir);
std::string testDbPath = (testDir / TEST_DB_FILE_NAME).string();
// Clean up any existing test file
if (std::filesystem::exists(testDbPath)) {
std::filesystem::remove(testDbPath);
}
return testDbPath;
}
// Helper function to clean up test environment
void cleanupTestEnvironment()
{
std::filesystem::path testDir =
std::filesystem::temp_directory_path() / TEST_DIR_NAME;
if (std::filesystem::exists(testDir)) {
std::filesystem::remove_all(testDir);
}
}
// Helper function to verify item properties match expected values
void verifyItemProperties(const domain::Item& item,
const std::string& expectedId,
const std::string& expectedName,
const std::string& expectedOrderUrl,
const std::string& expectedUserId)
{
REQUIRE(item.id == expectedId);
REQUIRE(item.name == expectedName);
REQUIRE(item.orderUrl == expectedOrderUrl);
REQUIRE(item.userId == expectedUserId);
}
// Helper function to verify item properties match default test item values
void verifyDefaultTestItem(const domain::Item& item)
{
verifyItemProperties(item, TEST_ITEM_ID_1, TEST_ITEM_NAME_1, TEST_ORDER_URL_1,
TEST_USER_ID_1);
}
// Helper function to verify item properties match second test item values
void verifySecondTestItem(const domain::Item& item)
{
verifyItemProperties(item, TEST_ITEM_ID_2, TEST_ITEM_NAME_2, TEST_ORDER_URL_2,
TEST_USER_ID_2);
}
} // namespace Test
TEST_CASE("FileItemRepository Integration Tests",
"[integration][FileItemRepository]")
{
// Setup test environment
std::string testDbPath = Test::setupTestEnvironment();
SECTION("when a new item is saved then it can be found by id")
{
// Given
infrastructure::FileItemRepository repository(testDbPath);
domain::Item testItem = Test::createTestItem();
// When
auto savedItemId = repository.save(testItem);
// Then
auto foundItem = repository.findById(savedItemId);
REQUIRE(foundItem.has_value());
REQUIRE(foundItem->id == savedItemId);
REQUIRE(foundItem->name == Test::TEST_ITEM_NAME_1);
REQUIRE(foundItem->orderUrl == Test::TEST_ORDER_URL_1);
REQUIRE(foundItem->userId == Test::TEST_USER_ID_1);
}
SECTION("when a new item is saved then it can be found by user")
{
// Given
infrastructure::FileItemRepository repository(testDbPath);
domain::Item testItem = Test::createTestItem();
// When
auto savedItemId = repository.save(testItem);
// Then
auto userItems = repository.findByOwner(Test::TEST_USER_ID_1);
REQUIRE(userItems.size() == 1);
REQUIRE(userItems[0].id == savedItemId);
REQUIRE(userItems[0].name == Test::TEST_ITEM_NAME_1);
REQUIRE(userItems[0].orderUrl == Test::TEST_ORDER_URL_1);
REQUIRE(userItems[0].userId == Test::TEST_USER_ID_1);
}
SECTION("when multiple items are saved then findAll returns all items")
{
// Given
infrastructure::FileItemRepository repository(testDbPath);
domain::Item firstItem = Test::createTestItem();
domain::Item secondItem = Test::createSecondTestItem();
// When
auto firstItemId = repository.save(firstItem);
auto secondItemId = repository.save(secondItem);
// Then
auto allItems =
repository.findWhere([](const domain::Item&) { return true; });
REQUIRE(allItems.size() == 2);
// Verify both items are present (order doesn't matter)
bool foundFirst = false;
bool foundSecond = false;
for (const auto& item : allItems) {
if (item.id == firstItemId) {
REQUIRE(item.name == Test::TEST_ITEM_NAME_1);
REQUIRE(item.orderUrl == Test::TEST_ORDER_URL_1);
REQUIRE(item.userId == Test::TEST_USER_ID_1);
foundFirst = true;
} else if (item.id == secondItemId) {
REQUIRE(item.name == Test::TEST_ITEM_NAME_2);
REQUIRE(item.orderUrl == Test::TEST_ORDER_URL_2);
REQUIRE(item.userId == Test::TEST_USER_ID_2);
foundSecond = true;
}
}
REQUIRE(foundFirst);
REQUIRE(foundSecond);
}
SECTION("when multiple items for same user are saved then findByUser returns "
"all user items")
{
// Given
infrastructure::FileItemRepository repository(testDbPath);
domain::Item firstItem = Test::createTestItem();
domain::Item secondItem =
Test::createTestItem("item789", "thirditem", "https://example.com/order3",
Test::TEST_USER_ID_1);
// When
auto firstItemId = repository.save(firstItem);
auto secondItemId = repository.save(secondItem);
// Then
auto userItems = repository.findByOwner(Test::TEST_USER_ID_1);
REQUIRE(userItems.size() == 2);
// Verify both items are present (order doesn't matter)
bool foundFirst = false;
bool foundSecond = false;
for (const auto& item : userItems) {
if (item.id == firstItemId) {
REQUIRE(item.name == Test::TEST_ITEM_NAME_1);
REQUIRE(item.orderUrl == Test::TEST_ORDER_URL_1);
REQUIRE(item.userId == Test::TEST_USER_ID_1);
foundFirst = true;
} else if (item.id == secondItemId) {
REQUIRE(item.name == "thirditem");
REQUIRE(item.orderUrl == "https://example.com/order3");
REQUIRE(item.userId == Test::TEST_USER_ID_1);
foundSecond = true;
}
}
REQUIRE(foundFirst);
REQUIRE(foundSecond);
}
SECTION("when an existing item is saved then it is updated")
{
// Given
infrastructure::FileItemRepository repository(testDbPath);
domain::Item testItem = Test::createTestItem();
auto savedItemId = repository.save(testItem);
// When
testItem.id = savedItemId;
testItem.name = "updateditemname";
testItem.orderUrl = "https://updated.example.com/order";
testItem.userId = Test::TEST_USER_ID_2;
auto updatedItemId = repository.save(testItem);
// Then
REQUIRE(savedItemId == updatedItemId); // ID should not change for updates
auto foundItem = repository.findById(updatedItemId);
REQUIRE(foundItem.has_value());
REQUIRE(foundItem->id == updatedItemId);
REQUIRE(foundItem->name == "updateditemname");
REQUIRE(foundItem->orderUrl == "https://updated.example.com/order");
REQUIRE(foundItem->userId == Test::TEST_USER_ID_2);
}
SECTION("when an item is removed then it cannot be found by id")
{
// Given
infrastructure::FileItemRepository repository(testDbPath);
domain::Item testItem = Test::createTestItem();
auto savedItemId = repository.save(testItem);
// When
repository.remove(savedItemId);
// Then
auto foundItem = repository.findById(savedItemId);
REQUIRE_FALSE(foundItem.has_value());
}
SECTION("when an item is removed then it is not in findByUser")
{
// Given
infrastructure::FileItemRepository repository(testDbPath);
domain::Item testItem = Test::createTestItem();
auto savedItemId = repository.save(testItem);
// When
repository.remove(savedItemId);
// Then
auto userItems = repository.findByOwner(Test::TEST_USER_ID_1);
REQUIRE(userItems.empty());
}
SECTION("when an item is removed then it is not in findAll")
{
// Given
infrastructure::FileItemRepository repository(testDbPath);
domain::Item firstItem = Test::createTestItem();
domain::Item secondItem = Test::createSecondTestItem();
auto firstItemId = repository.save(firstItem);
auto secondItemId = repository.save(secondItem);
// When
repository.remove(firstItemId);
// Then
auto allItems =
repository.findWhere([](const domain::Item&) { return true; });
REQUIRE(allItems.size() == 1);
REQUIRE(allItems[0].id == secondItemId);
REQUIRE(allItems[0].name == Test::TEST_ITEM_NAME_2);
REQUIRE(allItems[0].orderUrl == Test::TEST_ORDER_URL_2);
REQUIRE(allItems[0].userId == Test::TEST_USER_ID_2);
}
SECTION(
"when findById is called with non-existent id then it returns nullopt")
{
// Given
infrastructure::FileItemRepository repository(testDbPath);
// When
auto foundItem = repository.findById(Test::NON_EXISTENT_ID);
// Then
REQUIRE_FALSE(foundItem.has_value());
}
SECTION("when findByUser is called with non-existent user id then it returns "
"empty vector")
{
// Given
infrastructure::FileItemRepository repository(testDbPath);
domain::Item testItem = Test::createTestItem();
repository.save(testItem);
// When
auto userItems = repository.findByOwner(Test::NON_EXISTENT_USER_ID);
// Then
REQUIRE(userItems.empty());
}
SECTION("when remove is called with non-existent id then it does nothing")
{
// Given
infrastructure::FileItemRepository repository(testDbPath);
domain::Item testItem = Test::createTestItem();
auto savedItemId = repository.save(testItem);
// When
repository.remove(Test::NON_EXISTENT_ID);
// Then
auto allItems =
repository.findWhere([](const domain::Item&) { return true; });
REQUIRE(allItems.size() == 1);
REQUIRE(allItems[0].id == savedItemId);
REQUIRE(allItems[0].name == Test::TEST_ITEM_NAME_1);
REQUIRE(allItems[0].orderUrl == Test::TEST_ORDER_URL_1);
REQUIRE(allItems[0].userId == Test::TEST_USER_ID_1);
}
SECTION(
"when repository is created with existing data file then it loads the data")
{
// Given
std::string savedItemId;
{
infrastructure::FileItemRepository firstRepository(testDbPath);
domain::Item testItem = Test::createTestItem();
savedItemId = firstRepository.save(testItem);
}
// When
infrastructure::FileItemRepository secondRepository(testDbPath);
// Then
auto foundItem = secondRepository.findById(savedItemId);
REQUIRE(foundItem.has_value());
REQUIRE(foundItem->id == savedItemId);
REQUIRE(foundItem->name == Test::TEST_ITEM_NAME_1);
REQUIRE(foundItem->orderUrl == Test::TEST_ORDER_URL_1);
REQUIRE(foundItem->userId == Test::TEST_USER_ID_1);
}
SECTION("when repository is created with non-existent data file then it "
"starts empty")
{
// Given
std::filesystem::path testDir =
std::filesystem::temp_directory_path() / Test::TEST_DIR_NAME;
std::string nonExistentDbPath = (testDir / "nonexistent.json").string();
// When
infrastructure::FileItemRepository repository(nonExistentDbPath);
// Then
auto allItems =
repository.findWhere([](const domain::Item&) { return true; });
REQUIRE(allItems.empty());
}
// Clean up test environment
Test::cleanupTestEnvironment();
}

19
cpp17/tests/mocks/MockBlocker.h

@ -0,0 +1,19 @@
#pragma once
#include "application/interfaces/IBlocker.h"
#include <trompeloeil.hpp>
namespace test {
class MockBlocker : public nxl::autostore::application::IBlocker
{
public:
MAKE_MOCK0(block, void(), override);
MAKE_MOCK1(blockFor, void(const std::chrono::milliseconds&), override);
MAKE_MOCK1(blockUntil, void(const TimePoint&), override);
MAKE_MOCK0(notify, void(), override);
MAKE_MOCK0(isBlocked, bool(), override);
MAKE_MOCK0(wasNotified, bool(), override);
};
} // namespace test

24
cpp17/tests/mocks/MockItemRepository.h

@ -0,0 +1,24 @@
#pragma once
#include "application/interfaces/IItemRepository.h"
#include <trompeloeil.hpp>
namespace test {
using nxl::autostore::domain::Item;
using nxl::autostore::domain::User;
using nxl::autostore::domain::ItemExpirationSpec;
class MockItemRepository : public nxl::autostore::application::IItemRepository
{
public:
MAKE_MOCK1(save, Item::Id_t(const Item&), override);
MAKE_MOCK1(findById, std::optional<Item>(Item::Id_t), override);
MAKE_MOCK1(findByOwner, std::vector<Item>(User::Id_t), override);
MAKE_MOCK1(findWhere, std::vector<Item>(std::function<bool(const Item&)>),
override);
MAKE_MOCK1(findWhere, std::vector<Item>(const ItemExpirationSpec&), override);
MAKE_MOCK1(remove, void(Item::Id_t), override);
};
} // namespace test

14
cpp17/tests/mocks/MockOrderService.h

@ -0,0 +1,14 @@
#pragma once
#include "application/interfaces/IOrderService.h"
#include <trompeloeil.hpp>
namespace test {
class MockOrderService : public nxl::autostore::application::IOrderService
{
public:
MAKE_MOCK1(orderItem, void(const nxl::autostore::domain::Item&), override);
};
} // namespace test

24
cpp17/tests/mocks/MockThreadManager.h

@ -0,0 +1,24 @@
#pragma once
#include "application/interfaces/IThreadManager.h"
#include <trompeloeil.hpp>
namespace test {
class MockThreadHandle
: public nxl::autostore::application::IThreadManager::ThreadHandle
{
public:
MAKE_MOCK0(join, void(), override);
MAKE_CONST_MOCK0(joinable, bool(), override);
};
class MockThreadManager : public nxl::autostore::application::IThreadManager
{
public:
MAKE_MOCK1(createThread, ThreadHandlePtr(std::function<void()>), override);
MAKE_CONST_MOCK0(getCurrentThreadId, std::thread::id(), override);
MAKE_MOCK1(sleep, void(const std::chrono::milliseconds&), override);
};
} // namespace test

16
cpp17/tests/mocks/MockTimeProvider.h

@ -0,0 +1,16 @@
#pragma once
#include "application/interfaces/ITimeProvider.h"
#include <trompeloeil.hpp>
namespace test {
class MockTimeProvider : public nxl::autostore::application::ITimeProvider
{
public:
MAKE_MOCK0(now, Clock::time_point(), const override);
MAKE_MOCK1(to_tm, std::tm(const Clock::time_point&), const override);
MAKE_MOCK1(from_tm, Clock::time_point(const std::tm&), const override);
};
} // namespace test

51
cpp17/tests/mocks/TestLogger.h

@ -0,0 +1,51 @@
#pragma once
#include <autostore/ILogger.h>
#include <iostream>
#include <mutex>
namespace test {
class TestLogger : public nxl::autostore::ILogger
{
public:
TestLogger() = default;
virtual ~TestLogger() = default;
void log(LogLevel level, std::string_view message) override;
void vlog(int8_t, std::string_view message) override;
private:
std::mutex mutex_;
};
void TestLogger::log(LogLevel level, std::string_view message)
{
std::lock_guard<std::mutex> lock(mutex_);
const char* levelStr = "";
switch (level) {
case LogLevel::Info:
levelStr = "INFO";
break;
case LogLevel::Warning:
levelStr = "WARNING";
break;
case LogLevel::Error:
levelStr = "ERROR";
break;
case LogLevel::Debug:
levelStr = "DEBUG";
break;
case LogLevel::Verbose:
levelStr = "VERBOSE";
break;
}
std::cout << "[" << levelStr << "] " << message << std::endl;
}
void TestLogger::vlog(int8_t level, std::string_view message)
{
std::lock_guard<std::mutex> lock(mutex_);
std::cout << "[V" << static_cast<int>(level) << "] " << message << std::endl;
}
} // namespace test

223
cpp17/tests/unit/AddItem.test.cpp

@ -0,0 +1,223 @@
#include "application/commands/AddItem.h"
#include "domain/entities/Item.h"
#include "mocks/MockItemRepository.h"
#include "mocks/MockTimeProvider.h"
#include "mocks/MockOrderService.h"
#include "helpers/AddItemTestHelpers.h"
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_string.hpp>
#include <trompeloeil.hpp>
#include <memory>
#include <stdexcept>
using trompeloeil::_;
using namespace nxl::autostore;
using namespace std::chrono;
TEST_CASE("AddItem Unit Tests", "[unit][AddItem]")
{
test::MockItemRepository mockRepository;
test::MockTimeProvider mockClock;
test::MockOrderService mockOrderService;
SECTION(
"when user id is present and item is not expired then the item is saved")
{
// Given
auto testItem = test::createTestItem();
auto expectedItemId = "saved_item_id";
REQUIRE_CALL(mockRepository, save(testItem)).RETURN(expectedItemId);
REQUIRE_CALL(mockClock, now()).RETURN(test::TEST_TIMEPOINT_NOW);
FORBID_CALL(mockOrderService, orderItem(_));
application::AddItem addItem(mockRepository, mockClock, mockOrderService);
// When
auto resultItemId = addItem.execute(std::move(testItem));
// Then
REQUIRE(resultItemId == expectedItemId);
}
SECTION("when item has null user id then a runtime error is thrown")
{
// Given
auto testItem = test::createTestItem();
testItem.userId = domain::User::NULL_ID;
FORBID_CALL(mockRepository, save(_));
FORBID_CALL(mockClock, now());
FORBID_CALL(mockOrderService, orderItem(_));
application::AddItem addItem(mockRepository, mockClock, mockOrderService);
// When & Then
REQUIRE_THROWS_AS(addItem.execute(std::move(testItem)), std::runtime_error);
}
SECTION("when item is expired then the order is placed")
{
// Given
auto testItem = test::createExpiredTestItem();
REQUIRE_CALL(mockClock, now()).RETURN(test::TEST_TIMEPOINT_NOW);
REQUIRE_CALL(mockOrderService, orderItem(_));
FORBID_CALL(mockRepository, save(_));
application::AddItem addItem(mockRepository, mockClock, mockOrderService);
// When
addItem.execute(std::move(testItem));
// Then
// Order was placed (verified by REQUIRE_CALL above)
}
SECTION("when item is expired then null id is returned")
{
// Given
auto testItem = test::createExpiredTestItem();
REQUIRE_CALL(mockClock, now()).RETURN(test::TEST_TIMEPOINT_NOW);
REQUIRE_CALL(mockOrderService, orderItem(_));
FORBID_CALL(mockRepository, save(_));
application::AddItem addItem(mockRepository, mockClock, mockOrderService);
// When
auto resultItemId = addItem.execute(std::move(testItem));
// Then
REQUIRE(resultItemId == domain::Item::NULL_ID);
}
SECTION("when item expiration date is exactly current time then the order is "
"placed")
{
// Given
auto testItem = test::createTestItem();
testItem.expirationDate = test::TEST_TIMEPOINT_NOW;
REQUIRE_CALL(mockClock, now()).RETURN(test::TEST_TIMEPOINT_NOW);
REQUIRE_CALL(mockOrderService, orderItem(_));
FORBID_CALL(mockRepository, save(_));
application::AddItem addItem(mockRepository, mockClock, mockOrderService);
// When
addItem.execute(std::move(testItem));
// Then
// Order was placed (verified by REQUIRE_CALL above)
}
SECTION("when item expiration date is exactly current time then null id is "
"returned")
{
// Given
auto testItem = test::createTestItem();
testItem.expirationDate = test::TEST_TIMEPOINT_NOW;
REQUIRE_CALL(mockClock, now()).RETURN(test::TEST_TIMEPOINT_NOW);
REQUIRE_CALL(mockOrderService, orderItem(_));
FORBID_CALL(mockRepository, save(_));
application::AddItem addItem(mockRepository, mockClock, mockOrderService);
// When
auto resultItemId = addItem.execute(std::move(testItem));
// Then
REQUIRE(resultItemId == domain::Item::NULL_ID);
}
SECTION("when item expiration date is in the future then the item is saved")
{
// Given
auto testItem = test::createTestItem();
auto expectedItemId = "saved_item_id";
REQUIRE_CALL(mockClock, now()).RETURN(test::TEST_TIMEPOINT_NOW);
REQUIRE_CALL(mockRepository, save(testItem)).RETURN(expectedItemId);
FORBID_CALL(mockOrderService, orderItem(_));
application::AddItem addItem(mockRepository, mockClock, mockOrderService);
// When
addItem.execute(std::move(testItem));
// Then
// Item was saved (verified by REQUIRE_CALL above)
}
SECTION(
"when item expiration date is in the future then the item id is returned")
{
// Given
auto testItem = test::createTestItem();
auto expectedItemId = "saved_item_id";
REQUIRE_CALL(mockClock, now()).RETURN(test::TEST_TIMEPOINT_NOW);
REQUIRE_CALL(mockRepository, save(testItem)).RETURN(expectedItemId);
FORBID_CALL(mockOrderService, orderItem(_));
application::AddItem addItem(mockRepository, mockClock, mockOrderService);
// When
auto resultItemId = addItem.execute(std::move(testItem));
// Then
REQUIRE(resultItemId == expectedItemId);
}
SECTION(
"when repository save throws exception then a runtime error is thrown")
{
// Given
auto testItem = test::createTestItem();
auto expectedException = std::runtime_error("Repository error");
REQUIRE_CALL(mockClock, now()).RETURN(test::TEST_TIMEPOINT_NOW);
REQUIRE_CALL(mockRepository, save(testItem)).THROW(expectedException);
FORBID_CALL(mockOrderService, orderItem(_));
application::AddItem addItem(mockRepository, mockClock, mockOrderService);
// When & Then
REQUIRE_THROWS_AS(addItem.execute(std::move(testItem)), std::runtime_error);
}
SECTION("when order service throws exception then a runtime error is thrown")
{
// Given
auto testItem = test::createExpiredTestItem();
auto expectedException = std::runtime_error("Order service error");
REQUIRE_CALL(mockClock, now()).RETURN(test::TEST_TIMEPOINT_NOW);
REQUIRE_CALL(mockOrderService, orderItem(_)).THROW(expectedException);
FORBID_CALL(mockRepository, save(_));
application::AddItem addItem(mockRepository, mockClock, mockOrderService);
// When & Then
REQUIRE_THROWS_AS(addItem.execute(std::move(testItem)), std::runtime_error);
}
SECTION("when clock throws exception then a runtime error is thrown")
{
// Given
auto testItem = test::createTestItem();
auto expectedException = std::runtime_error("Clock error");
REQUIRE_CALL(mockClock, now()).THROW(expectedException);
FORBID_CALL(mockRepository, save(_));
FORBID_CALL(mockOrderService, orderItem(_));
application::AddItem addItem(mockRepository, mockClock, mockOrderService);
// When & Then
REQUIRE_THROWS_AS(addItem.execute(std::move(testItem)), std::runtime_error);
}
}

692
cpp17/tests/unit/Specification.test.cpp

@ -0,0 +1,692 @@
#include "domain/helpers/Specification.h"
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_string.hpp>
#include <trompeloeil.hpp>
#include <memory>
#include <stdexcept>
#include <chrono>
using trompeloeil::_;
using namespace nxl::helpers;
TEST_CASE("Specification Unit Tests", "[unit][Specification]")
{
const Renderer TEST_RENDERER{
.opEq = "=",
.opNe = "!=",
.opLt = "<",
.opLe = "<=",
.opGt = ">",
.opGe = ">=",
.opLike = "LIKE",
.opIsNull = "IS NULL",
.opIsNotNull = "IS NOT NULL",
.opAnd = "AND",
.opOr = "OR",
.groupStart = "(",
.groupEnd = ")",
.formatValue = [](const ConditionValue& v) -> std::string {
if (v.is<std::string>()) {
return "'" + v.as<std::string>() + "'";
} else if (v.is<const char*>()) {
return "'" + std::string(v.as<const char*>()) + "'";
} else if (v.is<bool>()) {
return v.as<bool>() ? "TRUE" : "FALSE";
} else if (v.is<int>()) {
return std::to_string(v.as<int>());
} else if (v.is<double>()) {
return std::to_string(v.as<double>());
} else if (v.is<std::chrono::system_clock::time_point>()) {
auto time = v.as<std::chrono::system_clock::time_point>();
auto time_t = std::chrono::system_clock::to_time_t(time);
std::string time_str = std::ctime(&time_t);
if (!time_str.empty() && time_str.back() == '\n') {
time_str.pop_back();
}
return "'" + time_str + "'";
}
return "'unknown'";
}};
SECTION("when single equals string condition is added then proper operator "
"is rendered")
{
// Given
auto spec = makeSpecification().field("F").equals("foo").build();
// When
auto result = spec->render(TEST_RENDERER);
// Then
REQUIRE(result == "(F = 'foo')");
}
SECTION(
"when condition with chrono time_point is used then it renders correctly")
{
// Given
auto now = std::chrono::system_clock::now();
auto spec = makeSpecification().field("timestamp").greaterThan(now).build();
// When
auto result = spec->render(TEST_RENDERER);
// Then
REQUIRE(result.find("timestamp >") != std::string::npos);
REQUIRE(result.find("'") != std::string::npos);
}
SECTION("when single not equals string condition is added then proper "
"operator is rendered")
{
// Given
auto spec = makeSpecification().field("F").notEquals("foo").build();
// When
auto result = spec->render(TEST_RENDERER);
// Then
REQUIRE(result == "(F != 'foo')");
}
SECTION(
"when single less than condition is added then proper operator is rendered")
{
// Given
auto spec = makeSpecification().field("F").lessThan(42).build();
// When
auto result = spec->render(TEST_RENDERER);
// Then
REQUIRE(result == "(F < 42)");
}
SECTION("when single less than or equal condition is added then proper "
"operator is rendered")
{
// Given
auto spec = makeSpecification().field("F").lessOrEqual(42).build();
// When
auto result = spec->render(TEST_RENDERER);
// Then
REQUIRE(result == "(F <= 42)");
}
SECTION("when single greater than condition is added then proper operator is "
"rendered")
{
// Given
auto spec = makeSpecification().field("F").greaterThan(42).build();
// When
auto result = spec->render(TEST_RENDERER);
// Then
REQUIRE(result == "(F > 42)");
}
SECTION("when single greater than or equal condition is added then proper "
"operator is rendered")
{
// Given
auto spec = makeSpecification().field("F").greaterOrEqual(42).build();
// When
auto result = spec->render(TEST_RENDERER);
// Then
REQUIRE(result == "(F >= 42)");
}
SECTION(
"when single like condition is added then proper operator is rendered")
{
// Given
auto spec = makeSpecification().field("F").like("%foo%").build();
// When
auto result = spec->render(TEST_RENDERER);
// Then
REQUIRE(result == "(F LIKE '%foo%')");
}
SECTION(
"when single is null condition is added then proper operator is rendered")
{
// Given
auto spec = makeSpecification().field("F").isNull().build();
// When
auto result = spec->render(TEST_RENDERER);
// Then
REQUIRE(result == "(F IS NULL)");
}
SECTION("when single is not null condition is added then proper operator is "
"rendered")
{
// Given
auto spec = makeSpecification().field("F").isNotNull().build();
// When
auto result = spec->render(TEST_RENDERER);
// Then
REQUIRE(result == "(F IS NOT NULL)");
}
SECTION("when condition with double value is used then it renders correctly")
{
// Given
auto spec = makeSpecification().field("price").equals(19.99).build();
// When
auto result = spec->render(TEST_RENDERER);
// Then
REQUIRE(result == "(price = 19.990000)");
}
SECTION("when condition with boolean value is used then it renders correctly")
{
// Given
auto spec = makeSpecification().field("active").equals(true).build();
// When
auto result = spec->render(TEST_RENDERER);
// Then
REQUIRE(result == "(active = TRUE)");
}
SECTION(
"when condition with const char* value is used then it renders correctly")
{
// Given
auto spec = makeSpecification().field("name").equals("test").build();
// When
auto result = spec->render(TEST_RENDERER);
// Then
REQUIRE(result == "(name = 'test')");
}
SECTION("when multiple AND conditions are added then they are rendered with "
"AND operator")
{
// Given
auto spec = makeSpecification()
.field("name")
.equals("John")
.field("age")
.greaterThan(30)
.field("active")
.equals(true)
.build();
// When
auto result = spec->render(TEST_RENDERER);
// Then
REQUIRE(result == "(name = 'John' AND age > 30 AND active = TRUE)");
}
SECTION("when AND group is created then conditions are grouped properly")
{
// Given
auto spec = makeSpecification()
.field("name")
.equals("John")
.andGroup()
.field("age")
.greaterThan(30)
.field("active")
.equals(true)
.endGroup()
.build();
// When
auto result = spec->render(TEST_RENDERER);
// Then
REQUIRE(result == "(name = 'John' AND (age > 30 AND active = TRUE))");
}
SECTION(
"when OR group is created then conditions are grouped with OR operator")
{
// Given
auto spec = makeSpecification()
.orGroup()
.field("name")
.equals("John")
.field("age")
.greaterThan(30)
.field("active")
.equals(true)
.endGroup()
.build();
// When
auto result = spec->render(TEST_RENDERER);
// Then
REQUIRE(result == "((name = 'John' OR age > 30 OR active = TRUE))");
}
SECTION("when nested groups are created then they are rendered properly")
{
// Given
auto spec = makeSpecification()
.field("category")
.equals("electronics")
.andGroup()
.field("price")
.lessThan(1000)
.orGroup()
.field("brand")
.equals("Sony")
.field("brand")
.equals("Samsung")
.endGroup()
.endGroup()
.build();
// When
auto result = spec->render(TEST_RENDERER);
// Then
REQUIRE(
result
== "(category = 'electronics' AND (price < 1000 AND (brand = 'Sony' "
"OR brand = 'Samsung')))");
}
SECTION("when complex specification with multiple nested groups is created "
"then it renders correctly")
{
// Given
auto spec = makeSpecification()
.orGroup()
.andGroup()
.field("type")
.equals("food")
.field("expiration_date")
.lessOrEqual("2023-01-01")
.endGroup()
.andGroup()
.field("type")
.equals("non-food")
.field("expiration_date")
.lessOrEqual("2023-03-01")
.endGroup()
.endGroup()
.build();
// When
auto result = spec->render(TEST_RENDERER);
// Then
REQUIRE(result
== "(((type = 'food' AND expiration_date <= '2023-01-01') OR (type "
"= 'non-food' AND expiration_date <= '2023-03-01')))");
}
SECTION("when empty group is created then it renders as empty")
{
// Given
auto spec = makeSpecification()
.field("name")
.equals("John")
.andGroup()
.endGroup()
.build();
// When
auto result = spec->render(TEST_RENDERER);
// Then
REQUIRE(result == "(name = 'John' AND )");
}
SECTION("when endGroup is called without matching startGroup then it should "
"handle gracefully")
{
// Given
auto spec =
makeSpecification().field("name").equals("John").endGroup().build();
// When
auto result = spec->render(TEST_RENDERER);
// Then
REQUIRE(result == "(name = 'John')");
}
SECTION("when condition is added without field then exception is thrown")
{
// Given
auto builder = makeSpecification();
// When/Then
REQUIRE_THROWS_AS(builder.equals("value"), std::runtime_error);
REQUIRE_THROWS_WITH(builder.equals("value"),
"No field specified for condition");
}
SECTION("when field is called multiple times then last field is used")
{
// Given
auto spec = makeSpecification()
.field("field1")
.field("field2")
.equals("value")
.build();
// When
auto result = spec->render(TEST_RENDERER);
// Then
REQUIRE(result == "(field2 = 'value')");
}
SECTION("when multiple conditions with same field are added then they are "
"rendered correctly")
{
// Given
auto spec = makeSpecification()
.field("age")
.greaterThan(18)
.field("age")
.lessThan(65)
.build();
// When
auto result = spec->render(TEST_RENDERER);
// Then
REQUIRE(result == "(age > 18 AND age < 65)");
}
SECTION("when specification has only conditions with no explicit grouping "
"then AND is used")
{
// Given
auto spec = makeSpecification()
.field("field1")
.equals("value1")
.field("field2")
.equals("value2")
.field("field3")
.equals("value3")
.build();
// When
auto result = spec->render(TEST_RENDERER);
// Then
REQUIRE(
result
== "(field1 = 'value1' AND field2 = 'value2' AND field3 = 'value3')");
}
SECTION("when mixed data types are used then they render correctly")
{
// Given
auto now = std::chrono::system_clock::now();
auto spec = makeSpecification()
.field("name")
.equals("John")
.field("age")
.greaterThan(30)
.field("salary")
.greaterOrEqual(50000.50)
.field("active")
.equals(true)
.field("created_at")
.lessThan(now)
.build();
// When
auto result = spec->render(TEST_RENDERER);
// Then
REQUIRE(result.find("(name = 'John' AND age > 30 AND salary >= "
"50000.500000 AND active = TRUE AND created_at < '")
!= std::string::npos);
REQUIRE(result.find("'") != std::string::npos);
}
SECTION("when ConditionValue is created with string then it stores and "
"retrieves correctly")
{
// Given
ConditionValue value(std::string("test"));
// Then
REQUIRE(value.is<std::string>());
REQUIRE_FALSE(value.is<int>());
REQUIRE(value.as<std::string>() == "test");
}
SECTION("when ConditionValue is created with int then it stores and "
"retrieves correctly")
{
// Given
ConditionValue value(42);
// Then
REQUIRE(value.is<int>());
REQUIRE_FALSE(value.is<std::string>());
REQUIRE(value.as<int>() == 42);
}
SECTION("when ConditionValue is created with double then it stores and "
"retrieves correctly")
{
// Given
ConditionValue value(3.14);
// Then
REQUIRE(value.is<double>());
REQUIRE_FALSE(value.is<int>());
REQUIRE(value.as<double>() == 3.14);
}
SECTION("when ConditionValue is created with bool then it stores and "
"retrieves correctly")
{
// Given
ConditionValue value(true);
// Then
REQUIRE(value.is<bool>());
REQUIRE_FALSE(value.is<int>());
REQUIRE(value.as<bool>() == true);
}
SECTION("when ConditionValue is created with time_point then it stores and "
"retrieves correctly")
{
// Given
auto now = std::chrono::system_clock::now();
ConditionValue value(now);
// Then
REQUIRE(value.is<std::chrono::system_clock::time_point>());
REQUIRE_FALSE(value.is<int>());
REQUIRE(value.as<std::chrono::system_clock::time_point>() == now);
}
SECTION(
"when renderer has custom formatValue function then it is used correctly")
{
// Given
Renderer customRenderer{
.opEq = "=",
.opNe = "!=",
.opLt = "<",
.opLe = "<=",
.opGt = ">",
.opGe = ">=",
.opLike = "LIKE",
.opIsNull = "IS NULL",
.opIsNotNull = "IS NOT NULL",
.opAnd = "AND",
.opOr = "OR",
.groupStart = "[",
.groupEnd = "]",
.formatValue = [](const ConditionValue& v) -> std::string {
if (v.is<std::string>()) {
return "\"" + v.as<std::string>() + "\"";
} else if (v.is<const char*>()) {
return "\"" + std::string(v.as<const char*>()) + "\"";
} else if (v.is<int>()) {
return "INT:" + std::to_string(v.as<int>());
}
return "CUSTOM";
}};
auto spec = makeSpecification()
.field("name")
.equals(std::string("John"))
.field("age")
.greaterThan(30)
.build();
// When
auto result = spec->render(customRenderer);
// Then
REQUIRE(result == "[name = \"John\" AND age > INT:30]");
}
SECTION(
"when renderer has custom grouping symbols then they are used correctly")
{
// Given
Renderer customRenderer{
.opEq = "=",
.opNe = "!=",
.opLt = "<",
.opLe = "<=",
.opGt = ">",
.opGe = ">=",
.opLike = "LIKE",
.opIsNull = "IS NULL",
.opIsNotNull = "IS NOT NULL",
.opAnd = "AND",
.opOr = "OR",
.groupStart = "{",
.groupEnd = "}",
.formatValue = [](const ConditionValue& v) -> std::string {
if (v.is<std::string>()) {
return "'" + v.as<std::string>() + "'";
} else if (v.is<const char*>()) {
return "'" + std::string(v.as<const char*>()) + "'";
} else if (v.is<int>()) {
return std::to_string(v.as<int>());
}
return "'unknown'";
}};
auto spec = makeSpecification()
.field("name")
.equals(std::string("John"))
.andGroup()
.field("age")
.greaterThan(30)
.endGroup()
.build();
// When
auto result = spec->render(customRenderer);
// Then
REQUIRE(result == "{name = 'John' AND {age > 30}}");
}
SECTION(
"when renderer has custom logical operators then they are used correctly")
{
// Given
Renderer customRenderer{
.opEq = "=",
.opNe = "!=",
.opLt = "<",
.opLe = "<=",
.opGt = ">",
.opGe = ">=",
.opLike = "LIKE",
.opIsNull = "IS NULL",
.opIsNotNull = "IS NOT NULL",
.opAnd = "&&",
.opOr = "||",
.groupStart = "(",
.groupEnd = ")",
.formatValue = [](const ConditionValue& v) -> std::string {
if (v.is<std::string>()) {
return "'" + v.as<std::string>() + "'";
} else if (v.is<const char*>()) {
return "'" + std::string(v.as<const char*>()) + "'";
} else if (v.is<int>()) {
return std::to_string(v.as<int>());
} else if (v.is<bool>()) {
return v.as<bool>() ? "TRUE" : "FALSE";
}
return "'unknown'";
}};
auto spec = makeSpecification()
.orGroup()
.field("name")
.equals(std::string("John"))
.orGroup()
.field("age")
.greaterThan(30)
.field("active")
.equals(true)
.endGroup()
.endGroup()
.build();
// When
auto result = spec->render(customRenderer);
// Then
REQUIRE(result == "((name = 'John' || (age > 30 || active = TRUE)))");
}
SECTION(
"when ConditionGroup has no children then render returns empty string")
{
// Given
ConditionGroup group(LogicalOp::AND);
// When
auto result = group.render(TEST_RENDERER);
// Then
REQUIRE(result == "");
}
SECTION("when makeSpecification helper is used then it creates valid builder")
{
// Given
auto builder = makeSpecification();
// When
auto spec = builder.field("test").equals("value").build();
// Then
REQUIRE(spec != nullptr);
auto result = spec->render(TEST_RENDERER);
REQUIRE(result == "(test = 'value')");
}
}

436
cpp17/tests/unit/TaskScheduler.test.cpp

@ -0,0 +1,436 @@
#include "application/services/TaskScheduler.h"
#include "mocks/TestLogger.h"
#include "mocks/MockTimeProvider.h"
#include "mocks/MockThreadManager.h"
#include "mocks/MockBlocker.h"
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_string.hpp>
#include <memory>
#include <atomic>
using trompeloeil::_;
using namespace nxl::autostore;
using namespace std::chrono;
using nxl::autostore::application::TaskScheduler;
namespace test {
// Fixed test timepoint: 2020-01-01 12:00
constexpr std::chrono::system_clock::time_point TIMEPOINT_NOW =
std::chrono::system_clock::time_point(std::chrono::seconds(1577880000));
} // namespace test
TEST_CASE("TaskScheduler Unit Tests", "[unit][TaskScheduler]")
{
// Common mock objects that all sections can use
auto logger = std::make_shared<test::TestLogger>();
auto timeProvider = std::make_unique<test::MockTimeProvider>();
auto threadMgr = std::make_unique<test::MockThreadManager>();
auto blocker = std::make_unique<test::MockBlocker>();
SECTION("when start is called then createThread is called")
{
// Given
// Expect createThread to be called
REQUIRE_CALL(*threadMgr, createThread(_))
.RETURN(std::make_unique<test::MockThreadHandle>());
TaskScheduler scheduler(logger, *timeProvider, *threadMgr,
std::move(blocker));
// When
scheduler.start();
}
SECTION("when scheduler is created then it is not running")
{
// Given - recreate blocker for this test since it was moved in previous
// section
auto testBlocker = std::make_unique<test::MockBlocker>();
// When
TaskScheduler scheduler(logger, *timeProvider, *threadMgr,
std::move(testBlocker));
// Then - calling stop on a non-running scheduler should not cause issues
// and no thread operations should be called
FORBID_CALL(*threadMgr, createThread(_));
scheduler.stop();
}
SECTION("when task is scheduled with OnStart mode then it executes "
"immediately after start")
{
// Given
bool taskExecuted = false;
std::function<void()> threadFn;
// Recreate blocker for this test
auto testBlocker = std::make_unique<test::MockBlocker>();
// Expect createThread to be called, save thread function
REQUIRE_CALL(*threadMgr, createThread(_))
.RETURN(std::make_unique<test::MockThreadHandle>())
.LR_SIDE_EFFECT(threadFn = std::move(_1));
ALLOW_CALL(*timeProvider, now()).LR_RETURN(test::TIMEPOINT_NOW);
FORBID_CALL(*testBlocker, blockFor(_));
TaskScheduler scheduler(logger, *timeProvider, *threadMgr,
std::move(testBlocker));
auto taskFunction = [&]() {
taskExecuted = true;
scheduler.stop(); // prevent infinite loop in threadFn
};
// When
scheduler.schedule(taskFunction, 0, 0, 0, TaskScheduler::RunMode::OnStart);
scheduler.start();
threadFn();
// Then
REQUIRE(taskExecuted);
scheduler.stop();
}
SECTION(
"when task is scheduled with Once mode then it executes at specified time")
{
// Given
auto threadHandle = std::make_unique<test::MockThreadHandle>();
bool taskExecuted = false;
std::function<void()> threadFn;
auto currentTime = test::TIMEPOINT_NOW; // current "now", starts at 12:00
std::chrono::seconds timeDelta{5};
std::chrono::milliseconds actualDelay{0};
auto initialTime = test::TIMEPOINT_NOW;
auto expectedExecutionTime = initialTime + timeDelta;
// Set up thread handle expectations before moving it
ALLOW_CALL(*threadHandle, join());
ALLOW_CALL(*threadHandle, joinable()).RETURN(true);
// Recreate blocker for this test
auto testBlocker = std::make_unique<test::MockBlocker>();
// Expect createThread to be called, save thread function
REQUIRE_CALL(*threadMgr, createThread(_))
.LR_RETURN(std::move(threadHandle))
.LR_SIDE_EFFECT(threadFn = std::move(_1));
// Mock time provider calls - return initial time first, then execution time
ALLOW_CALL(*timeProvider, now()).LR_RETURN(currentTime);
// Allow blocker calls, save delay value
ALLOW_CALL(*testBlocker, blockFor(_))
.LR_SIDE_EFFECT(actualDelay += _1; currentTime += _1 // let the time flow
);
ALLOW_CALL(*testBlocker, notify());
TaskScheduler scheduler(logger, *timeProvider, *threadMgr,
std::move(testBlocker));
auto taskFunction = [&]() {
taskExecuted = true;
scheduler.stop(); // prevent infinite loop in threadFn
};
// When
scheduler.schedule(taskFunction, 12, 0, timeDelta.count(),
TaskScheduler::RunMode::Once);
scheduler.start();
// Execute the thread function to simulate the scheduler thread
threadFn();
// Then
REQUIRE(taskExecuted);
REQUIRE(actualDelay == timeDelta);
}
SECTION("when task is scheduled with Forever and OnStart mode then it "
"executes repeatedly")
{
// Given
auto threadHandle = std::make_unique<test::MockThreadHandle>();
std::function<void()> threadFn;
int executionCount = 0;
auto currentTime = test::TIMEPOINT_NOW;
// Set up thread handle expectations before moving it
ALLOW_CALL(*threadHandle, join());
ALLOW_CALL(*threadHandle, joinable()).RETURN(true);
// Recreate blocker for this test
auto testBlocker = std::make_unique<test::MockBlocker>();
// Expect createThread to be called, save thread function
REQUIRE_CALL(*threadMgr, createThread(_))
.LR_RETURN(std::move(threadHandle))
.LR_SIDE_EFFECT(threadFn = std::move(_1));
// Mock time provider calls
ALLOW_CALL(*timeProvider, now()).LR_RETURN(currentTime);
// Allow blocker calls and simulate time passage
ALLOW_CALL(*testBlocker, blockFor(_)).LR_SIDE_EFFECT(currentTime += _1);
ALLOW_CALL(*testBlocker, notify());
TaskScheduler scheduler(logger, *timeProvider, *threadMgr,
std::move(testBlocker));
auto taskFunction = [&]() {
executionCount++;
if (executionCount >= 3) {
scheduler.stop(); // stop after 3 executions
}
};
// When
scheduler.schedule(taskFunction, 0, 0, 0,
TaskScheduler::RunMode::Forever
| TaskScheduler::RunMode::OnStart);
scheduler.start();
// Execute the thread function to simulate the scheduler thread
threadFn();
// Then
REQUIRE(executionCount >= 3);
}
SECTION("when invalid time parameters are provided then exception is thrown")
{
// Given - recreate blocker for this test
auto testBlocker = std::make_unique<test::MockBlocker>();
TaskScheduler scheduler(logger, *timeProvider, *threadMgr,
std::move(testBlocker));
// When & Then - invalid hour
REQUIRE_THROWS_AS(
scheduler.schedule([]() {}, -1, 0, 0, TaskScheduler::RunMode::Once),
std::invalid_argument);
REQUIRE_THROWS_AS(
scheduler.schedule([]() {}, 24, 0, 0, TaskScheduler::RunMode::Once),
std::invalid_argument);
// When & Then - invalid minute
REQUIRE_THROWS_AS(
scheduler.schedule([]() {}, 0, -1, 0, TaskScheduler::RunMode::Once),
std::invalid_argument);
REQUIRE_THROWS_AS(
scheduler.schedule([]() {}, 0, 60, 0, TaskScheduler::RunMode::Once),
std::invalid_argument);
// When & Then - invalid second
REQUIRE_THROWS_AS(
scheduler.schedule([]() {}, 0, 0, -1, TaskScheduler::RunMode::Once),
std::invalid_argument);
REQUIRE_THROWS_AS(
scheduler.schedule([]() {}, 0, 0, 61, TaskScheduler::RunMode::Once),
std::invalid_argument);
}
SECTION("when invalid mode combination is used then exception is thrown")
{
// Given - recreate blocker for this test
auto testBlocker = std::make_unique<test::MockBlocker>();
TaskScheduler scheduler(logger, *timeProvider, *threadMgr,
std::move(testBlocker));
// When & Then
REQUIRE_THROWS_AS(scheduler.schedule([]() {}, 0, 0, 0,
TaskScheduler::RunMode::Forever
| TaskScheduler::RunMode::Once),
std::invalid_argument);
}
SECTION("when multiple tasks are scheduled then all execute")
{
// Given
auto threadHandle = std::make_unique<test::MockThreadHandle>();
std::function<void()> threadFn;
bool task1Executed = false;
bool task2Executed = false;
// Set up thread handle expectations before moving it
ALLOW_CALL(*threadHandle, join());
ALLOW_CALL(*threadHandle, joinable()).RETURN(true);
// Recreate blocker for this test
auto testBlocker = std::make_unique<test::MockBlocker>();
// Expect createThread to be called, save thread function
REQUIRE_CALL(*threadMgr, createThread(_))
.LR_RETURN(std::move(threadHandle))
.LR_SIDE_EFFECT(threadFn = std::move(_1));
// Mock time provider calls
ALLOW_CALL(*timeProvider, now()).LR_RETURN(test::TIMEPOINT_NOW);
// Allow blocker calls
ALLOW_CALL(*testBlocker, blockFor(_));
ALLOW_CALL(*testBlocker, notify());
TaskScheduler scheduler(logger, *timeProvider, *threadMgr,
std::move(testBlocker));
auto taskFunction1 = [&]() { task1Executed = true; };
auto taskFunction2 = [&]() {
task2Executed = true;
scheduler.stop(); // stop after both tasks have had a chance to execute
};
// When
scheduler.schedule(taskFunction1, 0, 0, 0, TaskScheduler::RunMode::OnStart);
scheduler.schedule(taskFunction2, 0, 0, 0, TaskScheduler::RunMode::OnStart);
scheduler.start();
// Execute the thread function to simulate the scheduler thread
threadFn();
// Then
REQUIRE(task1Executed);
REQUIRE(task2Executed);
}
SECTION("when task is scheduled with Forever mode then it repeats")
{
// Given
auto threadHandle = std::make_unique<test::MockThreadHandle>();
std::function<void()> threadFn;
int executionCount = 0;
auto currentTime = test::TIMEPOINT_NOW;
// Set up thread handle expectations before moving it
ALLOW_CALL(*threadHandle, join());
ALLOW_CALL(*threadHandle, joinable()).RETURN(true);
// Recreate blocker for this test
auto testBlocker = std::make_unique<test::MockBlocker>();
// Expect createThread to be called, save thread function
REQUIRE_CALL(*threadMgr, createThread(_))
.LR_RETURN(std::move(threadHandle))
.LR_SIDE_EFFECT(threadFn = std::move(_1));
// Mock time provider calls - simulate time advancing
ALLOW_CALL(*timeProvider, now()).LR_RETURN(currentTime);
// Allow blocker calls and simulate time passage
ALLOW_CALL(*testBlocker, blockFor(_)).LR_SIDE_EFFECT(currentTime += _1);
ALLOW_CALL(*testBlocker, notify());
TaskScheduler scheduler(logger, *timeProvider, *threadMgr,
std::move(testBlocker));
auto taskFunction = [&]() {
executionCount++;
if (executionCount >= 2) {
scheduler.stop(); // stop after 2 executions
}
};
// Schedule task to run at a specific time (not immediately) and repeat
// forever This ensures the task doesn't get stuck in an infinite OnStart
// loop
scheduler.schedule(taskFunction, 12, 0, 1, TaskScheduler::RunMode::Forever);
// When
scheduler.start();
// Execute the thread function to simulate the scheduler thread
threadFn();
// Then
REQUIRE(executionCount >= 2);
}
SECTION("when task is scheduled with Forever and OnStart mode then it "
"executes on start and at scheduled time only")
{
// Given
auto threadHandle = std::make_unique<test::MockThreadHandle>();
std::function<void()> threadFn;
std::vector<std::chrono::system_clock::time_point> executionTimes;
auto currentTime = test::TIMEPOINT_NOW; // 2020-01-01 12:00:00
// Schedule task for 12:00:05 (5 seconds after current time)
auto scheduledTimeDelta = std::chrono::seconds{5};
auto scheduledTime = currentTime + scheduledTimeDelta;
// Set up thread handle expectations before moving it
ALLOW_CALL(*threadHandle, join());
ALLOW_CALL(*threadHandle, joinable()).RETURN(true);
// Recreate blocker for this test
auto testBlocker = std::make_unique<test::MockBlocker>();
// Expect createThread to be called, save thread function
REQUIRE_CALL(*threadMgr, createThread(_))
.LR_RETURN(std::move(threadHandle))
.LR_SIDE_EFFECT(threadFn = std::move(_1));
// Mock time provider calls - simulate time advancing
ALLOW_CALL(*timeProvider, now()).LR_RETURN(currentTime);
// Also add a timeout mechanism in case the scheduler doesn't execute as
// expected
auto timeoutTime = test::TIMEPOINT_NOW + std::chrono::minutes(2);
// Allow blocker calls and simulate time passage
ALLOW_CALL(*testBlocker, blockFor(_))
.LR_SIDE_EFFECT(
// Advance time by the blocked amount
currentTime += _1;);
ALLOW_CALL(*testBlocker, notify());
TaskScheduler scheduler(logger, *timeProvider, *threadMgr,
std::move(testBlocker));
auto taskFunction = [&]() {
// Record the current time when this execution happens
executionTimes.push_back(currentTime);
// Stop after 2 executions (the expected behavior)
if (executionTimes.size() >= 2) {
scheduler.stop();
}
};
// When - schedule task with both Forever and OnStart modes
// Set time to 12:00:05 (5 seconds after our test TIMEPOINT_NOW)
scheduler.schedule(taskFunction, 12, 0, 5,
TaskScheduler::RunMode::Forever
| TaskScheduler::RunMode::OnStart);
scheduler.start();
// Execute the thread function to simulate the scheduler thread
threadFn();
// Then - task should have executed exactly twice:
// 1. Once immediately due to OnStart (at TIMEPOINT_NOW)
// 2. Once at the scheduled time (12:00:05)
// But NOT more than that (which would indicate the bug)
// With the bug, executionTimes will have many entries due to infinite loop
// Without the bug, we should have exactly 2 entries
REQUIRE(executionTimes.size() == 2);
// First execution should be at the initial time (OnStart)
REQUIRE(executionTimes[0] == test::TIMEPOINT_NOW);
// Second execution should be at or after the scheduled time
REQUIRE(executionTimes[1] >= scheduledTime);
// Verify that time has advanced appropriately
// Current time should be at or after the scheduled time
REQUIRE(currentTime >= scheduledTime);
}
}

14
cpp17/vcpkg.json

@ -0,0 +1,14 @@
{
"name": "autostore",
"version-string": "1.0.0",
"dependencies": [
"cpp-httplib",
"nlohmann-json",
"jwt-cpp",
"spdlog",
"picosha2",
"catch2",
"trompeloeil"
],
"builtin-baseline": "ef7dbf94b9198bc58f45951adcf1f041fcbc5ea0"
}

31
golang/.devcontainer/Dockerfile

@ -0,0 +1,31 @@
FROM golang:1.25.1-alpine3.22
WORKDIR /usr/src/app
# Install system dependencies
RUN apk add --no-cache \
git \
bash \
curl \
sudo
# Configure user and group IDs (default: 1000:1000)
ARG USER_ID=1000
ARG GROUP_ID=1000
# Create a group and user with specific UID/GID
RUN addgroup -g ${GROUP_ID} developer \
&& adduser -D -u ${USER_ID} -G developer -s /bin/bash developer \
&& echo "developer ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/developer \
&& chmod 0440 /etc/sudoers.d/developer
RUN chown -R ${USER_ID}:${GROUP_ID} /usr/src/app
USER developer
# Install Go tools
RUN go install github.com/go-delve/delve/cmd/dlv@latest
EXPOSE 3000
CMD ["go", "run", "main.go"]

25
golang/.devcontainer/devcontainer.json

@ -0,0 +1,25 @@
{
"name": "Go dev container",
"dockerComposeFile": "./docker-compose.yml",
"service": "app",
"workspaceFolder": "/usr/src/app",
"customizations": {
"vscode": {
"settings": {
"terminal.integrated.defaultProfile.linux": "bash",
"go.useLanguageServer": true,
"go.gopath": "/go",
"go.goroot": "/usr/local/go"
},
"extensions": [
"golang.go",
"ms-vscode.go-tools",
"ms-vscode.vscode-go",
"ms-vscode.vscode-docker"
]
}
},
"forwardPorts": [3000],
"remoteUser": "developer",
"postCreateCommand": "sudo chown -R developer:1000 /usr/src/app && go mod tidy"
}

29
golang/.devcontainer/docker-compose.yml

@ -0,0 +1,29 @@
version: "3.9"
services:
app:
build:
context: ..
dockerfile: .devcontainer/Dockerfile
args:
USER_ID: ${USER_ID:-1000}
GROUP_ID: ${GROUP_ID:-1000}
image: dev-golang-img
container_name: dev-golang
user: "developer"
volumes:
- ../:/usr/src/app:cached
- golang_modules:/go/pkg/mod
environment:
NODE_ENV: development
ports:
- "50080:3000"
networks:
- dev-network
command: sleep infinity
volumes:
golang_modules:
networks:
dev-network:
driver: bridge

183
golang/SPEC_DETAILS.md

@ -0,0 +1,183 @@
# Specification Pattern Implementation
The Specification pattern allows you to encapsulate business logic for filtering and querying data. Instead of writing SQL queries directly, you build specifications using Go code that can later be converted to SQL, used for in-memory filtering, or other purposes.
## Core Components
### 1. Basic Data Structures (`condition_spec.go`)
#### `Condition`
Represents a single comparison operation:
```go
type Condition struct {
Field string // Field name (e.g., "age", "name")
Operator string // Comparison operator (e.g., "=", ">", "IN")
Value interface{} // Value to compare against
}
```
#### `LogicalGroup`
Combines multiple conditions with logical operators:
```go
type LogicalGroup struct {
Operator string // "AND", "OR", or "NOT"
Conditions []Condition // Simple conditions
Spec *Spec // Nested specification for complex logic
}
```
#### `Spec`
The main specification container (can hold either a single condition or a logical group):
```go
type Spec struct {
Condition *Condition // For simple conditions
LogicalGroup *LogicalGroup // For complex logic
}
```
### 2. Builder Functions
These functions create specifications in a fluent, readable way:
```go
// Simple conditions
userSpec := Eq("name", "John") // name = "John"
ageSpec := Gt("age", 18) // age > 18
roleSpec := In("role", []interface{}{"admin", "moderator"}) // role IN ("admin", "moderator")
// Logical combinations
complexSpec := And(
Eq("status", "active"),
Or(
Gt("age", 21),
Eq("role", "admin"),
),
)
```
### 3. Specification Interface (`simple_specification.go`)
The `Specification[T]` interface provides methods for:
- **Evaluation**: `IsSatisfiedBy(candidate T) bool`
- **Composition**: `And()`, `Or()`, `Not()`
- **Introspection**: `GetConditions()`, `GetSpec()`
## How It Works
### 1. Building Specifications
```go
// Create a specification for active users over 18
spec := And(
Eq("status", "active"),
Gt("age", 18),
)
```
### 2. Evaluating Specifications
The `SimpleSpecification` uses reflection to evaluate conditions against Go objects:
```go
user := &User{Name: "John", Age: 25, Status: "active"}
specification := NewSimpleSpecification[*User](spec)
isMatch := specification.IsSatisfiedBy(user) // true
```
### 3. Field Access
The implementation looks for field values in this order:
1. `GetFieldName()` method (e.g., `GetAge()`)
2. `FieldName()` method (e.g., `Age()`)
3. Exported struct field
### 4. Type Comparisons
The system handles different data types intelligently:
- **Numbers**: Direct comparison with type conversion
- **Strings**: Lexicographic comparison
- **Time**: Special handling for `time.Time` and objects with `Time()` methods
- **Collections**: `IN` and `NOT IN` operations
- **Nil values**: Proper null handling
## Examples
### Simple Usage
```go
// Find all active users
activeSpec := Eq("status", "active")
spec := NewSimpleSpecification[*User](activeSpec)
users := []User{{Status: "active"}, {Status: "inactive"}}
for _, user := range users {
if spec.IsSatisfiedBy(&user) {
fmt.Println("Active user:", user.Name)
}
}
```
### Complex Logic
```go
// Find admin users OR users with high scores
complexSpec := Or(
Eq("role", "admin"),
And(
Gt("score", 90),
Eq("status", "active"),
),
)
```
### Time Comparisons
```go
// Find expired items
expiredSpec := Lte("expirationDate", time.Now())
```
## Key Benefits
1. **Type Safety**: Compile-time checking with generics
2. **Readability**: Business logic expressed in readable Go code
3. **Reusability**: Specifications can be composed and reused
4. **Testability**: Easy to test business rules in isolation
5. **Flexibility**: Can be converted to SQL, used for filtering, etc.
## Performance Considerations
- Reflection is used for field access (consider caching for high-frequency operations)
- Complex nested specifications may impact performance
- Time comparisons handle multiple formats but may be slower than direct comparisons
## Common Patterns
### Repository Integration
```go
type UserRepository interface {
FindWhere(ctx context.Context, spec Specification[*User]) ([]*User, error)
}
```
### Business Rule Encapsulation
```go
func ActiveUserSpec() *Spec {
return And(
Eq("status", "active"),
Neq("deletedAt", nil),
)
}
```
### Dynamic Query Building
```go
func BuildUserSearchSpec(filters UserFilters) *Spec {
conditions := []*Spec{}
if filters.Status != "" {
conditions = append(conditions, Eq("status", filters.Status))
}
if filters.MinAge > 0 {
conditions = append(conditions, Gte("age", filters.MinAge))
}
return And(conditions...)
}

94
golang/cmd/main.go

@ -0,0 +1,94 @@
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"autostore/internal/application/interfaces"
"autostore/internal/config"
"autostore/internal/container"
)
func main() {
// Load configuration
cfg, err := config.Load()
if err != nil {
log.Fatalf("Failed to load configuration: %v", err)
}
// Validate configuration
if err := cfg.Validate(); err != nil {
log.Fatalf("Invalid configuration: %v", err)
}
// Create dependency injection container
container := container.NewContainer(cfg)
if err := container.Initialize(); err != nil {
log.Fatalf("Failed to initialize container: %v", err)
}
// Get server and scheduler from container
server := container.GetServer()
scheduler := container.GetExpiredItemsScheduler()
logger := container.GetLogger()
// Setup graceful shutdown
shutdownComplete := make(chan struct{})
go setupGracefulShutdown(server, scheduler, logger, shutdownComplete)
// Start scheduler
if err := scheduler.Start(context.Background()); err != nil {
logger.Error(context.Background(), "Failed to start scheduler", "error", err)
log.Fatalf("Failed to start scheduler: %v", err)
}
// Start server
if err := server.Start(); err != nil {
if err == http.ErrServerClosed {
// This is expected during graceful shutdown
logger.Info(context.Background(), "Server shutdown complete")
} else {
logger.Error(context.Background(), "Server failed to start", "error", err)
log.Fatalf("Server failed to start: %v", err)
}
}
// Wait for graceful shutdown to complete
<-shutdownComplete
logger.Info(context.Background(), "Application exiting gracefully")
}
func setupGracefulShutdown(server interface {
Shutdown(ctx context.Context) error
}, scheduler interface {
Stop() error
}, logger interfaces.ILogger, shutdownComplete chan struct{}) {
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
sig := <-sigChan
logger.Info(context.Background(), "Received shutdown signal", "signal", sig)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := scheduler.Stop(); err != nil {
logger.Error(ctx, "Scheduler shutdown failed", "error", err)
} else {
logger.Info(ctx, "Scheduler shutdown completed gracefully")
}
if err := server.Shutdown(ctx); err != nil {
logger.Error(ctx, "Server shutdown failed", "error", err)
} else {
logger.Info(ctx, "Server shutdown completed gracefully")
}
// Signal that shutdown is complete
close(shutdownComplete)
}

4
golang/docker/.dockerignore

@ -0,0 +1,4 @@
.devcontainer
.git
.gitignore
README.md

33
golang/docker/Dockerfile

@ -0,0 +1,33 @@
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
# Download all dependencies
RUN go mod download
COPY . .
# Generate go.sum
RUN go mod tidy
# Run tests
RUN go test ./tests/unit -v && \
go test ./tests/integration -v
# Build the application
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main ./cmd
# Runtime stage
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/main .
EXPOSE 3000
CMD ["./main"]

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save