From cc3843d92821fb2139139ba2aba639f8a396f6f2 Mon Sep 17 00:00:00 2001 From: Gaspard Kirira Date: Tue, 31 Mar 2026 16:50:30 +0300 Subject: [PATCH] chore(release): prepare v2.1.0 --- CHANGELOG.md | 104 +++ README.md | 121 ++-- docs/README_REPL.md | 276 -------- docs/api.md | 53 ++ docs/api/async.md | 0 docs/api/config.md | 158 +++++ docs/api/http.md | 232 +++++++ docs/api/index.md | 56 ++ docs/api/json.md | 260 +++++++ docs/api/middleware.md | 227 ++++++ docs/api/p2p.md | 0 docs/api/websocket.md | 203 ++++++ docs/architecture.md | 117 ---- docs/benchmarks.md | 85 --- docs/build.md | 185 ----- docs/examples/api-key.md | 197 ++++++ docs/examples/async.md | 209 ++++++ docs/examples/auth.md | 343 +++++++++ docs/examples/batch-insert-tx.md | 268 ++++++++ docs/examples/body-limit.md | 201 ++++++ docs/examples/caching.md | 214 ++++++ docs/examples/compression.md | 177 +++++ docs/examples/cookies.md | 197 ++++++ docs/examples/cors.md | 216 ++++++ docs/examples/csrf.md | 297 ++++++++ docs/examples/db-production-guide.md | 145 ++++ docs/examples/db-quickstart.md | 87 +++ docs/examples/db-transactions.md | 153 +++++ docs/examples/db.md | 170 +++++ docs/examples/delete_user.md | 30 - docs/examples/error-handling.md | 210 ++++++ docs/examples/errors.md | 235 +++++++ docs/examples/etag.md | 215 ++++++ docs/examples/form.md | 134 ++++ docs/examples/graceful-shutdown.md | 197 ++++++ docs/examples/group-advanced-patterns.md | 198 ++++++ docs/examples/group-api.md | 177 +++++ docs/examples/group-vs-prefix.md | 389 +++++++++++ docs/examples/headers.md | 287 ++++++++ docs/examples/hello-http.md | 298 ++++++++ docs/examples/hello_routes.md | 18 - docs/examples/index.md | 642 +++++++++++++++++ docs/examples/ip-filter.md | 164 +++++ docs/examples/json-basics.md | 448 ++++++++++++ docs/examples/json-parsers.md | 165 +++++ docs/examples/json_builders_routes.md | 37 - docs/examples/jwt.md | 164 +++++ docs/examples/logging-json.md | 245 +++++++ docs/examples/middleware.md | 206 ++++++ docs/examples/migrations.md | 0 docs/examples/multipart.md | 328 +++++++++ docs/examples/notifications.md | 143 ++++ docs/examples/openapi.md | 150 ++++ docs/examples/outbox.md | 346 ++++++++++ docs/examples/overview.md | 221 ------ docs/examples/p2p-http.md | 172 +++++ docs/examples/p2p-node.md | 128 ++++ docs/examples/p2p-sync.md | 385 +++++++++++ docs/examples/post_create_user.md | 43 -- docs/examples/presence.md | 172 +++++ docs/examples/production-api-groups.md | 354 ++++++++++ docs/examples/put_update_user.md | 46 -- docs/examples/querybuilder-update.md | 190 +++++ docs/examples/rate-limit.md | 224 ++++++ docs/examples/rbac-session.md | 369 ++++++++++ docs/examples/rbac.md | 272 ++++++++ docs/examples/realtime.md | 355 ++++++++++ docs/examples/repository-crud-full.md | 311 +++++++++ docs/examples/rest-api.md | 217 ++++++ docs/examples/retry-policy.md | 133 ++++ docs/examples/routing.md | 196 ++++++ docs/examples/session-advanced.md | 212 ++++++ docs/examples/session-jwt.md | 170 +++++ docs/examples/session.md | 188 +++++ docs/examples/static-files.md | 219 ++++++ docs/examples/streaming.md | 186 +++++ docs/examples/sync.md | 648 ++++++++++++++++++ docs/examples/tx-unit-of-work.md | 220 ++++++ docs/examples/user_crud_with_validation.md | 248 ------- docs/examples/users-crud.md | 370 ++++++++++ docs/examples/validation.md | 254 +++++++ docs/examples/wal-recovery.md | 284 ++++++++ docs/examples/ws-chat.md | 181 +++++ docs/fundamentals/errors.md | 183 +++++ docs/fundamentals/index.md | 118 ++++ docs/fundamentals/lifecycle.md | 144 ++++ docs/fundamentals/logging.md | 187 +++++ docs/fundamentals/performance.md | 149 ++++ docs/fundamentals/security.md | 168 +++++ docs/guide.md | 516 ++++++++++++++ docs/index.md | 6 + docs/inference/index.md | 116 ++++ docs/inference/providers.md | 131 ++++ docs/inference/reliability.md | 238 +++++++ docs/inference/routing.md | 176 +++++ docs/inference/streaming.md | 154 +++++ docs/install.md | 106 +++ docs/installation.md | 108 --- docs/introduction.md | 153 ----- docs/modules/async/api.md | 0 docs/modules/async/asio.md | 195 ++++++ docs/modules/async/cancel.md | 160 +++++ docs/modules/async/dns.md | 135 ++++ docs/modules/async/error.md | 146 ++++ docs/modules/async/example.md | 227 ++++++ docs/modules/async/index.md | 216 ++++++ docs/modules/async/io.md | 164 +++++ docs/modules/async/scheduler.md | 222 ++++++ docs/modules/async/signal.md | 306 +++++++++ docs/modules/async/spawn.md | 101 +++ docs/modules/async/task.md | 243 +++++++ docs/modules/async/tcp.md | 308 +++++++++ docs/modules/async/threadpool.md | 226 ++++++ docs/modules/async/timer.md | 243 +++++++ docs/modules/async/udp.md | 250 +++++++ docs/modules/async/when.md | 225 ++++++ docs/modules/cache/guide.md | 0 docs/modules/cache/index.md | 275 ++++++++ docs/modules/cli.md | 304 -------- docs/modules/cli/add.md | 81 +++ docs/modules/cli/build.md | 181 +++++ docs/modules/cli/deps.md | 126 ++++ docs/modules/cli/dev.md | 131 ++++ docs/modules/cli/index.md | 188 +++++ docs/modules/cli/install.md | 109 +++ docs/modules/cli/list.md | 64 ++ docs/modules/cli/modules.md | 193 ++++++ docs/modules/cli/new.md | 129 ++++ docs/modules/cli/orm.md | 150 ++++ docs/modules/cli/p2p.md | 140 ++++ docs/modules/cli/pack.md | 118 ++++ docs/modules/cli/publish.md | 102 +++ docs/modules/cli/registry.md | 131 ++++ docs/modules/cli/remove.md | 69 ++ docs/modules/cli/repl.md | 160 +++++ docs/modules/cli/run.md | 216 ++++++ docs/modules/cli/search.md | 68 ++ docs/modules/cli/tests.md | 123 ++++ docs/modules/cli/verify.md | 138 ++++ docs/modules/conversion/error.md | 199 ++++++ docs/modules/conversion/expected.md | 172 +++++ docs/modules/conversion/index.md | 174 +++++ docs/modules/conversion/parse.md | 103 +++ docs/modules/conversion/to_bool.md | 99 +++ docs/modules/conversion/to_enum.md | 180 +++++ docs/modules/conversion/to_float.md | 174 +++++ docs/modules/conversion/to_int.md | 151 ++++ docs/modules/conversion/to_string.md | 91 +++ docs/modules/core.md | 152 ---- docs/modules/core/api.md | 211 ++++++ docs/modules/core/app.md | 249 +++++++ docs/modules/core/config.md | 217 ++++++ docs/{ => modules/core}/console.md | 16 +- docs/modules/core/example.md | 266 +++++++ docs/modules/core/executor.md | 185 +++++ docs/modules/core/experimental.md | 177 +++++ docs/modules/core/guide.md | 210 ++++++ docs/modules/core/http.md | 259 +++++++ docs/modules/core/index.md | 220 ++++++ docs/modules/core/middleware.md | 291 ++++++++ docs/modules/core/openapi.md | 149 ++++ docs/modules/core/router.md | 231 +++++++ docs/modules/core/routing.md | 315 +++++++++ docs/modules/core/server.md | 246 +++++++ docs/modules/core/session.md | 218 ++++++ docs/modules/core/threadpool.md | 201 ++++++ docs/modules/core/timers.md | 123 ++++ docs/modules/crypto/guide.md | 0 docs/modules/crypto/index.md | 242 +++++++ docs/modules/db/guide.md | 0 docs/modules/db/index.md | 457 ++++++++++++ docs/modules/deploy/guide.md | 0 docs/modules/deploy/index.md | 229 +++++++ docs/modules/json.md | 217 ------ docs/modules/json/build.md | 301 ++++++++ docs/modules/json/convert.md | 287 ++++++++ docs/modules/json/dumps.md | 251 +++++++ docs/modules/json/index.md | 263 +++++++ docs/modules/json/jpath.md | 295 ++++++++ docs/modules/json/loads.md | 256 +++++++ docs/modules/json/simple.md | 447 ++++++++++++ docs/modules/middleware/guide.md | 0 docs/modules/middleware/index.md | 400 +++++++++++ docs/modules/net/guide.md | 0 docs/modules/net/index.md | 0 docs/modules/orm.md | 323 --------- docs/modules/orm/examples.md | 283 ++++++++ docs/modules/orm/index.md | 438 ++++++++++++ docs/modules/orm/repository.md | 367 ++++++++++ docs/modules/p2p/api.md | 0 docs/modules/p2p/guide.md | 0 docs/modules/p2p/index.md | 100 +++ docs/modules/p2p_http/guide.md | 0 docs/modules/p2p_http/index.md | 99 +++ docs/modules/rix.md | 119 ---- docs/modules/sync/engine.md | 242 +++++++ docs/modules/sync/examples.md | 362 ++++++++++ docs/modules/sync/index.md | 0 docs/modules/sync/operation.md | 246 +++++++ docs/modules/sync/outbox.md | 239 +++++++ docs/modules/sync/retry_policy.md | 218 ++++++ docs/modules/sync/wal.md | 246 +++++++ docs/modules/time/guide.md | 0 docs/modules/time/index.md | 151 ++++ docs/modules/utils.md | 172 ----- docs/modules/utils/env.md | 118 ++++ docs/modules/utils/index.md | 80 +++ docs/modules/utils/logger.md | 271 ++++++++ docs/modules/utils/prettylogs.md | 206 ++++++ docs/modules/utils/result.md | 127 ++++ docs/modules/utils/string.md | 230 +++++++ docs/modules/utils/time.md | 162 +++++ docs/modules/utils/uuid.md | 122 ++++ docs/modules/utils/validation.md | 232 +++++++ docs/modules/validation/guide.md | 0 docs/modules/validation/index.md | 298 ++++++++ docs/modules/webrpc/examples.md | 308 +++++++++ docs/modules/webrpc/index.md | 156 +++++ docs/modules/websocket.md | 358 ---------- docs/modules/websocket/api.md | 0 docs/modules/websocket/example-chat.md | 0 docs/modules/websocket/guide.md | 0 docs/modules/websocket/index.md | 227 ++++++ docs/modules/ws/CLIENT_GUIDE.md | 85 --- docs/modules/ws/EXAMPLES.md | 83 --- docs/modules/ws/LONGPOLLING.md | 63 -- docs/modules/ws/TECHNICAL.md | 87 --- docs/modules/ws/vix_websocket_examples.md | 186 ----- docs/options.md | 90 --- docs/orm/batch_insert_tx.md | 71 -- docs/orm/error_handling.md | 31 - docs/orm/examples.md | 33 - docs/orm/migrations.md | 217 ------ docs/orm/overview.md | 438 ------------ docs/orm/querybuilder_update.md | 55 -- docs/orm/repository_crud_full.md | 94 --- docs/orm/users_crud.md | 68 -- docs/project-setup.md | 119 ++++ docs/quick-start.md | 150 ++-- docs/services/consulting.md | 69 ++ docs/services/index.md | 50 ++ docs/services/support.md | 55 ++ docs/services/training.md | 71 ++ docs/vix-cli-help.md | 125 ---- examples/templates/01_render_basic.cpp | 0 examples/templates/02_loop_features.cpp | 50 ++ examples/templates/03_if_else.cpp | 44 ++ examples/templates/04_layout_extends.cpp | 44 ++ examples/templates/05_include_partial.cpp | 44 ++ examples/templates/06_filters_and_escape.cpp | 50 ++ examples/templates/07_shop_dashboard.cpp | 88 +++ examples/templates/08_blog_home.cpp | 97 +++ examples/templates/09_blog_post_page.cpp | 105 +++ examples/templates/10_docs_page.cpp | 136 ++++ .../templates/11_marketing_landing_page.cpp | 121 ++++ examples/templates/12_admin_dashboard.cpp | 185 +++++ examples/templates/README.md | 9 + examples/templates/basic_render.cpp | 46 -- examples/templates/filters.cpp | 44 -- examples/templates/includes.cpp | 65 -- examples/templates/layout_inheritance.cpp | 83 --- examples/templates/loops_and_conditions.cpp | 55 -- examples/templates/render_file.cpp | 44 -- examples/templates/views/01_basic/index.html | 11 + examples/templates/views/02_loop/index.html | 16 + examples/templates/views/03_if/index.html | 14 + examples/templates/views/04_extends/base.html | 16 + .../templates/views/04_extends/index.html | 6 + .../templates/views/05_include/header.html | 3 + .../templates/views/05_include/index.html | 12 + .../templates/views/06_filters/index.html | 15 + .../views/07_shop_dashboard/base.html | 79 +++ .../views/07_shop_dashboard/dashboard.html | 61 ++ .../views/07_shop_dashboard/header.html | 6 + .../templates/views/08_blog_home/base.html | 82 +++ .../templates/views/08_blog_home/header.html | 6 + .../templates/views/08_blog_home/index.html | 35 + .../views/09_blog_post_page/base.html | 71 ++ .../views/09_blog_post_page/header.html | 6 + .../views/09_blog_post_page/post.html | 53 ++ .../templates/views/10_docs_page/base.html | 85 +++ .../templates/views/10_docs_page/docs.html | 55 ++ .../templates/views/10_docs_page/header.html | 6 + .../views/11_marketing_landing_page/base.html | 124 ++++ .../11_marketing_landing_page/header.html | 11 + .../11_marketing_landing_page/index.html | 73 ++ .../views/12_admin_dashboard/base.html | 139 ++++ .../views/12_admin_dashboard/dashboard.html | 130 ++++ .../views/12_admin_dashboard/header.html | 12 + modules/async | 2 +- modules/core | 2 +- modules/template | 2 +- 292 files changed, 41276 insertions(+), 5392 deletions(-) delete mode 100644 docs/README_REPL.md create mode 100644 docs/api.md create mode 100644 docs/api/async.md create mode 100644 docs/api/config.md create mode 100644 docs/api/http.md create mode 100644 docs/api/index.md create mode 100644 docs/api/json.md create mode 100644 docs/api/middleware.md create mode 100644 docs/api/p2p.md create mode 100644 docs/api/websocket.md delete mode 100644 docs/architecture.md delete mode 100644 docs/benchmarks.md delete mode 100644 docs/build.md create mode 100644 docs/examples/api-key.md create mode 100644 docs/examples/async.md create mode 100644 docs/examples/auth.md create mode 100644 docs/examples/batch-insert-tx.md create mode 100644 docs/examples/body-limit.md create mode 100644 docs/examples/caching.md create mode 100644 docs/examples/compression.md create mode 100644 docs/examples/cookies.md create mode 100644 docs/examples/cors.md create mode 100644 docs/examples/csrf.md create mode 100644 docs/examples/db-production-guide.md create mode 100644 docs/examples/db-quickstart.md create mode 100644 docs/examples/db-transactions.md create mode 100644 docs/examples/db.md delete mode 100644 docs/examples/delete_user.md create mode 100644 docs/examples/error-handling.md create mode 100644 docs/examples/errors.md create mode 100644 docs/examples/etag.md create mode 100644 docs/examples/form.md create mode 100644 docs/examples/graceful-shutdown.md create mode 100644 docs/examples/group-advanced-patterns.md create mode 100644 docs/examples/group-api.md create mode 100644 docs/examples/group-vs-prefix.md create mode 100644 docs/examples/headers.md create mode 100644 docs/examples/hello-http.md delete mode 100644 docs/examples/hello_routes.md create mode 100644 docs/examples/index.md create mode 100644 docs/examples/ip-filter.md create mode 100644 docs/examples/json-basics.md create mode 100644 docs/examples/json-parsers.md delete mode 100644 docs/examples/json_builders_routes.md create mode 100644 docs/examples/jwt.md create mode 100644 docs/examples/logging-json.md create mode 100644 docs/examples/middleware.md create mode 100644 docs/examples/migrations.md create mode 100644 docs/examples/multipart.md create mode 100644 docs/examples/notifications.md create mode 100644 docs/examples/openapi.md create mode 100644 docs/examples/outbox.md delete mode 100644 docs/examples/overview.md create mode 100644 docs/examples/p2p-http.md create mode 100644 docs/examples/p2p-node.md create mode 100644 docs/examples/p2p-sync.md delete mode 100644 docs/examples/post_create_user.md create mode 100644 docs/examples/presence.md create mode 100644 docs/examples/production-api-groups.md delete mode 100644 docs/examples/put_update_user.md create mode 100644 docs/examples/querybuilder-update.md create mode 100644 docs/examples/rate-limit.md create mode 100644 docs/examples/rbac-session.md create mode 100644 docs/examples/rbac.md create mode 100644 docs/examples/realtime.md create mode 100644 docs/examples/repository-crud-full.md create mode 100644 docs/examples/rest-api.md create mode 100644 docs/examples/retry-policy.md create mode 100644 docs/examples/routing.md create mode 100644 docs/examples/session-advanced.md create mode 100644 docs/examples/session-jwt.md create mode 100644 docs/examples/session.md create mode 100644 docs/examples/static-files.md create mode 100644 docs/examples/streaming.md create mode 100644 docs/examples/sync.md create mode 100644 docs/examples/tx-unit-of-work.md delete mode 100644 docs/examples/user_crud_with_validation.md create mode 100644 docs/examples/users-crud.md create mode 100644 docs/examples/validation.md create mode 100644 docs/examples/wal-recovery.md create mode 100644 docs/examples/ws-chat.md create mode 100644 docs/fundamentals/errors.md create mode 100644 docs/fundamentals/index.md create mode 100644 docs/fundamentals/lifecycle.md create mode 100644 docs/fundamentals/logging.md create mode 100644 docs/fundamentals/performance.md create mode 100644 docs/fundamentals/security.md create mode 100644 docs/guide.md create mode 100644 docs/index.md create mode 100644 docs/inference/index.md create mode 100644 docs/inference/providers.md create mode 100644 docs/inference/reliability.md create mode 100644 docs/inference/routing.md create mode 100644 docs/inference/streaming.md create mode 100644 docs/install.md delete mode 100644 docs/installation.md delete mode 100644 docs/introduction.md create mode 100644 docs/modules/async/api.md create mode 100644 docs/modules/async/asio.md create mode 100644 docs/modules/async/cancel.md create mode 100644 docs/modules/async/dns.md create mode 100644 docs/modules/async/error.md create mode 100644 docs/modules/async/example.md create mode 100644 docs/modules/async/index.md create mode 100644 docs/modules/async/io.md create mode 100644 docs/modules/async/scheduler.md create mode 100644 docs/modules/async/signal.md create mode 100644 docs/modules/async/spawn.md create mode 100644 docs/modules/async/task.md create mode 100644 docs/modules/async/tcp.md create mode 100644 docs/modules/async/threadpool.md create mode 100644 docs/modules/async/timer.md create mode 100644 docs/modules/async/udp.md create mode 100644 docs/modules/async/when.md create mode 100644 docs/modules/cache/guide.md create mode 100644 docs/modules/cache/index.md delete mode 100644 docs/modules/cli.md create mode 100644 docs/modules/cli/add.md create mode 100644 docs/modules/cli/build.md create mode 100644 docs/modules/cli/deps.md create mode 100644 docs/modules/cli/dev.md create mode 100644 docs/modules/cli/index.md create mode 100644 docs/modules/cli/install.md create mode 100644 docs/modules/cli/list.md create mode 100644 docs/modules/cli/modules.md create mode 100644 docs/modules/cli/new.md create mode 100644 docs/modules/cli/orm.md create mode 100644 docs/modules/cli/p2p.md create mode 100644 docs/modules/cli/pack.md create mode 100644 docs/modules/cli/publish.md create mode 100644 docs/modules/cli/registry.md create mode 100644 docs/modules/cli/remove.md create mode 100644 docs/modules/cli/repl.md create mode 100644 docs/modules/cli/run.md create mode 100644 docs/modules/cli/search.md create mode 100644 docs/modules/cli/tests.md create mode 100644 docs/modules/cli/verify.md create mode 100644 docs/modules/conversion/error.md create mode 100644 docs/modules/conversion/expected.md create mode 100644 docs/modules/conversion/index.md create mode 100644 docs/modules/conversion/parse.md create mode 100644 docs/modules/conversion/to_bool.md create mode 100644 docs/modules/conversion/to_enum.md create mode 100644 docs/modules/conversion/to_float.md create mode 100644 docs/modules/conversion/to_int.md create mode 100644 docs/modules/conversion/to_string.md delete mode 100644 docs/modules/core.md create mode 100644 docs/modules/core/api.md create mode 100644 docs/modules/core/app.md create mode 100644 docs/modules/core/config.md rename docs/{ => modules/core}/console.md (97%) create mode 100644 docs/modules/core/example.md create mode 100644 docs/modules/core/executor.md create mode 100644 docs/modules/core/experimental.md create mode 100644 docs/modules/core/guide.md create mode 100644 docs/modules/core/http.md create mode 100644 docs/modules/core/index.md create mode 100644 docs/modules/core/middleware.md create mode 100644 docs/modules/core/openapi.md create mode 100644 docs/modules/core/router.md create mode 100644 docs/modules/core/routing.md create mode 100644 docs/modules/core/server.md create mode 100644 docs/modules/core/session.md create mode 100644 docs/modules/core/threadpool.md create mode 100644 docs/modules/core/timers.md create mode 100644 docs/modules/crypto/guide.md create mode 100644 docs/modules/crypto/index.md create mode 100644 docs/modules/db/guide.md create mode 100644 docs/modules/db/index.md create mode 100644 docs/modules/deploy/guide.md create mode 100644 docs/modules/deploy/index.md delete mode 100644 docs/modules/json.md create mode 100644 docs/modules/json/build.md create mode 100644 docs/modules/json/convert.md create mode 100644 docs/modules/json/dumps.md create mode 100644 docs/modules/json/index.md create mode 100644 docs/modules/json/jpath.md create mode 100644 docs/modules/json/loads.md create mode 100644 docs/modules/json/simple.md create mode 100644 docs/modules/middleware/guide.md create mode 100644 docs/modules/middleware/index.md create mode 100644 docs/modules/net/guide.md create mode 100644 docs/modules/net/index.md delete mode 100644 docs/modules/orm.md create mode 100644 docs/modules/orm/examples.md create mode 100644 docs/modules/orm/index.md create mode 100644 docs/modules/orm/repository.md create mode 100644 docs/modules/p2p/api.md create mode 100644 docs/modules/p2p/guide.md create mode 100644 docs/modules/p2p/index.md create mode 100644 docs/modules/p2p_http/guide.md create mode 100644 docs/modules/p2p_http/index.md delete mode 100644 docs/modules/rix.md create mode 100644 docs/modules/sync/engine.md create mode 100644 docs/modules/sync/examples.md create mode 100644 docs/modules/sync/index.md create mode 100644 docs/modules/sync/operation.md create mode 100644 docs/modules/sync/outbox.md create mode 100644 docs/modules/sync/retry_policy.md create mode 100644 docs/modules/sync/wal.md create mode 100644 docs/modules/time/guide.md create mode 100644 docs/modules/time/index.md delete mode 100644 docs/modules/utils.md create mode 100644 docs/modules/utils/env.md create mode 100644 docs/modules/utils/index.md create mode 100644 docs/modules/utils/logger.md create mode 100644 docs/modules/utils/prettylogs.md create mode 100644 docs/modules/utils/result.md create mode 100644 docs/modules/utils/string.md create mode 100644 docs/modules/utils/time.md create mode 100644 docs/modules/utils/uuid.md create mode 100644 docs/modules/utils/validation.md create mode 100644 docs/modules/validation/guide.md create mode 100644 docs/modules/validation/index.md create mode 100644 docs/modules/webrpc/examples.md create mode 100644 docs/modules/webrpc/index.md delete mode 100644 docs/modules/websocket.md create mode 100644 docs/modules/websocket/api.md create mode 100644 docs/modules/websocket/example-chat.md create mode 100644 docs/modules/websocket/guide.md create mode 100644 docs/modules/websocket/index.md delete mode 100644 docs/modules/ws/CLIENT_GUIDE.md delete mode 100644 docs/modules/ws/EXAMPLES.md delete mode 100644 docs/modules/ws/LONGPOLLING.md delete mode 100644 docs/modules/ws/TECHNICAL.md delete mode 100644 docs/modules/ws/vix_websocket_examples.md delete mode 100644 docs/options.md delete mode 100644 docs/orm/batch_insert_tx.md delete mode 100644 docs/orm/error_handling.md delete mode 100644 docs/orm/examples.md delete mode 100644 docs/orm/migrations.md delete mode 100644 docs/orm/overview.md delete mode 100644 docs/orm/querybuilder_update.md delete mode 100644 docs/orm/repository_crud_full.md delete mode 100644 docs/orm/users_crud.md create mode 100644 docs/project-setup.md create mode 100644 docs/services/consulting.md create mode 100644 docs/services/index.md create mode 100644 docs/services/support.md create mode 100644 docs/services/training.md delete mode 100644 docs/vix-cli-help.md create mode 100644 examples/templates/01_render_basic.cpp create mode 100644 examples/templates/02_loop_features.cpp create mode 100644 examples/templates/03_if_else.cpp create mode 100644 examples/templates/04_layout_extends.cpp create mode 100644 examples/templates/05_include_partial.cpp create mode 100644 examples/templates/06_filters_and_escape.cpp create mode 100644 examples/templates/07_shop_dashboard.cpp create mode 100644 examples/templates/08_blog_home.cpp create mode 100644 examples/templates/09_blog_post_page.cpp create mode 100644 examples/templates/10_docs_page.cpp create mode 100644 examples/templates/11_marketing_landing_page.cpp create mode 100644 examples/templates/12_admin_dashboard.cpp create mode 100644 examples/templates/README.md delete mode 100644 examples/templates/basic_render.cpp delete mode 100644 examples/templates/filters.cpp delete mode 100644 examples/templates/includes.cpp delete mode 100644 examples/templates/layout_inheritance.cpp delete mode 100644 examples/templates/loops_and_conditions.cpp delete mode 100644 examples/templates/render_file.cpp create mode 100644 examples/templates/views/01_basic/index.html create mode 100644 examples/templates/views/02_loop/index.html create mode 100644 examples/templates/views/03_if/index.html create mode 100644 examples/templates/views/04_extends/base.html create mode 100644 examples/templates/views/04_extends/index.html create mode 100644 examples/templates/views/05_include/header.html create mode 100644 examples/templates/views/05_include/index.html create mode 100644 examples/templates/views/06_filters/index.html create mode 100644 examples/templates/views/07_shop_dashboard/base.html create mode 100644 examples/templates/views/07_shop_dashboard/dashboard.html create mode 100644 examples/templates/views/07_shop_dashboard/header.html create mode 100644 examples/templates/views/08_blog_home/base.html create mode 100644 examples/templates/views/08_blog_home/header.html create mode 100644 examples/templates/views/08_blog_home/index.html create mode 100644 examples/templates/views/09_blog_post_page/base.html create mode 100644 examples/templates/views/09_blog_post_page/header.html create mode 100644 examples/templates/views/09_blog_post_page/post.html create mode 100644 examples/templates/views/10_docs_page/base.html create mode 100644 examples/templates/views/10_docs_page/docs.html create mode 100644 examples/templates/views/10_docs_page/header.html create mode 100644 examples/templates/views/11_marketing_landing_page/base.html create mode 100644 examples/templates/views/11_marketing_landing_page/header.html create mode 100644 examples/templates/views/11_marketing_landing_page/index.html create mode 100644 examples/templates/views/12_admin_dashboard/base.html create mode 100644 examples/templates/views/12_admin_dashboard/dashboard.html create mode 100644 examples/templates/views/12_admin_dashboard/header.html diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a233f9..0172405 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,110 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 --- ## [Unreleased] +## [v2.1.0] + +This release focuses on performance, developer experience, and ecosystem maturity. + +Vix is no longer just fast. +It is now structured, documented, and ready to be used in real projects. + +--- + +### Performance + +- optimized HTTP hot path in core runtime +- improved scheduler and task execution model +- reduced overhead in run queue processing +- optimized async coroutine fast-path +- removed redundant error handling in async layer + +Result: +- lower latency +- better throughput +- more predictable execution under load + +--- + +### Templates + +- complete refactor of template examples +- introduction of real-world use cases (dashboard, blog, marketing pages) +- improved layout system (extends, includes, filters) + +New examples include: +- shop dashboard +- blog home and post pages +- admin dashboards +- marketing landing pages + +--- + +### Documentation + +- massive addition of documentation across the entire ecosystem +- new structured docs for: + - core modules + - async + - cache + - p2p and sync + - middleware and HTTP + - database and ORM +- detailed real-world examples (auth, JWT, caching, rate limit, etc.) + +Result: +- significantly improved onboarding +- clearer mental model of Vix architecture +- easier adoption for new developers + +--- + +### Examples + +#### Added +- structured template examples (01 → 12 progression) +- new real-world scenarios across modules +- improved consistency across all example categories + +#### Removed +- removed outdated and legacy template examples +- cleaned old and unstructured demo files + +--- + +### Core & Modules + +- improvements across: + - core + - async + - template +- better internal consistency and structure +- improved maintainability for future features + +--- + +### Developer Experience + +- examples now follow a progressive learning path +- documentation aligned with real usage patterns +- clearer separation between basic and advanced concepts + +--- + +### Stability + +- improved runtime reliability +- better performance under load +- no breaking changes + +--- + +### Summary + +v2.1.0 marks a major step toward making Vix a complete developer platform. + +From performance to documentation to real-world examples, +Vix is now designed to be learned, used, and extended. + ## [v2.0.0] - 2026-03-31 Vix.cpp 2.0 starts here. diff --git a/README.md b/README.md index 017c781..4534251 100644 --- a/README.md +++ b/README.md @@ -1,71 +1,51 @@ - - - -
+

Vix.cpp

- X + - YouTube + - Stars - Forks - CI

- Vix.cpp is a modern C++ runtime for building HTTP, WebSocket, and P2P applications with - predictable performance, offline-first design, and a - Node/Deno-like developer experience. + A modern C++ runtime for real-world systems.

- 🌍 vixcpp.com
- 📘 Documentation + Build HTTP, WebSocket, and peer-to-peer applications with + predictable performance and offline-first reliability.

-
- Vix.cpp Logo -
- -
- -## Performance is not a feature it’s a requirement +

+ 🌍 Website · + 📘 Docs · + ⬇️ Download +

-Vix.cpp is designed to remove overhead, unpredictability, and GC pauses. + -### ⚡ Benchmarks (Dec 2025) + -| Framework | Requests/sec | Avg Latency | -| --------------------------- | ------------ | ----------- | -| ⭐ **Vix.cpp (pinned CPU)** | **~99,000** | 7–10 ms | -| Vix.cpp (default) | ~81,400 | 9–11 ms | -| Go (Fiber) | ~81,300 | ~0.6 ms | -| Deno | ~48,800 | ~16 ms | -| Node.js (Fastify) | ~4,200 | ~16 ms | -| PHP (Slim) | ~2,800 | ~17 ms | -| FastAPI (Python) | ~750 | ~64 ms | + -## Installation + + + -Install the Vix runtime on your system using one of the commands below. -Note that there are multiple ways to install Vix. +## Install #### Linux -**Ubuntu / Debian deps (example):** - ```bash sudo apt update sudo apt install -y \ @@ -149,27 +129,64 @@ Run C++ like a script: ```bash vix run main.cpp -vix dev main.cpp ``` -Vix handles compilation, linking, and execution automatically. +Open http://localhost:8080 + +## Why Vix.cpp + +Most systems assume perfect conditions. +Vix is built for when things are not. + +- predictable under load +- no GC pauses +- offline-first by design +- deterministic execution +- minimal setup + +--- + +## Performance + +Stable under sustained load. + +| Metric | Value | +|--------------|----------------| +| Requests/sec | ~66k – 68k | +| Avg Latency | ~13–20 ms | +| P99 Latency | ~17–50 ms | + +--- + +## Core principles + +- Local-first execution +- Network is optional +- Deterministic behavior +- Failure-tolerant +- Built for unreliable environments + +--- ## Learn more -- 📘 Docs: https://vixcpp.com/docs -- 🌍 Website: https://vixcpp.com -- 📦 Registry: https://vixcpp.com/registry -- 📦 Examples: https://vixcpp.com/docs/examples +- Docs: https://vixcpp.com/docs +- Registry: https://vixcpp.com/registry +- Examples: https://vixcpp.com/docs/examples + --- ## Contributing Contributions are welcome. -If you care about modern C++, performance, and real-world reliability, you’ll feel at home here. -Please read the contributing guide before opening a PR. + +### Focus areas + +- performance +- reliability +- networking +- offline-first systems --- -⭐ If this project resonates with you, consider starring the repository. MIT License - diff --git a/docs/README_REPL.md b/docs/README_REPL.md deleted file mode 100644 index bee4303..0000000 --- a/docs/README_REPL.md +++ /dev/null @@ -1,276 +0,0 @@ -# 🧠 Vix REPL — Interactive Runtime Shell - -The **Vix REPL** is an interactive shell built directly into the `vix` binary. -Just like **python**, **node**, or **deno**, you start it simply by typing: - -```bash -vix -``` - -No subcommand. No flags. -This is the **default interactive mode** of Vix. - ---- - -## ✨ What is the Vix REPL? - -The Vix REPL is a **developer-friendly interactive environment** designed to: - -- Experiment with C++-like expressions -- Test runtime logic quickly -- Evaluate math expressions -- Manipulate variables and JSON data -- Call built-in Vix runtime APIs -- Prototype logic before moving to real code - -It feels familiar if you’ve used: - -- Python REPL -- Node.js REPL -- Deno REPL - -…but adapted to the **Vix.cpp philosophy**. - ---- - -## ▶️ Starting the REPL - -```bash -vix -``` - -Example startup: - -``` -Vix.cpp v1.x (CLI) — Modern C++ backend runtime -[GCC 13.3.0] on linux -Exit: Ctrl+C / Ctrl+D | Clear: Ctrl+L | Type help for help -vix> -``` - ---- - -## 🧮 Math Expressions - -You can type expressions directly: - -```text -1 + 2 -10 * (3 + 4) -``` - -With variables: - -```text -x = 3 -x + 1 -x * 10 -``` - ---- - -## 📦 Variables - -### Assign values - -```text -x = 42 -name = "Gaspard" -``` - -### Print variables - -```text -x -name -``` - ---- - -## 🧩 JSON Support - -The REPL supports **strict JSON** using `nlohmann::json`. - -### 1. Simple Objects -```text -user = {"name":"Gaspard","age":10} -``` - -### 2. Arrays -```text -items = [1, 2, 3] -``` - -### 3. Nested Objects & Arrays -```text -profile = { - "name": "Gaspard", - "meta": { "country": "UG", "verified": true }, - "tags": ["cpp", "vix", "repl"] -} -``` - -### 4. Array of Objects -```text -users = [ - { "id": 1, "name": "Alice" }, - { "id": 2, "name": "Bob" } -] -``` - -### 5. Mixed Types -```text -config = { - "active": true, - "threshold": 3.14, - "backup": null -} -``` - -### ❓ Troubleshooting & Common Errors - -The JSON parser is **strict**. Here are common syntax mistakes: - -| Error Type | Invalid Syntax ❌ | Correct Syntax ✅ | -| :--- | :--- | :--- | -| **Missing Colon** | `{"name" "Gaspard"}` | `{"name": "Gaspard"}` | -| **Comma instead of Colon** | `{"name", "Gaspard"}` | `{"name": "Gaspard"}` | -| **Trailing Comma** | `{"a": 1,}` | `{"a": 1}` | -| **Single Quotes** | `{'name': 'Gaspard'}` | `{"name": "Gaspard"}` | - ---- - -## 🖨️ print / println - -### Basic output - -```text -print("Hello") -println("Hello world") -``` - -### Mix strings and expressions - -```text -x = 3 -println("x =", x) -println("x+1 =", x+1) -``` - ---- - -## ⚙️ Built-in Vix API - -The REPL exposes a built-in `Vix` object. - -### Working directory - -```text -cwd() -Vix.cwd() -``` - -### Change directory - -```text -Vix.cd("..") -``` - -### Process info - -```text -pid() -Vix.pid() -``` - -### Environment variables - -```text -Vix.env("HOME") -Vix.env("PATH") -``` - -### Arguments - -```text -Vix.args() -``` - ---- - -## 🛠️ Filesystem helpers - -```text -Vix.mkdir("tmp") -Vix.mkdir("tmp/logs", true) -``` - ---- - -## ▶️ Running CLI commands - -You can run CLI commands **from inside the REPL**: - -```text -Vix.run("version") -Vix.run("help") -Vix.run("check", "--help") -``` - ---- - -## 🧹 Session control - -### Clear screen - -```text -clear -``` - -or: - -```text -Ctrl + L -``` - -### Exit REPL - -```text -exit -``` - -or: - -```text -Ctrl + D -Ctrl + C -``` - ---- - -## 🧠 Tips & Best Practices - -- Use the REPL to **prototype logic** -- Validate math & JSON before writing C++ -- Use `println()` for debugging expressions -- Treat the REPL as your **scratchpad** - ---- - -## 🧭 Roadmap (REPL) - -Planned features: - -- Property access: `user.name` -- Function definitions -- History persistence -- Autocomplete for variables -- Structured error hints -- Module imports - ---- - -## 🧾 License - -MIT License © Gaspard Kirira -Part of the **Vix.cpp** ecosystem diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 0000000..7796249 --- /dev/null +++ b/docs/api.md @@ -0,0 +1,53 @@ +# API Reference + +This section documents the public API surface of Vix. + +It focuses on: + +- Core HTTP primitives +- Routing +- Request & Response +- JSON layer +- WebSocket +- Configuration +- Runtime behavior + +The goal of this section is precision. + +If the Guide shows *how* to use Vix, the API section shows *what exactly +exists*. + +------------------------------------------------------------------------ + +## Structure + +- App +- Request +- Response +- Routing methods +- JSON helpers +- WebSocket server +- Config loader + +Each page in this section describes: + +- Available methods +- Signatures +- Minimal usage examples +- Behavioral notes + +------------------------------------------------------------------------ + +## Philosophy + +The Vix API follows strict principles: + +- Explicit over magic +- Minimal abstraction layers +- Deterministic behavior +- No hidden global state +- No runtime reflection + +Everything that happens should be visible in code. + + diff --git a/docs/api/async.md b/docs/api/async.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/api/config.md b/docs/api/config.md new file mode 100644 index 0000000..f9a974b --- /dev/null +++ b/docs/api/config.md @@ -0,0 +1,158 @@ +# Config API + +This page documents the configuration loader in Vix. + +Header: + +``` cpp +#include +``` + +Core type: + +``` cpp +vix::config::Config +``` + +The configuration system is: + +- JSON-based +- Strongly typed +- Path-addressable via dot notation +- Safe with fallback defaults + +------------------------------------------------------------------------ + +# Constructor + +``` cpp +vix::config::Config cfg{"config.json"}; +``` + +Loads and parses the JSON file at construction. + +If the file cannot be loaded, behavior depends on implementation +configuration. In production, always validate at boot time. + +------------------------------------------------------------------------ + +# Getters + +All getters accept: + +- dot path string +- fallback value + +------------------------------------------------------------------------ + +## get_string + +``` cpp +std::string value = cfg.get_string("server.host", "127.0.0.1"); +``` + +------------------------------------------------------------------------ + +## get_int + +``` cpp +int port = cfg.get_int("server.port", 8080); +``` + +------------------------------------------------------------------------ + +## get_bool + +``` cpp +bool debug = cfg.get_bool("runtime.debug", false); +``` + +------------------------------------------------------------------------ + +## get_double + +``` cpp +double ratio = cfg.get_double("limits.ratio", 0.5); +``` + +------------------------------------------------------------------------ + +# Dot Path Access + +Nested JSON can be accessed using dot notation. + +Given: + +``` json +{ + "database": { + "host": "localhost", + "port": 5432 + } +} +``` + +Access: + +``` cpp +cfg.get_string("database.host", "127.0.0.1"); +cfg.get_int("database.port", 3306); +``` + +------------------------------------------------------------------------ + +# Minimal Usage Example + +``` cpp +#include +#include + +int main() +{ + vix::config::Config cfg{"config.json"}; + + std::string host = cfg.get_string("server.host", "0.0.0.0"); + int port = cfg.get_int("server.port", 8080); + + std::cout << host << ":" << port << "\n"; + + return 0; +} +``` + +------------------------------------------------------------------------ + +# Environment Override Pattern + +Configuration files should not contain secrets in production. + +Example override: + +``` cpp +#include +#include + +int main() +{ + vix::config::Config cfg{"config.json"}; + + int port = cfg.get_int("server.port", 8080); + + if (const char* env = std::getenv("PORT")) + port = std::atoi(env); + + return port; +} +``` + +------------------------------------------------------------------------ + +# Design Notes + +- Configuration is read at startup. +- No global mutable config state. +- Safe access always requires fallback. +- Deterministic behavior: no implicit environment merging. + +The Config API is intentionally small and predictable. + diff --git a/docs/api/http.md b/docs/api/http.md new file mode 100644 index 0000000..b5cfee4 --- /dev/null +++ b/docs/api/http.md @@ -0,0 +1,232 @@ +# HTTP API + +This page documents the core HTTP primitives in Vix. + +It covers: + +- App +- Routing methods +- Request +- Response + +All examples are minimal and placed entirely inside `main()`. + +------------------------------------------------------------------------ + +# App + +Header: + +``` cpp +#include +``` + +Core type: + +``` cpp +vix::App +``` + +### Constructor + +``` cpp +App app; +``` + +### Run + +``` cpp +app.run(8080); +``` + +Starts the HTTP server on the given port (blocking call). + +------------------------------------------------------------------------ + +# Routing Methods + +## GET + +``` cpp +app.get(path, handler); +``` + +## POST + +``` cpp +app.post(path, handler); +``` + +## PUT + +``` cpp +app.put(path, handler); +``` + +## DELETE + +``` cpp +app.del(path, handler); +``` + +------------------------------------------------------------------------ + +## Minimal Example + +``` cpp +#include + +using namespace vix; + +int main() +{ + App app; + + app.get("/", [](Request&, Response& res) { + res.send("Hello"); + }); + + app.run(8080); +} +``` + +------------------------------------------------------------------------ + +# Request + +Type: + +``` cpp +vix::Request +``` + +Provides access to: + +- Path parameters +- Query parameters +- Headers +- Body +- JSON payload + +------------------------------------------------------------------------ + +## Path parameter + +``` cpp +req.param("id", "0"); +``` + +Returns string value or fallback. + +------------------------------------------------------------------------ + +## Query parameter + +``` cpp +req.query_value("page", "1"); +``` + +------------------------------------------------------------------------ + +## Headers + +``` cpp +req.header("User-Agent"); +req.has_header("Authorization"); +``` + +------------------------------------------------------------------------ + +## Body + +``` cpp +std::string body = req.body(); +``` + +------------------------------------------------------------------------ + +## JSON + +``` cpp +const auto& j = req.json(); +``` + +Returns parsed JSON (high-level JSON layer). + +------------------------------------------------------------------------ + +# Response + +Type: + +``` cpp +vix::Response +``` + +Controls HTTP output. + +------------------------------------------------------------------------ + +## Status + +``` cpp +res.status(201); +res.set_status(404); +``` + +------------------------------------------------------------------------ + +## Send text + +``` cpp +res.send("Hello"); +``` + +------------------------------------------------------------------------ + +## Send JSON + +``` cpp +res.json({"message", "ok"}); +``` + +------------------------------------------------------------------------ + +## Auto-send return + +If a route handler returns: + +- `std::string` → text response +- JSON object → JSON response + +Example: + +``` cpp +app.get("/auto", [](Request&, Response&) { + return vix::json::o("message", "auto"); +}); +``` + +------------------------------------------------------------------------ + +# Behavior Notes + +- If `res.send()` or `res.json()` is called, returned values are + ignored. +- If nothing is sent, the handler must return a value. +- Status defaults to 200 unless changed. +- Handlers execute synchronously per request context. + +------------------------------------------------------------------------ + +# Design Philosophy + +The HTTP layer is: + +- Minimal +- Explicit +- Deterministic +- Zero hidden middleware stack by default + +You control exactly what happens in each handler. + diff --git a/docs/api/index.md b/docs/api/index.md new file mode 100644 index 0000000..11fca02 --- /dev/null +++ b/docs/api/index.md @@ -0,0 +1,56 @@ +# API Reference + +This section documents the public API surface of Vix. + +It focuses on: + +- Core HTTP primitives +- Routing +- Request & Response +- JSON layer +- WebSocket +- Configuration +- Runtime behavior + +The goal of this section is precision. + +If the Guide shows *how* to use Vix, the API section shows *what exactly +exists*. + +------------------------------------------------------------------------ + +## Structure + +- App +- Request +- Response +- Routing methods +- JSON helpers +- WebSocket server +- Config loader + +Each page in this section describes: + +- Available methods +- Signatures +- Minimal usage examples +- Behavioral notes + +------------------------------------------------------------------------ + +## Philosophy + +The Vix API follows strict principles: + +- Explicit over magic +- Minimal abstraction layers +- Deterministic behavior +- No hidden global state +- No runtime reflection + +Everything that happens should be visible in code. + +------------------------------------------------------------------------ + +Continue with the next API component page. + diff --git a/docs/api/json.md b/docs/api/json.md new file mode 100644 index 0000000..915a12a --- /dev/null +++ b/docs/api/json.md @@ -0,0 +1,260 @@ +# JSON API + +This page documents the JSON facilities available in Vix. + +Vix provides two JSON layers: + +1) Simple JSON (`vix/json/Simple.hpp`) +2) High-level JSON (`vix/json/json.hpp`) + +They serve different purposes and can coexist. + +------------------------------------------------------------------------ + +# 1) Simple JSON + +Header: + +``` cpp +#include +``` + +Namespace: + +``` cpp +vix::json +``` + +Core types: + +- `token` → generic JSON value +- `array_t` → JSON array +- `kvs` → JSON object storage + +------------------------------------------------------------------------ + +## token + +Represents any JSON value. + +Supported kinds: + +- null +- bool +- int64 +- double +- string +- array +- object + +Example: + +``` cpp +#include +#include + +using namespace vix::json; + +int main() +{ + token t = 42; + + if (t.is_i64()) + std::cout << t.as_i64_or(0) << "\n"; + + return 0; +} +``` + +------------------------------------------------------------------------ + +## array_t + +Dynamic JSON array. + +Key methods: + +- `push_int(...)` +- `push_string(...)` +- `push_bool(...)` +- `size()` +- `operator[]` + +Example: + +``` cpp +#include + +using namespace vix::json; + +int main() +{ + array_t arr; + arr.push_int(1); + arr.push_string("two"); + + return 0; +} +``` + +------------------------------------------------------------------------ + +## kvs + +Object-like key/value storage. + +Key methods: + +- `set_string(key, value)` +- `set_int(key, value)` +- `set_bool(key, value)` +- `get_string_or(key, fallback)` +- `ensure_object(key)` +- `ensure_array(key)` +- `merge_from(other, overwrite)` +- `erase(key)` +- `keys()` + +Example: + +``` cpp +#include + +using namespace vix::json; + +int main() +{ + kvs obj; + obj.set_string("name", "Vix"); + + return 0; +} +``` + +------------------------------------------------------------------------ + +# 2) High-Level JSON + +Header: + +``` cpp +#include +``` + +Powered by nlohmann::json. + +Namespace helpers: + +``` cpp +vix::json +``` + +Core type: + +``` cpp +Json +``` + +------------------------------------------------------------------------ + +## Object Builder + +``` cpp +#include + +using namespace vix::json; + +int main() +{ + auto j = o( + "name", "Vix", + "version", 1 + ); + + return 0; +} +``` + +------------------------------------------------------------------------ + +## Array Builder + +``` cpp +auto arr = a(1, 2, 3); +``` + +------------------------------------------------------------------------ + +## kv initializer + +``` cpp +auto j = kv({ + {"a", 1}, + {"b", true} +}); +``` + +------------------------------------------------------------------------ + +## dumps + +Serialize with indentation: + +``` cpp +std::string s = dumps(j, 2); +``` + +------------------------------------------------------------------------ + +## loads + +Parse string: + +``` cpp +auto j = loads(R"({"a":1})"); +``` + +------------------------------------------------------------------------ + +## File IO + +``` cpp +dump_file("out.json", j, 2); +auto j2 = load_file("out.json"); +``` + +------------------------------------------------------------------------ + +## jset / jget (Path Access) + +Mutate nested structures using path strings. + +``` cpp +Json j = obj(); + +jset(j, "user.profile.name", "Ada"); + +if (auto v = jget(j, "user.profile.name")) +{ + // value exists +} +``` + +------------------------------------------------------------------------ + +# Design Notes + +Simple JSON: + +- Minimal overhead +- Strong control +- Useful for internal state and WebSocket payloads + +High-level JSON: + +- Expressive builders +- API responses +- Parsing and file operations + +The JSON API is explicit and deterministic. + diff --git a/docs/api/middleware.md b/docs/api/middleware.md new file mode 100644 index 0000000..d28f95c --- /dev/null +++ b/docs/api/middleware.md @@ -0,0 +1,227 @@ +# Middleware API + +This page documents the middleware system in Vix. + +Vix supports two middleware styles: + +1) Context-based middleware (recommended) +2) Legacy HTTP middleware (Request/Response based) + +Both can be adapted and chained. + +------------------------------------------------------------------------ + +# Headers + +Context middleware: + +``` cpp +#include +``` + +Legacy middleware: + +``` cpp +#include +``` + +------------------------------------------------------------------------ + +# 1) Context Middleware (Recommended) + +Type: + +``` cpp +vix::middleware::MiddlewareFn +``` + +Signature: + +``` cpp +(Context& ctx, Next next) +``` + +------------------------------------------------------------------------ + +## Minimal Example + +``` cpp +#include +#include + +using namespace vix; + +int main() +{ + App app; + + vix::middleware::MiddlewareFn mw = + [](vix::middleware::Context& ctx, vix::middleware::Next next) + { + // pre logic + next(); + // post logic + }; + + app.use(mw); + + app.get("/", [](Request&, Response& res) + { + res.send("Hello"); + }); + + app.run(8080); +} +``` + +------------------------------------------------------------------------ + +# Registering Middleware + +## Global + +``` cpp +app.use(mw); +``` + +Applies to all routes. + +------------------------------------------------------------------------ + +## Prefix + +``` cpp +app.use("/api/", mw); +``` + +Applies only to routes under `/api/`. + +------------------------------------------------------------------------ + +## Exact Path + +``` cpp +app.use_exact("/ping", mw); +``` + +Applies only to the exact path. + +------------------------------------------------------------------------ + +# Chaining Middleware + +Multiple middleware can be chained: + +``` cpp +auto chained = vix::middleware::chain(mw1, mw2, mw3); + +app.use(chained); +``` + +Execution order: + +- mw1 pre +- mw2 pre +- mw3 pre +- handler +- mw3 post +- mw2 post +- mw1 post + +------------------------------------------------------------------------ + +# Request State Storage + +Context allows storing typed state per request. + +``` cpp +ctx.emplace_state(42); + +int& value = ctx.state(); +``` + +Safe access: + +``` cpp +if (auto* v = ctx.try_state()) +{ + // state exists +} +``` + +------------------------------------------------------------------------ + +# 2) Legacy HTTP Middleware + +Type: + +``` cpp +vix::HttpMiddleware +``` + +Signature: + +``` cpp +(Request&, Response&, Next) +``` + +Example: + +``` cpp +#include +#include + +using namespace vix; + +int main() +{ + App app; + + HttpMiddleware mw = + [](Request& req, Response& res, auto next) + { + (void)req; + (void)res; + next(); + }; + + app.use(mw); + + app.get("/", [](Request&, Response& res) + { + res.send("Hello"); + }); + + app.run(8080); +} +``` + +------------------------------------------------------------------------ + +# Adapters + +Adapt legacy to context: + +``` cpp +auto adapted = vix::middleware::adapt(mw); +app.use(adapted); +``` + +Adapt context to legacy: + +``` cpp +auto adapted = vix::middleware::adapt_ctx(ctx_mw); +app.use(adapted); +``` + +------------------------------------------------------------------------ + +# Behavior Notes + +- Middleware executes in registration order. +- If `next()` is not called, the chain stops. +- Middleware can modify request state before handler execution. +- Middleware should not block for long operations. + +The middleware system is explicit and composable by design. + diff --git a/docs/api/p2p.md b/docs/api/p2p.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/api/websocket.md b/docs/api/websocket.md new file mode 100644 index 0000000..8590501 --- /dev/null +++ b/docs/api/websocket.md @@ -0,0 +1,203 @@ +# WebSocket API + +This page documents the WebSocket server API in Vix. + +It covers: + +- WebSocket server +- Event hooks +- Typed message protocol +- Broadcast functions +- Long polling bridge + +All examples are minimal and placed inside `main()`. + +------------------------------------------------------------------------ + +# Server + +Header: + +``` cpp +#include +``` + +Core type: + +``` cpp +vix::websocket::Server +``` + +------------------------------------------------------------------------ + +## Constructor + +``` cpp +Server ws(cfg, executor); +``` + +Parameters: + +- `cfg` → configuration object +- `executor` → thread pool executor + +------------------------------------------------------------------------ + +# Minimal Standalone Example + +``` cpp +#include +#include +#include + +int main() +{ + vix::config::Config cfg{"config.json"}; + + auto exec = vix::experimental::make_threadpool_executor(4, 8, 0); + + vix::websocket::Server ws(cfg, std::move(exec)); + + ws.on_open([](auto& session) { + (void)session; + }); + + ws.on_typed_message([](auto& session, + const std::string& type, + const vix::json::kvs& payload) + { + (void)session; + (void)type; + (void)payload; + }); + + ws.listen_blocking(); + + return 0; +} +``` + +------------------------------------------------------------------------ + +# Event Hooks + +## on_open + +Called when a client connects. + +``` cpp +ws.on_open([](auto& session) { + // session is active +}); +``` + +------------------------------------------------------------------------ + +## on_close + +Called when a client disconnects. + +``` cpp +ws.on_close([](auto& session) { + // cleanup +}); +``` + +------------------------------------------------------------------------ + +## on_typed_message + +Called when a typed message is received. + +Signature: + +``` cpp +(session, type, payload) +``` + +- `type` → string +- `payload` → `vix::json::kvs` + +------------------------------------------------------------------------ + +# Typed Message Protocol + +Expected message shape: + +``` json +{ + "type": "event.name", + "payload": { ... } +} +``` + +Vix automatically parses and routes this to `on_typed_message`. + +------------------------------------------------------------------------ + +# Broadcast + +## Broadcast to all + +``` cpp +ws.broadcast_json("chat.message", { + "user", "Alice", + "text", "Hello" +}); +``` + +------------------------------------------------------------------------ + +## Broadcast to room + +``` cpp +ws.broadcast_room_json("room1", "chat.message", payload); +``` + +------------------------------------------------------------------------ + +# Long Polling Bridge + +Header: + +``` cpp +#include +``` + +Attach bridge: + +``` cpp +ws.attach_long_polling_bridge(bridge); +``` + +Purpose: + +- Fallback when WebSocket is blocked +- HTTP endpoints simulate WebSocket behavior + +------------------------------------------------------------------------ + +# HTTP + WS Combined + +Use helper: + +``` cpp +vix::serve_http_and_ws([](auto& app, auto& ws) { + // register HTTP routes + // register WebSocket handlers +}); +``` + +This creates a single runtime handling both protocols. + +------------------------------------------------------------------------ + +# Notes + +- `listen_blocking()` blocks the thread. +- WebSocket handlers should be short and non-blocking. +- Use broadcast carefully in high-load scenarios. +- Use room broadcasting for scalable chat systems. + +The WebSocket API is minimal by design. + diff --git a/docs/architecture.md b/docs/architecture.md deleted file mode 100644 index 8316f49..0000000 --- a/docs/architecture.md +++ /dev/null @@ -1,117 +0,0 @@ -# Architecture — Vix.cpp - -Vix.cpp is organized as a **small, sharp core** with optional modules. You can consume modules independently or as an umbrella package. - ---- - -## High‑Level View - -``` -+----------------------+ +---------------------+ -| Your App | | CLI (vix) | -| (routes, services) | | new/build/run | -+----------+-----------+ +----------+----------+ - | | - v v -+----------------------+ +---------------------+ -| core |<---->| devtools | -| App, Router, HTTP | | scripts, presets | -+----+----+----+-------+ +---------------------+ - | | | - | | +-------------------+ - | | | - v v v -+----+----+----+--------+ +------+-------+ +-------------------+ -| utils | json | | websocket | | orm | -| Logger | builders | | (WIP) | | Repo/UoW, drivers | -| UUID/Env | nlohmann | | | | MySQL/SQLite | -+-----------+-----------+ +--------------+ +--------------------+ -``` - -- **core** — HTTP server (Asio/Beast), router, request/response, status codes. -- **utils** — Logger (sync/async), UUID, Time, Env, Validation helpers. -- **json** — light wrappers/builders around _nlohmann/json_ for ergonomic responses. -- **orm** — optional: Repository + Unit of Work, QueryBuilder, connection pool. -- **websocket** — WIP: channels, rooms, backpressure. -- **cli** — `vix new/build/run` to scaffold and operate projects. -- **devtools** — local tooling, scripts, presets. - ---- - -## Core Components - -### `App` - -- Owns the HTTP server and route registry. -- Methods: `get`, `post`, `put`, `del`, `use(middleware)`. -- `run(port)` starts the event loop; graceful stop via signals. - -### Router - -- Templates routes with path parameters: `/users/{id}`. -- Parameter extraction is type‑agnostic (string‑first, convert as needed). -- Middleware hooks (logging/validation) composed per route or globally. - -### HTTP - -- Thin wrappers around Beast types. -- `ResponseWrapper` exposes convenient `json(...)`, `status(...)`, `send(...)` helpers. -- Intentional minimalism: no hidden thread‑locals or globals. - ---- - -## Concurrency Model - -- Event‑driven I/O based on Asio. -- Compute offloading via a **thread pool** (configurable). -- Lock boundaries minimized; prefer immutable data in hot paths. -- Graceful shutdown coordinates listener, workers, and in‑flight requests. - ---- - -## Error Handling & Logging - -- Runtime errors bubble to a predictable handler in core. -- Structured logs via `utils::Logger` (sync/async) with context (request_id/module). -- Sanitizer‑friendly builds and optional static analysis (`clang-tidy`, `cppcheck`). - ---- - -## JSON Path - -- Input: `nlohmann::json` parsing recommended for request bodies. -- Output: `Vix::json` builders (tokens/obj/array) for zero‑friction responses. -- Avoids repetitive boilerplate while keeping conversions explicit. - ---- - -## ORM Integration (Optional) - -- Connection pooling, prepared statements, RAII transactions. -- Repository/Unit‑of‑Work pattern reduces boilerplate and enforces consistency. -- Can be used standalone (separate CMake target) or with the umbrella build. - ---- - -## Configuration - -- Simple JSON at `config/config.json` copied to build dir. -- ENV helpers: `utils::env_str`, `env_int`, `env_bool` for overrides. -- Prefer explicit constructor injection for services and repositories. - ---- - -## Build System - -- CMake ≥ 3.20, generator‑agnostic (Make/Ninja/VS). -- Presets recommended for debug/asan/release. -- `compile_commands.json` exported automatically for IDEs. - ---- - -## Roadmap Notes - -- WebSocket engine (channels/rooms/backpressure). -- Middlewares: CORS presets, rate limiting, auth helpers. -- Devtools: profiler hooks, trace exporters. -- ORM: query planner and driver adapters. diff --git a/docs/benchmarks.md b/docs/benchmarks.md deleted file mode 100644 index d85c95f..0000000 --- a/docs/benchmarks.md +++ /dev/null @@ -1,85 +0,0 @@ -# ⚡ Benchmarks (Updated — Dec 2025) - -All benchmarks were executed using **wrk** -`8 threads`, `800 connections`, for **30 seconds**, on the same machine: -**Ubuntu 24.04 — Intel Xeon — C++20 optimized build — Logging disabled** - -Results represent steady-state throughput on a simple `"OK"` endpoint. - ---- - -## 🚀 Requests per second - -| Framework | Requests/sec | Avg Latency | Transfer/sec | -| ------------------------- | -------------------------- | --------------- | -------------- | -| ⭐ **Vix.cpp (v1.10.6)** | **~98,942** _(pinned CPU)_ | **7.3–10.8 ms** | **~13.8 MB/s** | -| **Vix.cpp (default run)** | 81,300 – 81,400 | 9.7–10.8 ms | ≈ 11.3 MB/s | -| Go (Fiber) | 81,336 | 0.67 ms | 10.16 MB/s | -| **Deno** | ~48,868 | 16.34 ms | ~6.99 MB/s | -| Node.js (Fastify) | 4,220 | 16.00 ms | 0.97 MB/s | -| PHP (Slim) | 2,804 | 16.87 ms | 0.49 MB/s | -| Crow (C++) | 1,149 | 41.60 ms | 0.35 MB/s | -| FastAPI (Python) | 752 | 63.71 ms | 0.11 MB/s | - -> 🔥 **New record:** When pinned to a single core (`taskset -c 2`) -> Vix.cpp reaches **~99k req/s**, surpassing Go and matching the fastest C++ microframeworks. - ---- - -## 📝 Notes - -### ✔ Why Vix.cpp reaches Go-level performance - -- zero-cost abstractions -- custom ThreadPool tuned for HTTP workloads -- optimized HTTP pipeline -- fast-path routing -- Beast-based IO -- minimal memory allocations -- predictable threading model - ---- - -## 🦕 Deno benchmark (reference) - -```bash -$ wrk -t8 -c800 -d30s --latency http://127.0.0.1:8000 -Requests/sec: 48,868.73 -``` - -### ✔ Vix.cpp recommended benchmark mode - -When benchmarking from inside the Vix.cpp repository (using the built-in example): - -```bash -cd ~/vixcpp/vix -export VIX_LOG_LEVEL=critical -export VIX_LOG_ASYNC=false - -# Run the optimized example server -vix run example main -``` - -Then, in another terminal: - -```bash -wrk -t8 -c800 -d30s --latency http://127.0.0.1:8080/bench -``` - -If you want CPU pinning for more stable results: - -```bash -taskset -c 2 ./build/main -wrk -t8 -c800 -d30s --latency http://127.0.0.1:8080/bench -``` - -#### 🏁 Result: ~98,942 req/s - -✔ Fast-path routing gives +1–3% - -## Use /fastbench to bypass RequestHandler overhead. - -## 🧠 Takeaway - -Vix.cpp provides **Go-level performance** with full C++ control and type safety. -For deeper details, see [docs/architecture.md](./architecture.md) or run your own local tests. diff --git a/docs/build.md b/docs/build.md deleted file mode 100644 index f4403bb..0000000 --- a/docs/build.md +++ /dev/null @@ -1,185 +0,0 @@ -# Build & Packaging — Vix.cpp - -This page explains the different build configurations, debugging tools, and packaging options for **Vix.cpp**. - ---- - -## 🏗️ Build Modes - -Vix.cpp uses **CMake ≥ 3.20** and supports multiple build types: - -| Mode | Description | Flags | -| ------------------ | -------------------------------------------------------------- | ----------------------------------- | -| **Debug** | Includes debug symbols, no optimization. Best for development. | `-DCMAKE_BUILD_TYPE=Debug` | -| **Release** | Optimized for performance. Default for production. | `-DCMAKE_BUILD_TYPE=Release` | -| **RelWithDebInfo** | Mix of Release + debug symbols. | `-DCMAKE_BUILD_TYPE=RelWithDebInfo` | -| **MinSizeRel** | Optimized for size. | `-DCMAKE_BUILD_TYPE=MinSizeRel` | - -Example: - -```bash -cmake -S . -B build-rel -DCMAKE_BUILD_TYPE=Release -cmake --build build-rel -j -``` - ---- - -## ⚙️ Sanitizers (ASan / UBSan) - -Enable runtime memory and undefined behavior checks: - -```bash -cmake -S . -B build-asan -DCMAKE_BUILD_TYPE=Debug -DVIX_ENABLE_SANITIZERS=ON -cmake --build build-asan -j -``` - -This automatically adds: - -``` --fsanitize=address,undefined -O1 -g -fno-omit-frame-pointer -``` - -Use in **Debug only** — disable for Release builds. - ---- - -## 🔍 Static Analysis - -Vix.cpp integrates with common static analysis tools. - -### Clang-Tidy - -```bash -cmake -S . -B build -DVIX_ENABLE_CLANG_TIDY=ON -cmake --build build -j -``` - -### Cppcheck - -```bash -cmake -S . -B build -DVIX_ENABLE_CPPCHECK=ON -``` - ---- - -## 🧩 Link-Time Optimization (LTO) - -For higher performance, enable **LTO** in Release builds: - -```bash -cmake -S . -B build-lto -DCMAKE_BUILD_TYPE=Release -DVIX_ENABLE_LTO=ON -cmake --build build-lto -j -``` - -LTO reduces binary size and improves runtime performance by optimizing across translation units. - ---- - -## 🧪 Code Coverage (Developers) - -For measuring test coverage in Debug builds: - -```bash -cmake -S . -B build-cov -DCMAKE_BUILD_TYPE=Debug -DVIX_ENABLE_COVERAGE=ON -cmake --build build-cov -j -``` - -Run tests, then use `gcov`, `lcov`, or `llvm-cov` to generate reports. - ---- - -## 🧱 Packaging / Installation - -Build and install all umbrella modules: - -```bash -cmake -S . -B build-pkg -DCMAKE_BUILD_TYPE=Release -DVIX_ENABLE_INSTALL=ON -cmake --build build-pkg -j -sudo cmake --install build-pkg --prefix /usr/local -``` - -Inspect installation layout: - -```bash -cmake --install build-pkg --prefix /usr/local --dry-run -``` - -To uninstall manually, remove installed files or rebuild with a different prefix (e.g., `/opt/vixcpp`). - ---- - -## 🧠 Developer Workflow - -- Use **Ninja** for faster incremental builds: - ```bash - cmake -G Ninja -S . -B build - ``` -- Regenerate submodules: - ```bash - git submodule update --remote --merge - ``` -- Export compile commands for IDEs: - ```bash - -DCMAKE_EXPORT_COMPILE_COMMANDS=ON - ``` - ---- - -## 🧩 Example Preset Configurations - -You can define presets in `CMakePresets.json`: - -```json -{ - "version": 3, - "configurePresets": [ - { - "name": "release", - "generator": "Ninja", - "binaryDir": "build-rel", - "cacheVariables": { - "CMAKE_BUILD_TYPE": "Release", - "VIX_ENABLE_LTO": "ON", - "VIX_ENABLE_INSTALL": "ON" - } - }, - { - "name": "debug-asan", - "generator": "Ninja", - "binaryDir": "build-asan", - "cacheVariables": { - "CMAKE_BUILD_TYPE": "Debug", - "VIX_ENABLE_SANITIZERS": "ON" - } - } - ] -} -``` - -Then run: - -```bash -cmake --preset release -cmake --build --preset release -``` - ---- - -## 🧰 Troubleshooting Build Issues - -| Problem | Cause | Solution | -| --------------------------------- | -------------------- | -------------------------------------------------- | -| `nlohmann/json.hpp not found` | Missing dependency | Install with `sudo apt install nlohmann-json3-dev` | -| `undefined reference to spdlog::` | Missing spdlog lib | Install with `sudo apt install libspdlog-dev` | -| `fatal error: boost/asio.hpp` | Boost not installed | `sudo apt install libboost-all-dev` | -| `cmake: invalid preset` | Missing CMake ≥ 3.20 | Upgrade CMake via `pip install cmake --upgrade` | - ---- - -## ✅ Next Steps - -- [Quick Start](./quick-start.md) -- [Installation](./installation.md) -- [Benchmarks](./benchmarks.md) -- [CMake Options](./options.md) -- [Architecture](./architecture.md) diff --git a/docs/examples/api-key.md b/docs/examples/api-key.md new file mode 100644 index 0000000..2643529 --- /dev/null +++ b/docs/examples/api-key.md @@ -0,0 +1,197 @@ +# API Key Middleware Guide (Vix.cpp) — Beginner Friendly + +## What is an API Key? + +An API key is a simple secret string sent by the client to prove it is allowed to access a protected endpoint. + +It is NOT as powerful as JWT, but it is: + +- Simple +- Fast +- Perfect for internal APIs or microservices + +--- + +# How API Key Works in Vix.cpp + +Client sends: + +Header: +```bash + x-api-key: secret +``` +OR + +Query param: +```bash + ?api_key=secret +``` +Middleware checks: +- Is key present? +- Is key valid? +- If yes → continue +- If no → 401 or 403 + +--- + +# Minimal Example + +File: `api_key_app_simple.cpp` + +```cpp +App app; + +// Protect only /secure +app.use("/secure", middleware::app::api_key_dev("secret")); + +app.get("/secure", [](Request& req, Response& res) +{ + auto& key = req.state(); + + res.json({ + "ok", true, + "api_key", key.value + }); +}); +``` + +# Run + +```bash +vix run api_key_app_simple.cpp +``` + +Server runs on: +```bash +http://localhost:8080 +``` + +# Test With curl + +## 1 Missing key + +```bash +curl -i http://localhost:8080/secure +``` + +Result: +401 Unauthorized + +--- + +## 2) Invalid key + +```bash +curl -i -H "x-api-key: wrong" http://localhost:8080/secure +``` + +Result: +403 Forbidden + +--- + +## 3) Valid key (Header) + +```bash +curl -i -H "x-api-key: secret" http://localhost:8080/secure +``` + +Result: +200 OK + +--- + +## 4) Valid key (Query param) + +```bash +curl -i "http://localhost:8080/secure?api_key=secret" +``` + +Result: +200 OK + +--- + +# What Happens Internally? + +If key is valid: +```cpp +req.state().value +``` +contains: + +"secret" + +You can use it inside your route. + +--- + +# When Should You Use API Key? + +Good for: + +- Internal service-to-service auth +- Dev environments +- Small private APIs + +Not ideal for: + +- Complex user systems +- Role-based permissions +- Public user authentication + +--- + +# Full Working Example + +```cpp +#include +#include + +#include +#include + +using namespace vix; + +int main() +{ + App app; + + // Protect /secure + app.use("/secure", middleware::app::api_key_dev("secret")); + + // Public route + app.get("/", [](Request&, Response& res) + { + res.send("API Key example: /secure requires x-api-key: secret"); + }); + + // Protected route + app.get("/secure", [](Request& req, Response& res) + { + auto& key = req.state(); + + res.json({ + "ok", true, + "api_key", key.value + }); + }); + + app.run(8080); + return 0; +} +``` + +--- + +# Summary + +API Key = Simple shared secret + +Very fast +Very lightweight +Very easy to understand + +Vix.cpp keeps it clean and minimal. + + diff --git a/docs/examples/async.md b/docs/examples/async.md new file mode 100644 index 0000000..4bba768 --- /dev/null +++ b/docs/examples/async.md @@ -0,0 +1,209 @@ +# Async Worker (Beginner Guide) + +Welcome 👋 + +This page explains how to use Vix async in a simple and practical way. +If you are new to coroutines or async C++, this guide is for you. + +------------------------------------------------------------------------ + +## What is an Async Worker? + +An async worker allows your program to: + +- Run tasks without blocking the main thread +- Wait for timers without freezing +- Run heavy CPU work in background threads +- Stop cleanly when receiving Ctrl+C + +In simple words: + +Async = Do not block.\ +Worker = Do work safely in background. + +------------------------------------------------------------------------ + +# 1️⃣ Minimal Example -- Hello Async + +This is the smallest possible example. + +``` cpp +#include +#include +#include + +using vix::async::core::io_context; +using vix::async::core::task; + +task app(io_context& ctx) +{ + std::cout << "Hello async world!\n"; + ctx.stop(); + co_return; +} + +int main() +{ + io_context ctx; + + auto t = app(ctx); + ctx.post(t.handle()); + ctx.run(); + + return 0; +} +``` + +What happens here? + +- io_context = async runtime +- task\<\> = coroutine function +- ctx.post() = schedule the task +- ctx.run() = start the event loop +- ctx.stop() = stop the runtime + +------------------------------------------------------------------------ + +# 2️⃣ Waiting Without Blocking (Timer) + +This example waits 1 second without freezing the program. + +``` cpp +#include +#include + +#include +#include +#include + +using vix::async::core::io_context; +using vix::async::core::task; + +task app(io_context& ctx) +{ + std::cout << "Waiting 1 second...\n"; + + co_await ctx.timers().sleep_for(std::chrono::seconds(1)); + + std::cout << "Done!\n"; + ctx.stop(); + co_return; +} + +int main() +{ + io_context ctx; + auto t = app(ctx); + ctx.post(t.handle()); + ctx.run(); +} +``` + +Important: + +sleep_for() does NOT block the event loop thread. + +------------------------------------------------------------------------ + +# 3️⃣ Running Heavy Work in Background (CPU Pool) + +Never block your event loop with heavy computation. + +Instead: + +``` cpp +#include +#include +#include +#include + +using vix::async::core::io_context; +using vix::async::core::task; + +task app(io_context& ctx) +{ + int result = co_await ctx.cpu_pool().submit([] { + int sum = 0; + for (int i = 0; i < 100000; ++i) + sum += i; + return sum; + }); + + std::cout << "Result: " << result << "\n"; + + ctx.stop(); + co_return; +} + +int main() +{ + io_context ctx; + auto t = app(ctx); + ctx.post(t.handle()); + ctx.run(); +} +``` + +Here: + +- The heavy loop runs on a worker thread +- The event loop stays responsive + +------------------------------------------------------------------------ + +# 4️⃣ Clean Shutdown (Ctrl+C) + +Production programs must stop safely. + +``` cpp +#include +#include + +#include +#include +#include + +using vix::async::core::io_context; +using vix::async::core::task; + +task app(io_context& ctx) +{ + auto& sig = ctx.signals(); + + sig.add(SIGINT); + sig.add(SIGTERM); + + std::cout << "Press Ctrl+C to stop\n"; + + sig.on_signal([&](int){ + std::cout << "Stopping...\n"; + ctx.stop(); + }); + + co_await sig.async_wait(); + co_return; +} + +int main() +{ + io_context ctx; + auto t = app(ctx); + ctx.post(t.handle()); + ctx.run(); +} +``` + +------------------------------------------------------------------------ + +# Final Notes for Beginners + +✔ Always use cpu_pool() for heavy work\ +✔ Use timers instead of std::this_thread::sleep_for()\ +✔ Handle SIGINT for production apps\ +✔ Keep event loop clean and responsive + +------------------------------------------------------------------------ + +Generated on 2026-02-17\ +Vix Async Beginner Guide + diff --git a/docs/examples/auth.md b/docs/examples/auth.md new file mode 100644 index 0000000..f5acb0a --- /dev/null +++ b/docs/examples/auth.md @@ -0,0 +1,343 @@ +# Auth and Middleware (Minimal Patterns) + +This page shows minimal auth and middleware patterns in Vix.cpp. + +Each section is: - one concept - one minimal `main()` - a quick curl +test + +------------------------------------------------------------------------ + +## 1) API key middleware (protect one route) + +A public route plus a secure route that requires `x-api-key`. + +``` cpp +#include +#include +using namespace vix; + +int main() +{ + App app; + + app.get("/public", [](Request&, Response& res){ + res.json({ "ok", true, "scope", "public" }); + }); + + // Install API key middleware only on this prefix + middleware::app::install(app, "/secure/", middleware::app::api_key_dev("dev_key_123")); + + app.get("/secure/whoami", [](Request&, Response& res){ + res.json({ "ok", true, "scope", "secure", "message", "API key accepted" }); + }); + + app.run(8080); + return 0; +} +``` + +Try: + +``` bash +curl -i http://127.0.0.1:8080/public +curl -i http://127.0.0.1:8080/secure/whoami +curl -i -H "x-api-key: dev_key_123" http://127.0.0.1:8080/secure/whoami +``` + +------------------------------------------------------------------------ + +## 2) Prefix protection (protect all /api routes) + +Everything under `/api/` is protected. + +``` cpp +#include +#include +using namespace vix; + +int main() +{ + App app; + + middleware::app::install(app, "/api/", middleware::app::api_key_dev("dev_key_123")); + + app.get("/api/ping", [](Request&, Response& res){ + res.json({ "ok", true, "pong", true }); + }); + + app.get("/api/users", [](Request&, Response& res){ + res.json({ "ok", true, "data", json::array({ "u1", "u2" }) }); + }); + + app.run(8080); + return 0; +} +``` + +Try: + +``` bash +curl -i http://127.0.0.1:8080/api/ping +curl -i -H "x-api-key: dev_key_123" http://127.0.0.1:8080/api/ping +``` + +------------------------------------------------------------------------ + +## 3) Custom middleware (context style + RequestState) + +This shows how to store data in RequestState and read it in the handler. + +``` cpp +#include +#include +#include +#include +using namespace vix; + +struct RequestId{ + std::string value; +}; + +static long long now_ms(){ + using namespace std::chrono; + return (long long)time_point_cast(system_clock::now()).time_since_epoch().count(); +} + +static vix::middleware::MiddlewareFn mw_request_id(){ + return [](vix::middleware::Context& ctx, vix::middleware::Next next) + { + RequestId rid; + rid.value = std::to_string(now_ms()); + + ctx.req().emplace_state(rid); + ctx.res().header("x-request-id", rid.value); + + next(); + }; +} + +int main() +{ + App app; + + // Adapt context middleware into app middleware + app.use(vix::middleware::app::adapt_ctx(mw_request_id())); + + app.get("/who", [](Request& req, Response& res){ + const auto rid = req.state().value; + res.json({ "ok", true, "request_id", rid }); + }); + + app.run(8080); + return 0; +} +``` + +Try: + +``` bash +curl -i http://127.0.0.1:8080/who +``` + +------------------------------------------------------------------------ + +## 4) Role gating (fake auth + admin only) + +This is a minimal RBAC-style gate using headers for the demo. + +``` cpp +#include +#include +#include + +using namespace vix; +namespace J = vix::json; + +struct AuthInfo{ + bool authed{false}; + std::string subject; + std::string role; +}; + +static vix::middleware::MiddlewareFn mw_fake_auth(){ + return [](vix::middleware::Context& ctx, vix::middleware::Next next) + { + AuthInfo a; + const std::string user = ctx.req().header("x-user"); + const std::string role = ctx.req().header("x-role"); + + if (!user.empty()) + { + a.authed = true; + a.subject = user; + a.role = role.empty() ? "user" : role; + } + + ctx.req().emplace_state(a); + next(); + }; +} + +static vix::middleware::MiddlewareFn mw_require_admin(){ + return [](vix::middleware::Context& ctx, vix::middleware::Next next){ + if (!ctx.req().has_state_type() || !ctx.req().state().authed) + { + ctx.res().status(401).json(J::obj({ "ok", false, "error", "unauthorized" })); + return; + } + + if (ctx.req().state().role != "admin") + { + ctx.res().status(403).json(J::obj({ "ok", false, "error", "forbidden", "hint", "admin required" })); + return; + } + + next(); + }; +} + +int main() +{ + App app; + + app.use(vix::middleware::app::adapt_ctx(mw_fake_auth())); + + // Install admin guard only under /admin/ + vix::middleware::app::install(app, "/admin/", vix::middleware::app::adapt_ctx(mw_require_admin())); + + app.get("/admin/stats", [](Request& req, Response& res) + { + const auto& a = req.state(); + res.json({ "ok", true, "admin", true, "subject", a.subject }); + }); + + app.run(8080); + return 0; +} +``` + +Try: + +``` bash +curl -i http://127.0.0.1:8080/admin/stats +curl -i -H "x-user: gaspard" http://127.0.0.1:8080/admin/stats +curl -i -H "x-user: gaspard" -H "x-role: admin" http://127.0.0.1:8080/admin/stats +``` + +------------------------------------------------------------------------ + +## 5) Legacy HttpMiddleware style (adapt) + +If you have an older middleware signature `(Request, Response, next)` +you can adapt it. + +``` cpp +#include +#include +#include + +using namespace vix; +namespace J = vix::json; + +static vix::middleware::HttpMiddleware require_header(std::string header, std::string expected) +{ + return [header = std::move(header), expected = std::move(expected)](Request& req, Response& res, vix::middleware::Next next) + { + const std::string got = req.header(header); + if (got != expected) + { + res.status(401).json(J::obj({ + "ok", false, + "error", "unauthorized", + "required_header", header + })); + return; + } + next(); + }; +} + +int main() +{ + App app; + + vix::middleware::app::install_exact(app, "/api/ping", vix::middleware::app::adapt(require_header("x-demo", "1"))); + + app.get("/api/ping", [](Request&, Response& res){ + res.json({ "ok", true, "pong", true }); + }); + + app.run(8080); + return 0; +} +``` + +Try: + +``` bash +curl -i http://127.0.0.1:8080/api/ping +curl -i -H "x-demo: 1" http://127.0.0.1:8080/api/ping +``` + +------------------------------------------------------------------------ + +## 6) Chaining middleware + +Apply multiple middlewares on the same prefix. + +``` cpp +#include +#include +#include + +using namespace vix; + +static vix::middleware::MiddlewareFn mw_mark() +{ + return [](vix::middleware::Context& ctx, vix::middleware::Next next) + { + ctx.res().header("x-mw", "on"); + next(); + }; +} + +int main() +{ + App app; + + // Chain: api key then a custom marker middleware + vix::middleware::app::install( + app, + "/secure/", + vix::middleware::app::chain( + vix::middleware::app::api_key_dev("dev_key_123"), + vix::middleware::app::adapt_ctx(mw_mark()) + ) + ); + + app.get("/secure/hello", [](Request&, Response& res) + { + res.json({ "ok", true, "message", "Hello secure" }); + }); + + app.run(8080); + return 0; +} +``` + +Try: + +``` bash +curl -i -H "x-api-key: dev_key_123" http://127.0.0.1:8080/secure/hello +``` + +------------------------------------------------------------------------ + +## What this teaches + +- Prefix install: protect a group of routes +- Exact install: protect one route +- Context middleware: state and headers +- Legacy middleware adaptation +- Basic RBAC-style gating +- Middleware chaining diff --git a/docs/examples/batch-insert-tx.md b/docs/examples/batch-insert-tx.md new file mode 100644 index 0000000..bb4e4ee --- /dev/null +++ b/docs/examples/batch-insert-tx.md @@ -0,0 +1,268 @@ +# ORM Example Guide: Batch Insert + Transaction (MySQL) + +This guide explains the `batch_insert_tx` example step by step. + +Goal: - Connect to MySQL using the ORM connection pool - Run multiple +INSERT operations efficiently - Wrap everything inside a transaction +(commit or rollback) + +## 1. What this example demonstrates + +This example shows how to: + +- Create a MySQL driver factory with `make_mysql_factory()` +- Build a `ConnectionPool` with `PoolConfig` +- Warm up the pool so it opens connections early +- Use `Transaction` (RAII) for safe commits and automatic rollbacks +- Use prepared statements with parameter binding +- Insert multiple rows inside one transaction + +## 2. Full Example Code + +``` cpp +#include + +#include +#include +#include +#include + +using namespace vix::orm; + +int main(int argc, char **argv) +{ + const std::string host = (argc > 1 ? argv[1] : "tcp://127.0.0.1:3306"); + const std::string user = (argc > 2 ? argv[2] : "root"); + const std::string pass = (argc > 3 ? argv[3] : ""); + const std::string db = (argc > 4 ? argv[4] : "vixdb"); + + try + { + // DB factory (MySQL driver) + auto factory = make_mysql_factory(host, user, pass, db); + + PoolConfig cfg; + cfg.min = 1; + cfg.max = 8; + + ConnectionPool pool{factory, cfg}; + pool.warmup(); + + // Transaction (RAII rollback if not committed) + Transaction tx(pool); + auto &c = tx.conn(); + + auto st = c.prepare("INSERT INTO users(name,email,age) VALUES(?,?,?)"); + + struct Row + { + const char *name; + const char *email; + int age; + }; + + const std::vector rows = { + {"Zoe", "zoe@example.com", 23}, + {"Mina", "mina@example.com", 31}, + {"Omar", "omar@example.com", 35}, + }; + + std::uint64_t total = 0; + for (const auto &r : rows) + { + st->bind(1, r.name); + st->bind(2, r.email); + st->bind(3, r.age); + total += st->exec(); + } + + tx.commit(); + std::cout << "[OK] inserted rows = " << total << "\n"; + return 0; + } + catch (const DBError &e) + { + std::cerr << "[DBError] " << e.what() << "\n"; + return 1; + } + catch (const std::exception &e) + { + std::cerr << "[ERR] " << e.what() << "\n"; + return 1; + } +} +``` + +## 3. Step by Step Explanation + +### 3.1 CLI arguments (connection info) + +``` cpp +const std::string host = (argc > 1 ? argv[1] : "tcp://127.0.0.1:3306"); +const std::string user = (argc > 2 ? argv[2] : "root"); +const std::string pass = (argc > 3 ? argv[3] : ""); +const std::string db = (argc > 4 ? argv[4] : "vixdb"); +``` + +This allows running the same binary with different databases without +editing code. + +Example: + +``` bash +./batch_insert_tx tcp://127.0.0.1:3306 root "" vixdb +``` + +### 3.2 Create the MySQL driver factory + +``` cpp +auto factory = make_mysql_factory(host, user, pass, db); +``` + +The factory knows how to create new MySQL connections. The pool will use +this factory when it needs more connections. + +### 3.3 Configure and create the connection pool + +``` cpp +PoolConfig cfg; +cfg.min = 1; +cfg.max = 8; + +ConnectionPool pool{factory, cfg}; +pool.warmup(); +``` + +- `min` is the minimum number of connections kept ready. +- `max` is the maximum number of connections allowed. +- `warmup()` opens the initial connections early, so the first query + is not slow. + +Production tip: - Warmup is useful for APIs to avoid a slow first +request. + +### 3.4 Start a transaction (RAII) + +``` cpp +Transaction tx(pool); +auto &c = tx.conn(); +``` + +This creates a transaction using one connection acquired from the pool. + +RAII rule: - If `tx.commit()` is not called, rollback happens +automatically. + +### 3.5 Prepare the insert statement once + +``` cpp +auto st = c.prepare("INSERT INTO users(name,email,age) VALUES(?,?,?)"); +``` + +Prepared statements: - Improve performance (parse/plan once) - Avoid SQL +injection - Allow safe binding + +### 3.6 Define rows to insert + +``` cpp +struct Row { const char *name; const char *email; int age; }; +``` + +Then a vector of rows: + +``` cpp +const std::vector rows = { + {"Zoe", "zoe@example.com", 23}, + {"Mina", "mina@example.com", 31}, + {"Omar", "omar@example.com", 35}, +}; +``` + +### 3.7 Bind parameters and execute in a loop + +``` cpp +for (const auto &r : rows) +{ + st->bind(1, r.name); + st->bind(2, r.email); + st->bind(3, r.age); + total += st->exec(); +} +``` + +Binding rules: - Indexes start at 1 - Types are converted safely - The +same prepared statement is reused for every row + +Note: - This is not a multi-row SQL insert. - It is still fast because +the statement is prepared once and executed multiple times in one +transaction. + +### 3.8 Commit + +``` cpp +tx.commit(); +``` + +Without commit: - rollback happens automatically - rows are not inserted + +With commit: - all rows become visible and durable + +## 4. Required SQL Table + +This example expects a `users` table with: + +- name +- email +- age + +Example schema: + +``` sql +CREATE TABLE IF NOT EXISTS users ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + name VARCHAR(255) NOT NULL, + email VARCHAR(255) NOT NULL, + age INT NOT NULL +); +``` + +## 5. Error Handling + +``` cpp +catch (const DBError &e) +{ + std::cerr << "[DBError] " << e.what() << "\n"; +} +``` + +- `DBError` is used for database driver / SQL errors. +- A second catch handles other exceptions. + +Production tip: - Log DBError message internally - Return generic +messages to HTTP clients + +## 6. Production Notes + +Use this pattern when: + +- You need to insert many rows +- You want atomic behavior +- You want speed and safety + +Best practices: + +- Keep the transaction short +- Avoid external API calls inside a transaction +- Use pool warmup at startup +- Tune `cfg.max` using load tests + +## Summary + +You learned: + +- How to configure a MySQL pool with Vix ORM +- How to batch insert rows efficiently +- How to use RAII transactions for safety +- How to bind parameters with prepared statements +- How to handle database exceptions + diff --git a/docs/examples/body-limit.md b/docs/examples/body-limit.md new file mode 100644 index 0000000..07cc128 --- /dev/null +++ b/docs/examples/body-limit.md @@ -0,0 +1,201 @@ +# Body Limit (Beginner Guide) + +Welcome 👋\ +This page explains how to use the **body limit middleware** in Vix.cpp. + +Body limits protect your server from: + +- very large requests (accidental or malicious) +- memory pressure (huge JSON uploads) +- slow uploads (DoS patterns) +- endpoints that should never accept big bodies + +When the request body is too large, Vix returns: + +- **413 Payload Too Large** + +## What does "body limit" mean? + +It means: **do not accept requests bigger than N bytes**. + +Example: if you set `max_bytes = 32`: + +- 0..32 bytes is allowed +- 33+ bytes is rejected with 413 + +## 1) Minimal example + +This server: + +- keeps `/` public +- limits `/api/*` requests to **32 bytes** +- lets GET pass (default behavior) +- demonstrates a strict route that rejects chunked uploads + +``` cpp +#include +#include + +using namespace vix; + +static void register_routes(App &app) +{ + app.get("/", [](Request &, Response &res) + { res.send("body_limit example: /api/ping, /api/echo, /api/strict"); }); + + app.get("/api/ping", [](Request &, Response &res) + { res.json({"ok", true, "msg", "pong"}); }); + + app.post("/api/echo", [](Request &req, Response &res) + { res.json({"ok", true, + "bytes", static_cast(req.body().size()), + "content_type", req.header("content-type")}); }); + + app.post("/api/strict", [](Request &req, Response &res) + { res.json({"ok", true, + "msg", "strict accepted", + "bytes", static_cast(req.body().size())}); }); +} + +int main() +{ + App app; + + // /api: max 32 bytes (demo), chunked allowed + app.use("/api", middleware::app::body_limit_dev( + 32, // max_bytes + false, // apply_to_get + true // allow_chunked + )); + + // /api/strict: max 32 bytes, chunked NOT allowed + app.use("/api/strict", middleware::app::body_limit_dev( + 32, // max_bytes + false, // apply_to_get + false // allow_chunked (strict) + )); + + register_routes(app); + + app.run(8080); + return 0; +} +``` + +## 2) Test with curl + +Run: + +``` bash +vix run body_limit_app.cpp +``` + +### Small body (OK) + +``` bash +curl -i -X POST http://localhost:8080/api/echo -H "Content-Type: text/plain" --data "hello" +``` + +### Large body (413 Payload Too Large) + +``` bash +python3 - <<'PY' +import requests +print(requests.post("http://localhost:8080/api/echo", data="x"*64).status_code) +PY +``` + +### GET is ignored by default + +``` bash +curl -i http://localhost:8080/api/ping +``` + +### Strict mode: reject chunked bodies + +This simulates a request with `Transfer-Encoding: chunked` (no +Content-Length).\ +If `allow_chunked=false`, the server returns **411 Length Required**. + +``` bash +curl -i -X POST http://localhost:8080/api/strict -H "Transfer-Encoding: chunked" -H "Content-Type: text/plain" --data "hello" +``` + +## 3) A safer production preset: only limit write methods + +Usually, you only want body limits on: + +- POST +- PUT +- PATCH + +Vix provides an alias for that: + +``` cpp +app.use("/", middleware::app::body_limit_write_dev(16)); +``` + +Meaning: + +- GET is not limited +- only write methods are limited to 16 bytes + +This is perfect for: + +- login endpoints +- webhook endpoints +- small JSON APIs +- protecting uploads unless you explicitly allow them + +## 4) Conditional body limit with `should_apply` (advanced but useful) + +Sometimes you want to limit only some paths. + +Example idea: + +- allow unlimited `/health` and `/api/ping` +- limit `/upload` and `/api/*` + +You can do it using the `should_apply` callback in `body_limit_dev()`. + +``` cpp +app.use("/", middleware::app::body_limit_dev( + 16, // max_bytes + false, // apply_to_get + true, // allow_chunked + [](const vix::middleware::Context& ctx){ + const auto m = ctx.req().method(); + if (m != "POST" && m != "PUT" && m != "PATCH") + return false; + + const auto p = ctx.req().path(); + return (p == "/upload" || p.rfind("/api/", 0) == 0); + } +)); +``` + +If you're a beginner, you can skip this section and use +`body_limit_write_dev()`. + +## Common beginner mistakes + +1) Setting the limit too low\ + If you limit to 32 bytes but your JSON request is 200 bytes, + everything fails. + +2) Forgetting chunked uploads exist\ + Some clients stream data. If you want strict enforcement, set + `allow_chunked=false`. + +3) Applying to GET by accident\ + Usually GET has no body. Keep `apply_to_get=false` unless you really + need it. + +## Summary + +- `body_limit_dev(max_bytes, apply_to_get, allow_chunked)` is the main + preset +- For most apps, use `body_limit_write_dev(max_bytes)` +- Too large body -\> 413 +- Strict mode with `allow_chunked=false` can return 411 + diff --git a/docs/examples/caching.md b/docs/examples/caching.md new file mode 100644 index 0000000..84e1a5a --- /dev/null +++ b/docs/examples/caching.md @@ -0,0 +1,214 @@ +# HTTP Caching Guide (Vix.cpp) — Beginner Friendly + +Caching means: **save a response**, then reuse it for the next request. + +This makes your API: + +- Faster +- Cheaper (less CPU) +- More scalable + +In this guide, we cache **GET** responses under `/api/*`. + +--- + +## What gets cached? + +Usually only: + +- GET requests +- Safe endpoints (no user-specific secrets unless you vary by headers) + +You typically do NOT cache: + +- POST +- PUT +- PATCH +- DELETE + +--- + +## Minimal Caching Example + +This caches GET `/api/*` for 30 seconds: + +```cpp +App app; + +app.use("/api/", middleware::app::http_cache({ + .ttl_ms = 30'000, + .allow_bypass = true, + .bypass_header = "x-vix-cache", + .bypass_value = "bypass", +})); + +app.get("/api/users", [](Request&, Response& res){ + res.text("users from origin"); +}); + +app.run(8080); +``` + +--- + +## Run + +```bash +vix run examples/http_cache_app_simple.cpp +``` + +--- + +## Test + +### 1) First request = MISS (origin) + +```bash +curl -i http://localhost:8080/api/users +``` + +### 2) Second request = HIT (cached) + +```bash +curl -i http://localhost:8080/api/users +``` + +### 3) Force origin (bypass) + +```bash +curl -i -H "x-vix-cache: bypass" http://localhost:8080/api/users +``` + +--- + +## Debug Header + +If you enable debug headers: + +```cpp +.add_debug_header = true, +.debug_header = "x-vix-cache-status", +``` + +You will see: + +- MISS +- HIT +- BYPASS + +Example: + +```bash +curl -i http://localhost:8080/api/users +``` + +Look for: + +x-vix-cache-status: HIT + +--- + +## Vary Headers (different cache per language) + +If you return different content depending on headers, you must vary the cache key: + +Example: `Accept-Language` + +```cpp +app.use("/api/", middleware::app::http_cache({ + .ttl_ms = 30'000, + .vary_headers = {"accept-language"}, + .add_debug_header = true, + .debug_header = "x-vix-cache-status", +})); +``` + +Test: + +```bash +curl -i -H "Accept-Language: fr" http://localhost:8080/api/users +curl -i -H "Accept-Language: en" http://localhost:8080/api/users +``` + +These should be **two separate cache entries**. + +--- + +## Custom Cache Injection (advanced but useful) + +Sometimes you want to inject your own cache instance. + +Example: + +- MemoryStore +- Custom eviction policy +- Shared cache between middlewares + +```cpp +auto cache = middleware::app::make_default_cache({ + .ttl_ms = 30'000, +}); + +app.use("/api/", middleware::app::http_cache_mw({ + .prefix = "/api/", + .only_get = true, + .ttl_ms = 30'000, + .cache = cache, + .add_debug_header = true, + .debug_header = "x-vix-cache-status", +})); +``` + +--- + +# Complete Example (copy-paste) + +This is a full working file you can run. + +Save as: `http_cache_app_simple.cpp` + +```cpp +#include +#include + +using namespace vix; + +static void register_routes(App& app) +{ + app.get("/", [](Request&, Response& res) + { + res.text("home (not cached)"); + }); + + app.get("/api/users", [](Request&, Response& res) + { + res.text("users from origin"); + }); +} + +int main() +{ + App app; + + // Cache GET requests under /api/* + app.use("/api/", middleware::app::http_cache({ + .ttl_ms = 30'000, + .allow_bypass = true, + .bypass_header = "x-vix-cache", + .bypass_value = "bypass", + })); + + register_routes(app); + + app.run(8080); + return 0; +} +``` + +## Run + +```bash +vix run http_cache_app_simple.cpp +``` + + diff --git a/docs/examples/compression.md b/docs/examples/compression.md new file mode 100644 index 0000000..5367f87 --- /dev/null +++ b/docs/examples/compression.md @@ -0,0 +1,177 @@ +# Compression Middleware Guide + +## What is HTTP Compression? + +Compression reduces the size of HTTP responses before sending them to the client. + +Benefits: + +- Faster network transfer +- Lower bandwidth usage +- Better performance on mobile or slow connections + +In Vix.cpp, compression is middleware-based and automatic. + +--- + +## How Compression Works + +1. Client sends: + Accept-Encoding: gzip, br + +2. Server checks: + - Is compression enabled? + - Is response size >= min_size? + - Does client support compression? + +3. If yes: + - Response is compressed + - Vary header is added (if enabled) + +--- + +## Minimal Example + +```cpp +App app; + +auto mw = vix::middleware::app::adapt_ctx( + vix::middleware::performance::compression({ + .min_size = 8, + .add_vary = true, + .enabled = true, + })); + +app.use(std::move(mw)); + +app.get("/x", [](Request&, Response& res) +{ + res.send(std::string(20, 'a')); +}); + +app.run(8080); +``` + +--- + +## Run + +```bash +vix run compression_app_simple.cpp +``` + +--- + +## Test With curl + +### 1) No Accept-Encoding + +```bash +curl -i http://localhost:8080/x +``` + +Result: +No compression applied. + +--- + +### 2) With Accept-Encoding + +```bash +curl -i -H "Accept-Encoding: gzip, br" http://localhost:8080/x +``` + +If body is large enough: +Compression is applied. + +--- + +### 3) Small body (below min_size) + +```bash +curl -i -H "Accept-Encoding: gzip" http://localhost:8080/small +``` + +No compression because body < min_size. + +--- + +## Important Options + +.min_size +Minimum response size required to compress. + +.enabled +Enable or disable compression. + +.add_vary +Adds: +Vary: Accept-Encoding + +This is important for proper caching behavior. + +--- + +# Complete Working Example + +Save as: compression_app_simple.cpp + +```cpp +#include +#include +#include + +#include +#include + +using namespace vix; + +int main() +{ + App app; + + // Install compression globally + auto mw = vix::middleware::app::adapt_ctx( + vix::middleware::performance::compression({ + .min_size = 8, + .add_vary = true, + .enabled = true, + })); + + app.use(std::move(mw)); + + app.get("/", [](Request&, Response& res) + { + res.send("Compression middleware installed."); + }); + + // Large body (should compress if client supports it) + app.get("/x", [](Request&, Response& res) + { + res.status(200).send(std::string(20, 'a')); + }); + + // Small body (no compression) + app.get("/small", [](Request&, Response& res) + { + res.status(200).send("aaaa"); + }); + + app.run(8080); + return 0; +} +``` + +--- + +## Summary + +Compression middleware: + +- Automatically checks Accept-Encoding +- Compresses only when useful +- Keeps your code clean +- Improves performance without changing route logic + +Vix.cpp keeps compression simple and explicit. + diff --git a/docs/examples/cookies.md b/docs/examples/cookies.md new file mode 100644 index 0000000..40a8f50 --- /dev/null +++ b/docs/examples/cookies.md @@ -0,0 +1,197 @@ +# Cookies Guide + +Cookies are small key/value strings stored by the browser and sent back on future requests. + +They are useful for: + +- sessions (login) +- user preferences +- CSRF tokens +- small state across requests + +In HTTP, the server sends a cookie using the header: + +Set-Cookie: name=value; options... + +The browser then sends it back using: + +Cookie: name=value; other=value + +--- + +## Minimal Example: Set a Cookie + +This endpoint sets a cookie named `hello` with value `vix`. + +```cpp +#include +#include + +using namespace vix; + +int main() +{ + App app; + + app.get("/cookie", [](Request&, Response& res) + { + vix::middleware::cookies::Cookie c; + c.name = "hello"; + c.value = "vix"; + c.max_age = 3600; // 1 hour + + vix::middleware::cookies::set(res, c); + + res.text("cookie set"); + }); + + app.run(8080); +} +``` + +--- + +## Run + +```bash +vix run cookie_app_simple.cpp +``` + +--- + +## Test With curl + +### 1) See the Set-Cookie header + +```bash +curl -i http://localhost:8080/cookie +``` + +You should see something like: + +Set-Cookie: hello=vix; Path=/; Max-Age=3600; HttpOnly; SameSite=Lax + +--- + +### 2) Send the cookie back manually + +```bash +curl -i http://localhost:8080/echo-cookie -H "Cookie: hello=vix" +``` + +(We add this route in the complete example below.) + +--- + +### 3) Let curl store cookies automatically + +```bash +# Save cookies into jar.txt +curl -i -c jar.txt http://localhost:8080/cookie + +# Reuse cookies from jar.txt +curl -i -b jar.txt http://localhost:8080/echo-cookie +``` + +--- + +## Most Important Cookie Options + +### Path +Controls which URLs receive the cookie. +Default is `/`. + +### Max-Age +How long the cookie lives in seconds. +- `max_age = 3600` => 1 hour +- `max_age = -1` => omit Max-Age (session cookie) + +### HttpOnly +If true, JavaScript cannot read the cookie. +This is safer for session cookies. + +### Secure +If true, cookie is only sent over HTTPS. +Use this in production. + +### SameSite +Helps protect against CSRF. + +Common values: + +- Lax (default, good for most apps) +- Strict (more locked down) +- None (required for cross-site cookies, but must use Secure=true) + +--- + +# Complete Example (set + read) + +Save as: cookie_app_simple.cpp + +```cpp +#include +#include + +using namespace vix; + +static void register_routes(App& app) +{ + // 1) Set a cookie + app.get("/cookie", [](Request&, Response& res) + { + vix::middleware::cookies::Cookie c; + c.name = "hello"; + c.value = "vix"; + c.max_age = 3600; + c.http_only = true; + c.secure = false; + c.same_site = "Lax"; + + vix::middleware::cookies::set(res, c); + res.text("cookie set"); + }); + + // 2) Read a cookie from the request + app.get("/echo-cookie", [](Request& req, Response& res) + { + auto v = vix::middleware::cookies::get(req, "hello"); + + res.json({ + "ok", true, + "cookie_hello", v ? *v : "", + "has_cookie", (bool)v + }); + }); +} + +int main() +{ + App app; + register_routes(app); + app.run(8080); + return 0; +} +``` + +--- + +## Quick Demo Commands + +```bash +vix run cookie_app_simple.cpp +``` + +```bash +curl -i -c jar.txt http://localhost:8080/cookie +curl -i -b jar.txt http://localhost:8080/echo-cookie +``` + +--- + +## Summary + +- Use `cookies::set(res, cookie)` to send Set-Cookie +- Use `cookies::get(req, "name")` to read Cookie +- In production, enable Secure=true + HTTPS for session cookies + diff --git a/docs/examples/cors.md b/docs/examples/cors.md new file mode 100644 index 0000000..4a87839 --- /dev/null +++ b/docs/examples/cors.md @@ -0,0 +1,216 @@ +# CORS Middleware Guide + +## What is CORS? + +CORS means **Cross-Origin Resource Sharing**. + +Browsers block frontend applications from calling APIs hosted on a different origin unless the server explicitly allows it. + +An origin is: +- protocol (http / https) +- domain +- port + +Example: +```bash +- http://localhost:5173 +- http://localhost:8080 +``` +These are different origins → CORS is required. + +--- + +## Why CORS exists + +Without CORS, any website could call your API using a logged-in user's browser. + +CORS allows the server to say: + +- Which origins are allowed +- Which HTTP methods are allowed +- Which headers are allowed +- Whether credentials are allowed + +--- + +# 1) Basic CORS Example + +This allows only `https://example.com` to call `/api`. + +```cpp +#include +#include + +using namespace vix; + +int main() +{ + App app; + + app.use("/api", middleware::app::cors_dev({"https://example.com"})); + + app.get("/api", [](Request &, Response &res) + { + res.header("X-Request-Id", "req_123"); + res.json({ "ok", true }); + }); + + app.run(8080); +} +``` + +### Test + +```bash +curl -i http://localhost:8080/api -H "Origin: https://example.com" +``` + +Expected: +- 200 OK +- Access-Control-Allow-Origin header present + +--- + +# 2) Strict CORS with Preflight (OPTIONS) + +Browsers send a **preflight request** before certain requests (POST, PUT, custom headers). + +This is an OPTIONS request. + +You should define explicit OPTIONS routes so the middleware can respond correctly. + +```cpp +#include +#include + +using namespace vix; + +int main() +{ + App app; + + app.use("/api", middleware::app::cors_dev({"https://example.com"})); + + app.options("/api", [](Request &, Response &res) + { + res.status(204).send(); + }); + + app.get("/api", [](Request &, Response &res) + { + res.json({ "ok", true }); + }); + + app.run(8080); +} +``` + +### Test allowed origin + +```bash +curl -i -X OPTIONS http://localhost:8080/api -H "Origin: https://example.com" -H "Access-Control-Request-Method: POST" +``` + +Expected: 204 + CORS headers. + +### Test blocked origin + +```bash +curl -i -X OPTIONS http://localhost:8080/api -H "Origin: https://evil.com" -H "Access-Control-Request-Method: POST" +``` + +Expected: 403 Forbidden. + +--- + +# 3) CORS in Production (Important) + +In real applications: + +- Apply CORS only on API routes (not public static routes) +- Combine with: + - Security headers + - CSRF protection + - Authentication +- Never allow "*" with credentials + +Correct order: + +```cpp +app.use("/api", middleware::app::security_headers_dev()); +app.use("/api", middleware::app::cors_dev({"https://example.com"})); +app.use("/api", middleware::app::csrf_dev()); +``` + +Order matters. + +--- + +# Common Beginner Mistakes + +1) Forgetting OPTIONS route +2) Allowing "*" in production +3) Not understanding that CORS is enforced by browsers, not curl +4) Confusing CORS with authentication (they are different) + +--- + +# Complete Working Example + +Save as: `cors_app_full.cpp` + +```cpp +#include +#include + +using namespace vix; + +int main() +{ + App app; + + app.use("/api", middleware::app::security_headers_dev()); + app.use("/api", middleware::app::cors_dev({"https://example.com"})); + + app.options("/api/data", [](Request &, Response &res) + { + res.status(204).send(); + }); + + app.get("/api/data", [](Request &, Response &res) + { + res.json({ + "ok", true, + "message", "CORS working" + }); + }); + + app.get("/", [](Request &, Response &res) + { + res.send("Public route"); + }); + + app.run(8080); +} +``` + +Run: + +```bash +vix run cors_app_full.cpp +``` + +--- + +# Summary + +CORS controls which origins can call your API. + +Use: +- `cors_dev()` for development +- Explicit OPTIONS routes +- Combine with CSRF and security headers +- Restrict allowed origins in production + +CORS is a browser security feature, not an authentication system. + diff --git a/docs/examples/csrf.md b/docs/examples/csrf.md new file mode 100644 index 0000000..4605fc1 --- /dev/null +++ b/docs/examples/csrf.md @@ -0,0 +1,297 @@ +# CSRF (Beginner Guide) + +This guide explains CSRF protection in Vix.cpp with tiny examples you can copy paste. + +CSRF means Cross Site Request Forgery. It matters when you use cookies for auth (sessions) because browsers attach cookies automatically to cross site requests. + +If your API accepts a state changing request (POST, PUT, PATCH, DELETE) and the browser sends cookies automatically, an attacker can trick a user into sending requests from another site unless you protect it. + +Vix.cpp CSRF middleware uses a simple model: + +- Server sets a CSRF token in a cookie (for example `csrf_token=abc`) +- Client must echo the same token in a header (for example `x-csrf-token: abc`) +- If cookie and header do not match, the request is rejected + +--- + +## 1) Minimal CSRF on `/api` prefix + +This is the smallest server: + +- `GET /api/csrf` sets a CSRF cookie and returns the token +- `POST /api/update` requires the header token to match the cookie token + +```cpp +/** + * + * @file csrf_strict_server.cpp - CSRF middleware example (Vix.cpp) + * + */ +#include +#include + +using namespace vix; + +int main() +{ + App app; + + // Protect all /api routes with CSRF + // Default cookie: csrf_token + // Default header: x-csrf-token + // Default protect_get: false + app.use("/api", middleware::app::csrf_dev()); + + // Issue token (cookie + JSON response for convenience) + app.get("/api/csrf", [](Request &, Response &res) + { + res.header("Set-Cookie", "csrf_token=abc; Path=/; SameSite=Lax"); + res.json({"csrf_token", "abc"}); + }); + + // Protected write route + app.post("/api/update", [](Request &, Response &res) + { + res.json({"ok", true, "message", "CSRF passed"}); + }); + + app.run(8080); + return 0; +} +``` + +### Test with curl (cookie jar) + +```bash +# 1) Get token (cookie) +curl -i -c cookies.txt http://localhost:8080/api/csrf + +# 2) FAIL: missing header +curl -i -b cookies.txt -X POST http://localhost:8080/api/update -d "x=1" + +# 3) FAIL: wrong token +curl -i -b cookies.txt -X POST http://localhost:8080/api/update \ + -H "x-csrf-token: wrong" -d "x=1" + +# 4) OK: header token matches cookie token +curl -i -b cookies.txt -X POST http://localhost:8080/api/update \ + -H "x-csrf-token: abc" -d "x=1" +``` + +Expected: + +- Missing token header -> 403 (or 401 depending on config) +- Wrong token -> 403 +- Matching token -> 200 + +--- + +## 2) CSRF + CORS (browser realistic) + +When you call an API from a browser on another origin, you usually need both: + +- CORS to allow the origin +- CSRF to protect cookie based write requests + +Important detail: browsers send OPTIONS preflight requests. If you want the CORS middleware to answer preflight correctly, define explicit OPTIONS routes for endpoints you call from the browser. + +### Minimal pattern + +```cpp +/** + * + * @file security_cors_csrf_server.cpp - CORS + CSRF (Vix.cpp) + * + */ +#include +#include + +using namespace vix; + +int main() +{ + App app; + + // Apply on /api prefix (order matters for a production style pipeline) + app.use("/api", middleware::app::cors_dev({"https://example.com"})); + app.use("/api", middleware::app::csrf_dev("csrf_token", "x-csrf-token", false)); + + // Explicit OPTIONS routes for browser preflight + app.options("/api/update", [](Request &, Response &res){ res.status(204).send(); }); + app.options("/api/csrf", [](Request &, Response &res){ res.status(204).send(); }); + + // Token endpoint + app.get("/api/csrf", [](Request &, Response &res) + { + res.header("Set-Cookie", "csrf_token=abc; Path=/; SameSite=Lax"); + res.json({"csrf_token", "abc"}); + }); + + // Protected write endpoint + app.post("/api/update", [](Request &, Response &res) + { + res.json({"ok", true, "message", "CORS ok + CSRF ok"}); + }); + + app.get("/", [](Request &, Response &res){ res.send("public"); }); + + app.run(8080); + return 0; +} +``` + +### Test preflight (curl) + +```bash +# Allowed origin preflight should return 204 + CORS headers +curl -i -X OPTIONS http://localhost:8080/api/update \ + -H "Origin: https://example.com" \ + -H "Access-Control-Request-Method: POST" \ + -H "Access-Control-Request-Headers: Content-Type, X-CSRF-Token" + +# Blocked origin preflight should return 403 +curl -i -X OPTIONS http://localhost:8080/api/update \ + -H "Origin: https://evil.com" \ + -H "Access-Control-Request-Method: POST" +``` + +--- + +## 3) Strict mode (protect GET too) + +Most APIs do not need CSRF on GET because GET should be read only. But if you expose a dangerous GET endpoint (bad design, but it happens), you can protect it too. + +```cpp +// Strict CSRF preset also protects GET +app.use("/api", middleware::app::csrf_strict_dev("csrf_token", "x-csrf-token")); +``` + +Rule of thumb: + +- protect_get = false is typical +- protect_get = true only if you have state changes on GET or you want extreme hardening + +--- + +## Common beginner mistakes + +1) Confusing CORS with CSRF +CORS controls which origins can read responses. CSRF controls whether a cookie based write request is allowed. + +2) Missing OPTIONS routes for browser calls +If preflight is not handled properly, the browser will block your requests even if your API works with curl. + +3) Cookie SameSite and Secure flags +- SameSite=Lax often blocks cookies in some cross site POST cases +- For true cross site cookies in modern browsers you need HTTPS and SameSite=None; Secure +- For local dev HTTP, SameSite=Lax is fine for curl demos + +4) CSRF is not needed for Authorization header auth +If you only use `Authorization: Bearer ` and do not rely on cookies, CSRF is usually not required because the browser will not attach Authorization headers automatically. + +--- + +## Production notes (practical) + +- Use CSRF when you use Session cookies. +- Prefer short lived CSRF tokens or rotate them on login. +- Combine with security headers and rate limiting on `/api`. + +Recommended order for `/api`: + +1) security headers +2) CORS +3) auth (session or jwt) +4) CSRF (if cookie based) +5) rate limit +6) business routes + +--- + +# Complete example (copy paste) + +This single file is a realistic production demo: + +- Security headers on all `/api` responses +- CORS for selected origins +- CSRF protection for write requests +- Explicit OPTIONS routes for browser preflight +- Two endpoints: `/api/csrf` and `/api/update` + +Save as: `security_cors_csrf_headers_server.cpp` + +```cpp +/** + * + * @file security_cors_csrf_headers_server.cpp - CORS + CSRF + Security Headers (Vix.cpp) + * + */ +#include +#include + +using namespace vix; + +static void register_options(App& app) +{ + auto options_noop = [](Request &, Response &res){ res.status(204).send(); }; + + app.options("/api/update", options_noop); + app.options("/api/csrf", options_noop); +} + +static void register_routes(App& app) +{ + app.get("/api/csrf", [](Request &, Response &res) + { + // Local dev: SameSite=Lax is fine. + // Cross site in browsers: use HTTPS + SameSite=None; Secure. + res.header("Set-Cookie", "csrf_token=abc; Path=/; SameSite=Lax"); + res.header("X-Request-Id", "req_csrf_1"); + res.json({"csrf_token", "abc"}); + }); + + app.post("/api/update", [](Request &, Response &res) + { + res.header("X-Request-Id", "req_update_1"); + res.json({"ok", true, "message", "CORS ok + CSRF ok + HEADERS ok"}); + }); + + app.get("/", [](Request &, Response &res){ res.send("public route"); }); +} + +int main() +{ + App app; + + // Apply on ALL /api/* + // Order matters: headers first, then CORS, then CSRF. + app.use("/api", middleware::app::security_headers_dev()); // HSTS off by default + app.use("/api", middleware::app::cors_dev({ + "http://localhost:5173", + "http://0.0.0.0:5173", + "https://example.com" + })); + app.use("/api", middleware::app::csrf_dev("csrf_token", "x-csrf-token", false)); + + register_options(app); + register_routes(app); + + app.run(8080); + return 0; +} +``` + +### Run + +```bash +vix run security_cors_csrf_headers_server.cpp +``` + +### Curl test + +```bash +curl -i -c cookies.txt http://localhost:8080/api/csrf +curl -i -b cookies.txt -X POST http://localhost:8080/api/update -d "x=1" +curl -i -b cookies.txt -X POST http://localhost:8080/api/update -H "x-csrf-token: abc" -d "x=1" +``` + diff --git a/docs/examples/db-production-guide.md b/docs/examples/db-production-guide.md new file mode 100644 index 0000000..7387148 --- /dev/null +++ b/docs/examples/db-production-guide.md @@ -0,0 +1,145 @@ +# Vix DB Production Guide + +This guide explains how to use the Vix C++ DB module safely in +production environments. + +It focuses on: + +- Connection pooling strategy +- Transactions best practices +- Error handling +- Migrations strategy +- Performance recommendations +- Security considerations + +# 1. Connection Pooling Strategy + +## Recommended Settings + +``` cpp +cfg.mysql.pool.min = 2; +cfg.mysql.pool.max = 16; +``` + +Guidelines: + +- Small apps: 2--4 connections +- Medium apps: 8--16 connections +- High traffic APIs: tune based on load testing + +Why pooling matters: + +- Avoids connection creation overhead +- Improves latency +- Reduces database stress + +Never create Database per request. Create it once at application +startup. + +# 2. Transaction Best Practices + +Always use RAII transactions: + +``` cpp +Transaction tx(db.pool()); +// work +tx.commit(); +``` + +Rules: + +- Keep transactions short +- Do not perform network calls inside transactions +- Commit explicitly +- Let rollback happen automatically on exceptions + +# 3. Error Handling + +Always wrap DB logic in try/catch: + +``` cpp +try { + Transaction tx(db.pool()); + // DB logic + tx.commit(); +} catch (const std::exception& e) { + log_error(e.what()); +} +``` + +Never expose raw DB errors to HTTP clients. + +# 4. Migrations in Production + +Strategy: + +- Use versioned migrations +- Never modify old migrations +- Always add new migration files + +Recommended flow: + +1. Deploy code +2. Run migrations +3. Restart services if needed + +Keep migrations idempotent. + +# 5. Performance Optimization + +Use prepared statements everywhere. + +Bad: + +``` cpp +"SELECT * FROM users WHERE id = " + id +``` + +Good: + +``` cpp +st->bind(1, id); +``` + +Add proper DB indexes: + +- Index foreign keys +- Index frequently filtered columns +- Avoid over-indexing + +Measure before optimizing. + +# 6. Security Best Practices + +- Never store plaintext passwords +- Use environment variables for DB credentials +- Limit DB user privileges +- Use TLS connections in production +- Rotate credentials periodically + +# 7. Deployment Checklist + +Before production: + +- Pool size tuned +- Indexes verified +- Migrations tested +- Error logging enabled +- Backups configured +- Monitoring enabled + +# Final Advice + +Database is critical infrastructure. + +Treat it as: + +- Stateful +- Sensitive +- Performance-critical + +Design carefully. Test under load. Monitor continuously. + +Vix DB gives you control. Production reliability depends on your +discipline. + diff --git a/docs/examples/db-quickstart.md b/docs/examples/db-quickstart.md new file mode 100644 index 0000000..203ed61 --- /dev/null +++ b/docs/examples/db-quickstart.md @@ -0,0 +1,87 @@ +# Vix DB Quickstart (5 Minutes) + +This is the fastest way to get started with the Vix C++ DB module. + +Goal: connect, create table, insert, query. + +## 1️⃣ Configure Database + +``` cpp +#include +#include + +using namespace vix::db; + +DbConfig make_cfg() +{ + DbConfig cfg; + cfg.engine = Engine::MySQL; + cfg.mysql.host = "tcp://127.0.0.1:3306"; + cfg.mysql.user = "root"; + cfg.mysql.password = ""; + cfg.mysql.database = "vixdb"; + cfg.mysql.pool.min = 1; + cfg.mysql.pool.max = 4; + return cfg; +} +``` + +## 2️⃣ Full Working Example + +``` cpp +int main() +{ + Database db(make_cfg()); + + Transaction tx(db.pool()); + + // Create table + tx.conn().prepare( + "CREATE TABLE IF NOT EXISTS users (" + "id BIGINT PRIMARY KEY AUTO_INCREMENT," + "name VARCHAR(255) NOT NULL," + "age INT NOT NULL)" + )->exec(); + + // Insert + auto insert = tx.conn().prepare( + "INSERT INTO users (name, age) VALUES (?, ?)" + ); + insert->bind(1, std::string("Alice")); + insert->bind(2, static_cast(22)); + insert->exec(); + + // Query + auto query = tx.conn().prepare( + "SELECT id, name, age FROM users" + ); + + auto rs = query->query(); + while (rs->next()) + { + const auto& row = rs->row(); + std::cout + << row.getInt64(0) << " " + << row.getString(1) << " " + << row.getInt64(2) << "\n"; + } + + tx.commit(); + return 0; +} +``` + +## 3️⃣ What You Just Used + +Database\ +Connection Pool\ +Prepared Statements\ +Transaction (RAII) + +## 🎯 That's it. + +You now have a working database setup in Vix C++. + +Next step: - Add migrations - Add error handling - Integrate with your +HTTP routes + diff --git a/docs/examples/db-transactions.md b/docs/examples/db-transactions.md new file mode 100644 index 0000000..8d01a8c --- /dev/null +++ b/docs/examples/db-transactions.md @@ -0,0 +1,153 @@ +# Vix DB Transactions Guide + +This guide explains how transactions work in Vix DB and how to use them +correctly in real applications. + +Transactions ensure that multiple database operations behave as a single +atomic unit. + +If something fails, everything is rolled back. + +# 1. What is a Transaction + +A transaction guarantees: + +- Atomicity +- Consistency +- Isolation +- Durability + +In simple terms: + +Either everything succeeds, or nothing is applied. + +# 2. Basic RAII Transaction + +``` cpp +#include +using namespace vix::db; + +void create_user(Database& db) +{ + Transaction tx(db.pool()); + + auto st = tx.conn().prepare( + "INSERT INTO users (name, age) VALUES (?, ?)" + ); + + st->bind(1, std::string("Alice")); + st->bind(2, static_cast(25)); + st->exec(); + + tx.commit(); +} +``` + +Important: + +If commit() is not called, rollback happens automatically when tx goes +out of scope. + +This is RAII safety. + +# 3. Multiple Operations in One Transaction + +``` cpp +Transaction tx(db.pool()); + +// Insert user +auto insert_user = tx.conn().prepare( + "INSERT INTO users (name, age) VALUES (?, ?)" +); +insert_user->bind(1, "Bob"); +insert_user->bind(2, static_cast(30)); +insert_user->exec(); + +// Insert profile +auto insert_profile = tx.conn().prepare( + "INSERT INTO profiles (user_id, bio) VALUES (?, ?)" +); +insert_profile->bind(1, static_cast(1)); +insert_profile->bind(2, "Engineer"); +insert_profile->exec(); + +tx.commit(); +``` + +If the second insert fails, both inserts are rolled back. + +# 4. Automatic Rollback on Exception + +``` cpp +try +{ + Transaction tx(db.pool()); + + auto st = tx.conn().prepare("DELETE FROM users WHERE id = ?"); + st->bind(1, static_cast(5)); + st->exec(); + + throw std::runtime_error("Something failed"); + + tx.commit(); +} +catch (...) +{ + // No manual rollback needed +} +``` + +Because commit() was not called, rollback happens automatically. + +# 5. Best Practices + +Keep transactions short. + +Do not: + +- Perform HTTP calls inside a transaction +- Wait for external APIs +- Sleep or block unnecessarily + +Do: + +- Execute DB operations quickly +- Commit immediately +- Handle errors properly + +# 6. Nested Transactions + +Avoid nested transactions unless your database supports savepoints. + +If needed, use explicit savepoint logic at the SQL level. + +# 7. Production Advice + +Transactions lock resources. + +Long transactions reduce concurrency and performance. + +Monitor: + +- Slow queries +- Deadlocks +- Lock wait timeouts + +Design carefully. + +# Summary + +Transactions in Vix DB are: + +- RAII safe +- Automatic rollback +- Explicit commit required + +Use them for: + +- Multi-step writes +- Data integrity guarantees +- Critical operations + +Correct transaction usage is essential for production-grade systems. + diff --git a/docs/examples/db.md b/docs/examples/db.md new file mode 100644 index 0000000..7aba174 --- /dev/null +++ b/docs/examples/db.md @@ -0,0 +1,170 @@ +# Vix DB Module – Beginner Guide + +This guide introduces the Vix C++ database module in a simple and practical way. + +The goal is to help beginners understand: + +- How to connect to a database +- How to run queries +- How to use transactions +- How to run migrations + +--- + +## 1. Basic Connection (MySQL) + +```cpp +#include +#include + +using namespace vix::db; + +int main() +{ + DbConfig cfg; + cfg.engine = Engine::MySQL; + cfg.mysql.host = "tcp://127.0.0.1:3306"; + cfg.mysql.user = "root"; + cfg.mysql.password = ""; + cfg.mysql.database = "vixdb"; + + Database db(cfg); + + auto conn = db.pool().acquire(); + if (!conn->ping()) + { + std::cerr << "DB ping failed\n"; + return 1; + } + + std::cout << "DB connected successfully\n"; + return 0; +} +``` + +What happens here: + +1. We configure the database connection. +2. We create a Database object. +3. We acquire a connection from the pool. +4. We ping the database to verify connectivity. + +--- + +## 2. Simple Query with Prepared Statements + +```cpp +auto conn = db.pool().acquire(); +auto st = conn->prepare("SELECT id, name FROM users WHERE age > ?"); + +st->bind(1, 18); + +auto rs = st->query(); +while (rs->next()) +{ + const auto &row = rs->row(); + std::cout << row.getInt64(0) << " " + << row.getString(1) << "\n"; +} +``` + +Why prepared statements? + +- Prevent SQL injection +- Handle type-safe parameter binding +- Improve performance + +--- + +## 3. Transactions (RAII Style) + +Transactions are automatically rolled back if not committed. + +```cpp +Transaction tx(db.pool()); + +auto st = tx.conn().prepare( + "INSERT INTO users (name, age) VALUES (?, ?)" +); + +st->bind(1, std::string("Alice")); +st->bind(2, static_cast(20)); +st->exec(); + +tx.commit(); +``` + +If commit() is not called, the transaction rolls back automatically. + +--- + +## 4. Code-Based Migration + +```cpp +class CreateUsersTable final : public Migration +{ +public: + std::string id() const override { return "2026-01-22-create-users"; } + + void up(Connection &c) override + { + c.prepare( + "CREATE TABLE IF NOT EXISTS users (" + "id BIGINT PRIMARY KEY AUTO_INCREMENT," + "name VARCHAR(255) NOT NULL," + "age INT NOT NULL);" + )->exec(); + } + + void down(Connection &c) override + { + c.prepare("DROP TABLE IF EXISTS users;")->exec(); + } +}; +``` + +Migrations allow you to version your schema safely. + +--- + +## 5. File-Based Migrations + +Place files inside: + +``` +migrations/ + 001_create_users.up.sql + 001_create_users.down.sql +``` + +Then run: + +```cpp +FileMigrationsRunner runner(tx.conn(), "migrations"); +runner.applyAll(); +``` + +--- + +## Key Concepts for Beginners + +Connection Pool: +- Reuses database connections +- Improves performance + +Prepared Statements: +- Use ? placeholders +- Bind values safely + +Transactions: +- Ensure atomic operations +- Commit or rollback + +Migrations: +- Keep database schema versioned +- Safe evolution of your database + +--- + +You are now ready to use Vix DB in real applications. + diff --git a/docs/examples/delete_user.md b/docs/examples/delete_user.md deleted file mode 100644 index 9dec9fe..0000000 --- a/docs/examples/delete_user.md +++ /dev/null @@ -1,30 +0,0 @@ -# Example — delete_user.cpp - -```cpp -#include -#include -#include - -using namespace vix; -namespace J = vix::json; - -int main() -{ - App app; - - // DELETE /users/{id} - app.del("/users/{id}", [](Request &req, Response &res) - { - const std::string id = req.param("id"); - - // In a real app you'd remove the resource from DB or memory here - res.json({ - "action", "delete", - "status", "deleted", - "user_id", id - }); }); - - app.run(8080); - return 0; -} -``` diff --git a/docs/examples/error-handling.md b/docs/examples/error-handling.md new file mode 100644 index 0000000..73dff55 --- /dev/null +++ b/docs/examples/error-handling.md @@ -0,0 +1,210 @@ +# ORM Example Guide: Error Handling + +This guide explains the `error_handling` example. + +Goal: - Show what happens when the database connection is invalid - +Demonstrate how to catch `DBError` cleanly - Encourage safe production +error handling patterns + +This example intentionally uses a wrong database name to trigger errors. + +# 1. What this example demonstrates + +You will learn: + +- Where database errors happen (factory, pool warmup, queries) +- Why `pool.warmup()` is important +- How to catch `DBError` separately from other exceptions +- How to write safe CLI-style error handling + +# 2. Full Example Code + +``` cpp +#include + +#include +#include + +using namespace vix::orm; + +int main(int argc, char **argv) +{ + (void)argc; + (void)argv; + + try + { + // Intentionally wrong DB name to show error handling + const std::string host = "tcp://127.0.0.1:3306"; + const std::string user = "root"; + const std::string pass = ""; + const std::string db = "db_does_not_exist"; + + auto factory = make_mysql_factory(host, user, pass, db); + + PoolConfig cfg; + cfg.min = 1; + cfg.max = 8; + + ConnectionPool pool{factory, cfg}; + + // will throw if factory returns invalid connection (recommended after our warmup fix), + // or later when first query fails. + pool.warmup(); + + UnitOfWork uow{pool}; + auto &con = uow.conn(); + + auto st = con.prepare("SELECT 1"); + (void)st->exec(); + + std::cout << "[INFO] This message may not be reached if connection fails.\n"; + return 0; + } + catch (const DBError &e) + { + std::cerr << "[DBError] " << e.what() << "\n"; + return 1; + } + catch (const std::exception &e) + { + std::cerr << "[std::exception] " << e.what() << "\n"; + return 1; + } +} +``` + +# 3. Step by Step Explanation + +## 3.1 Intentional failure + +``` cpp +const std::string db = "db_does_not_exist"; +``` + +This database does not exist, so MySQL will fail during connection or +query. + +This is a controlled demo to show how errors look and how to handle +them. + +## 3.2 Create factory and pool + +``` cpp +auto factory = make_mysql_factory(host, user, pass, db); +ConnectionPool pool{factory, cfg}; +``` + +The factory creates MySQL connections. The pool stores and reuses them. + +## 3.3 Why warmup matters + +``` cpp +pool.warmup(); +``` + +Warmup tries to pre-create connections early. + +Benefits: + +- Fail fast at startup (not during first real request) +- Catch invalid credentials immediately +- Avoid "first request slow" latency + +In this example, warmup is expected to throw because the database is +invalid. + +## 3.4 UnitOfWork and query + +``` cpp +UnitOfWork uow{pool}; +auto st = con.prepare("SELECT 1"); +st->exec(); +``` + +If warmup did not throw, the next likely failure point is the first +query. + +Either way, errors become exceptions. + +# 4. Catching DBError vs std::exception + +``` cpp +catch (const DBError &e) +{ + std::cerr << "[DBError] " << e.what() << "\n"; +} +``` + +`DBError` is the dedicated Vix ORM exception type for database errors, +such as: + +- Connection failures +- Authentication failures +- Database not found +- Query syntax errors +- Constraint violations + +Then a generic catch for everything else: + +``` cpp +catch (const std::exception &e) +{ + std::cerr << "[std::exception] " << e.what() << "\n"; +} +``` + +This separation makes logs easier to understand. + +# 5. Production Advice + +## 5.1 Fail fast at startup + +Call `pool.warmup()` in your application startup: + +- you detect DB misconfiguration immediately +- you avoid serving traffic with broken DB access + +## 5.2 Do not leak DB errors to clients + +If you build an HTTP API: + +- log full DBError internally +- return a generic error to clients + +Example mapping: + +- DBError: "database unavailable" +- client message: "temporary server issue" + +## 5.3 Add context in logs + +In production, log: + +- host +- database name +- error message +- operation context (connect, warmup, query) + +Do not log secrets like passwords. + +# 6. Typical causes of DBError + +- Wrong host or port +- Wrong username/password +- Database missing +- Permissions missing +- Network blocked (firewall) +- Connection limit reached +- Invalid SQL syntax +- Foreign key constraint failure + +# Summary + +This example demonstrates: + +- How to force a predictable DB failure +- Why warmup is important +- How to catch DBError cleanly +- How to structure safe production error handling + diff --git a/docs/examples/errors.md b/docs/examples/errors.md new file mode 100644 index 0000000..c7d5587 --- /dev/null +++ b/docs/examples/errors.md @@ -0,0 +1,235 @@ +# Errors + Status Codes + +This section shows how to handle errors and HTTP status codes in Vix.cpp. + +Each example is minimal and self-contained. + +--- + +## 1. Simple 404 Response + +Return a JSON error with explicit status. + +```cpp +#include + +using namespace vix; + +int main() +{ + App app; + + app.get("/not-found", [](Request&, Response& res) + { + res.status(404).json({ + "ok", false, + "error", "Resource not found" + }); + }); + + app.run(8080); + return 0; +} +``` + +Test: + + curl -i http://localhost:8080/not-found + +--- + +## 2. 400 Bad Request (Validation-style) + +```cpp +#include + +using namespace vix; + +int main() +{ + App app; + + app.get("/bad", [](Request&, Response& res) + { + res.status(400).json({ + "ok", false, + "error", "Invalid input" + }); + }); + + app.run(8080); + return 0; +} +``` + +--- + +## 3. 401 Unauthorized + +```cpp +#include + +using namespace vix; + +int main() +{ + App app; + + app.get("/private", [](Request&, Response& res) + { + res.status(401).json({ + "ok", false, + "error", "Unauthorized" + }); + }); + + app.run(8080); + return 0; +} +``` + +--- + +## 4. 403 Forbidden + +```cpp +#include + +using namespace vix; + +int main() +{ + App app; + + app.get("/admin", [](Request&, Response& res) + { + res.status(403).json({ + "ok", false, + "error", "Forbidden" + }); + }); + + app.run(8080); + return 0; +} +``` + +--- + +## 5. 500 Internal Server Error + +You can manually return 500: + +```cpp +#include + +using namespace vix; + +int main() +{ + App app; + + app.get("/error", [](Request&, Response& res) + { + res.status(500).json({ + "ok", false, + "error", "Internal Server Error" + }); + }); + + app.run(8080); + return 0; +} +``` + +--- + +## 6. Throwing an Exception + +If a handler throws, Vix will convert it into a 500 response +(depending on your dev/production configuration). + +```cpp +#include +#include + +using namespace vix; + +int main() +{ + App app; + + app.get("/boom", [](Request&, Response&) + { + throw std::runtime_error("Something went wrong"); + return "unreachable"; + }); + + app.run(8080); + return 0; +} +``` + +--- + +## 7. Using set_status() + send() + +```cpp +#include + +using namespace vix; + +int main() +{ + App app; + + app.get("/created", [](Request&, Response& res) + { + res.set_status(201); + res.send("Created"); + }); + + app.run(8080); + return 0; +} +``` + +--- + +## 8. Returning JSON Automatically + +If nothing is sent explicitly, returning JSON auto-sends the response. + +```cpp +#include +#include + +using namespace vix; + +int main() +{ + App app; + + app.get("/auto-error", [](Request&, Response&) + { + return vix::json::o( + "ok", false, + "error", "Auto-sent error" + ); + }); + + app.run(8080); + return 0; +} +``` + +--- + +## What this teaches + +- How to set HTTP status codes +- How to return structured JSON errors +- How exceptions behave +- The difference between res.status() and set_status() +- Auto-send return style + diff --git a/docs/examples/etag.md b/docs/examples/etag.md new file mode 100644 index 0000000..667feaa --- /dev/null +++ b/docs/examples/etag.md @@ -0,0 +1,215 @@ +# ETag Middleware Guide + +## What is an ETag? + +ETag stands for **Entity Tag**. + +It is an HTTP response header used for **cache validation**. + +Instead of re-downloading a resource every time, the client can say: + +> "I already have version X. Has it changed?" + +If not changed → server replies: + + 304 Not Modified + +No body is sent.\ +This saves bandwidth and improves performance. + +## How ETag Works + +1. Server sends response with: + +```{=html} + +``` + ETag: "abc123" + +2. Client stores it. + +3. On next request, client sends: + +```{=html} + +``` + If-None-Match: "abc123" + +4. Server compares: + +- If same → `304 Not Modified` +- If different → `200 OK` with new body + new ETag + +# Minimal Example + +Save as: `etag_app_simple.cpp` + +``` cpp +#include +#include +#include + +#include +#include + +using namespace vix; + +static void print_help() +{ + std::cout + << "Vix ETag example running:\n" + << " http://localhost:8080/x\n\n" + << "Try:\n" + << " curl -i http://localhost:8080/x\n" + << " curl -i -H 'If-None-Match: ' http://localhost:8080/x\n" + << " curl -I http://localhost:8080/x\n"; +} + +int main() +{ + App app; + + // Install ETag middleware globally + auto mw = vix::middleware::app::adapt_ctx( + vix::middleware::performance::etag({ + .weak = true, + .add_cache_control_if_missing = false, + .min_body_size = 1 + })); + + app.use(std::move(mw)); + + app.get("/x", [](Request &, Response &res) + { + res.text("Hello ETag world"); + }); + + app.head("/x", [](Request &, Response &res) + { + res.status(200); + }); + + print_help(); + app.run(8080); + return 0; +} +``` + +## Run + +``` bash +vix run etag_app_simple.cpp +``` + +## Test Step by Step + +### 1) First request + +``` bash +curl -i http://localhost:8080/x +``` + +Response will contain: + + ETag: W/"..." + +Copy that value. + +### 2) Send If-None-Match + +``` bash +curl -i -H 'If-None-Match: W/"...your_etag_here..."' http://localhost:8080/x +``` + +If unchanged → + + 304 Not Modified + +No body returned. + +### 3) HEAD request + +``` bash +curl -I http://localhost:8080/x +``` + +HEAD returns headers only.\ +ETag is still calculated. + +# Middleware Options Explained + +``` cpp +vix::middleware::performance::etag({ + .weak = true, + .add_cache_control_if_missing = false, + .min_body_size = 1 +}); +``` + +### weak + +- `true` → `W/"hash"` +- `false` → `"hash"` (strong) + +Weak ETags are recommended for dynamic APIs. + +### add_cache_control_if_missing + +If true and response has no Cache-Control header, middleware can inject +a default one. + +### min_body_size + +Only compute ETag if body \>= this size. + +Helps skip tiny responses. + +# When to Use ETag + +Use ETag for: + +- JSON APIs +- Static content +- CDN friendly responses +- Any idempotent GET endpoint + +Avoid for: + +- Non-deterministic responses +- Streaming endpoints + +# Production Pattern + +Typical production setup: + + Compression + ETag + Cache-Control + +Example: + +``` cpp +app.use(adapt_ctx(compression(...))); +app.use(adapt_ctx(etag({...}))); +``` + +Order matters. + +# Common Mistakes + +1. Forgetting GET route (HEAD alone is not enough) +2. Sending different body order (JSON fields shuffled) +3. Mixing weak/strong inconsistently +4. Not handling 304 correctly on client side + +# Summary + +ETag gives you: + +- Conditional requests +- Bandwidth savings +- Faster responses +- Cleaner caching logic + +Minimal, powerful, and production-ready. + diff --git a/docs/examples/form.md b/docs/examples/form.md new file mode 100644 index 0000000..d0aed40 --- /dev/null +++ b/docs/examples/form.md @@ -0,0 +1,134 @@ +# Form & Body Parsers Guide + +Beginner-friendly guide for: + +- application/x-www-form-urlencoded +- application/json +- multipart/form-data + +Each section includes: - Minimal server - curl tests - Expected behavior + +# 1) Form Parser (application/x-www-form-urlencoded) + +## Minimal example + +``` cpp +#include +#include + +using namespace vix; + +int main() +{ + App app; + + app.use("/form", middleware::app::form_dev(128)); + + app.post("/form", [](Request &req, Response &res) + { + auto& fb = req.state(); + + auto it = fb.fields.find("b"); + res.send(it == fb.fields.end() ? "" : it->second); + }); + + app.run(8080); +} +``` + +## Test + +``` bash +curl -i -X POST http://localhost:8080/form \ + -H "Content-Type: application/x-www-form-urlencoded" \ + --data "a=1&b=hello+world" +``` + +# 2) JSON Parser (application/json) + +## Minimal example + +``` cpp +#include +#include + +using namespace vix; + +int main() +{ + App app; + + app.use("/json", middleware::app::json_dev(256, true, true)); + + app.post("/json", [](Request &req, Response &res) + { + auto &jb = req.state(); + res.json({ + "ok", true, + "raw", jb.value.dump() + }); + }); + + app.run(8080); +} +``` + +## Test + +``` bash +curl -i -X POST http://localhost:8080/json \ + -H "Content-Type: application/json" \ + --data '{"x":1}' +``` + +# 3) Multipart Parser (File Uploads) + +## Minimal example + +``` cpp +#include +#include + +using namespace vix; + +int main() +{ + App app; + + app.use("/mp", middleware::app::multipart_save_dev("uploads")); + + app.post("/mp", [](Request &req, Response &res) + { + auto &form = req.state(); + res.json(middleware::app::multipart_json(form)); + }); + + app.run(8080); +} +``` + +## Test + +``` bash +curl -i -X POST http://localhost:8080/mp \ + -F "a=1" -F "file=@/etc/hosts" +``` + +Files are saved to ./uploads/ + +# Error Codes Summary + + Code Meaning + ------ -------------------------- + 400 Invalid body + 413 Payload too large + 415 Unsupported Content-Type + +# Recommended Usage + +- Use form_dev() for HTML forms +- Use json_dev() for APIs +- Use multipart_save_dev() for file uploads + +Keep max_bytes small in production. + diff --git a/docs/examples/graceful-shutdown.md b/docs/examples/graceful-shutdown.md new file mode 100644 index 0000000..50a28d2 --- /dev/null +++ b/docs/examples/graceful-shutdown.md @@ -0,0 +1,197 @@ +# Graceful Shutdown (Vix.cpp) + +Graceful shutdown ensures your server: + +- Stops accepting new connections +- Finishes in-flight requests +- Releases resources safely +- Shuts down cleanly without corruption + +Vix provides built-in support for safe shutdown using: + +- `run()` +- `listen()` +- `close()` +- `wait()` +- `request_stop_from_signal()` + +# 1. Basic Blocking Mode (run) + +The simplest mode: + +``` cpp +#include +using namespace vix; + +int main() +{ + App app; + + app.get("/", [](Request&, Response& res) { + res.json({"ok", true}); + }); + + app.run(8080); // blocks until stopped +} +``` + +Press `Ctrl+C` to stop the server. + +Vix will stop the server loop cleanly. + +# 2. Background Mode (listen + wait) + +Use this when you need more control. + +``` cpp +#include +using namespace vix; + +int main() +{ + App app; + + app.get("/", [](Request&, Response& res) { + res.send("Running..."); + }); + + app.listen(8080, [](){ + console.log("Server is running on http://localhost:8080"); + }); + + app.wait(); // wait until shutdown +} +``` + +# 3. Programmatic Shutdown + +You can stop the server from inside the application: + +``` cpp +app.get("/stop", [&](Request&, Response& res) { + res.send("Stopping..."); + app.close(); +}); +``` + +`close()` requests graceful stop. Use `wait()` if running in background +mode. + +# 4. Signal-Safe Shutdown (Production Pattern) + +For production systems, handle SIGINT / SIGTERM: + +``` cpp +#include +#include + +using namespace vix; + +static App* g_app = nullptr; + +void signal_handler(int) +{ + if (g_app) + g_app->request_stop_from_signal(); +} + +int main() +{ + App app; + g_app = &app; + + std::signal(SIGINT, signal_handler); + std::signal(SIGTERM, signal_handler); + + app.get("/", [](Request&, Response& res) { + res.json({"ok", true}); + }); + + app.run(8080); +} +``` + +This is safe to call inside signal handlers. + +# 5. Shutdown Callback + +You can run cleanup logic: + +``` cpp +app.set_shutdown_callback([]() { + vix::console.info("Server shutting down..."); +}); +``` + +Useful for: + +- Closing database connections +- Flushing logs +- Stopping background workers +- Releasing file handles + +# 6. Production Best Practices + +When building serious systems: + +- Use `listen()` + `wait()` +- Install signal handlers +- Avoid long blocking tasks +- Ensure DB pools close cleanly +- Stop background threads before exit + +# 7. Complete Production Example + +``` cpp +#include +#include + +using namespace vix; + +static App* g_app = nullptr; + +void signal_handler(int) +{ + if (g_app) + g_app->request_stop_from_signal(); +} + +int main() +{ + App app; + g_app = &app; + + std::signal(SIGINT, signal_handler); + std::signal(SIGTERM, signal_handler); + + app.set_shutdown_callback([]() { + vix::console.info("Cleaning resources..."); + }); + + app.get("/", [](Request&, Response& res) { + res.json({"message", "Graceful server running"}); + }); + + app.listen(8080); + app.wait(); + + return 0; +} +``` + +# Summary + +Graceful shutdown in Vix is: + +- Explicit +- Signal-safe +- Non-blocking +- Production-ready + +Use: + +- `run()` for simple apps +- `listen()` + `wait()` for production +- `request_stop_from_signal()` for signal handling +- `set_shutdown_callback()` for cleanup + diff --git a/docs/examples/group-advanced-patterns.md b/docs/examples/group-advanced-patterns.md new file mode 100644 index 0000000..2bddfff --- /dev/null +++ b/docs/examples/group-advanced-patterns.md @@ -0,0 +1,198 @@ +# Advanced Group Patterns + +This guide explains advanced architectural patterns using `group()` in Vix.cpp. + +These patterns are useful for: + +- Large APIs +- Multi-tenant systems +- Versioned APIs +- Modular backend design +- Enterprise-grade structure + +--- + +## 1) Versioned API Pattern (v1 / v2) + +Structure: + +/api/v1/... +/api/v2/... + +Example: + +```cpp +App app; + +app.group("/api", [&](App::Group& api) { + + api.group("/v1", [&](App::Group& v1) { + v1.get("/users", [](Request&, Response& res){ + res.json({"version","v1","data","users list"}); + }); + }); + + api.group("/v2", [&](App::Group& v2) { + v2.get("/users", [](Request&, Response& res){ + res.json({"version","v2","data","users list (new schema)"}); + }); + }); + +}); + +app.run(8080); +``` + +Why this is powerful: +- Clean API evolution +- Backward compatibility +- Zero route collision + +--- + +## 2) Multi‑Tenant Group Pattern + +Structure: + +/tenant/{id}/... + +You isolate logic per tenant. + +```cpp +app.group("/tenant", [&](App::Group& tenant){ + + tenant.get("/{id}/dashboard", [](Request& req, Response& res){ + auto id = req.param("id"); + res.json({"tenant", id}); + }); + +}); +``` + +Best practice: +- Inject tenant ID into request state via middleware +- Apply tenant‑level RBAC inside the group + +--- + +## 3) Admin Isolation Pattern + +Separate public API from admin API. + +```cpp +app.group("/admin", [&](App::Group& admin){ + + admin.use(middleware::app::jwt_auth("secret")); + admin.use(middleware::app::rbac_admin()); + + admin.get("/stats", [](Request&, Response& res){ + res.json({"admin", true}); + }); + +}); +``` + +Benefits: +- All admin logic isolated +- Security applied once +- No repetition + +--- + +## 4) Feature Module Mounting + +Simulate plugin-style mounting. + +```cpp +void mountUsers(App::Group& api) +{ + api.group("/users", [&](App::Group& users){ + users.get("/", [](Request&, Response& res){ + res.send("Users list"); + }); + }); +} + +int main() +{ + App app; + auto api = app.group("/api"); + + mountUsers(api); + + app.run(8080); +} +``` + +This allows: +- Clean separation by domain +- Team-based development +- Independent modules + +--- + +## 5) Security Layer Stacking + +Order matters: + +Headers → CORS → Auth → RBAC → Rate limit + +```cpp +api.use(middleware::app::security_headers_dev()); +api.use(middleware::app::cors_dev()); +api.use(middleware::app::jwt_auth("secret")); +api.use(middleware::app::rbac_admin()); +api.use(middleware::app::rate_limit_dev(60, std::chrono::minutes(1))); +``` + +This ensures: +- Preflight handled correctly +- Auth before permission checks +- Protection against abuse + +--- + +## 6) Enterprise Folder Architecture + +Recommended structure: + +``` +/src + /api + v1_users.cpp + v1_orders.cpp + v2_users.cpp + /admin + dashboard.cpp + /modules + billing.cpp + analytics.cpp +``` + +Each file exports a mount function: + +```cpp +void mount(App::Group& group); +``` + +Main app only wires modules together. + +--- + +## Key Takeaway + +`group()` is not just route prefixing. + +It is an architectural boundary: + +- Security boundary +- Version boundary +- Tenant boundary +- Domain boundary + +Mastering groups = building scalable backend systems. + +--- + +Vix.cpp Advanced Routing Patterns. + diff --git a/docs/examples/group-api.md b/docs/examples/group-api.md new file mode 100644 index 0000000..3e8d15a --- /dev/null +++ b/docs/examples/group-api.md @@ -0,0 +1,177 @@ +# Route Groups Guide + +This guide explains how to use **route groups** in Vix.cpp. + +Groups let you: + +- Share a common URL prefix +- Apply middleware to multiple routes at once +- Organize APIs cleanly +- Nest sub-groups (ex: /api/admin) + +--- + +# 1) What is a Route Group? + +Instead of writing: +```bash + /api/users + /api/products + /api/admin/dashboard +``` +You define: +```cpp + app.group("/api", ...) +``` +And all routes inside automatically start with `/api`. + +--- + +# 2) Basic Group Example + +```cpp +#include +using namespace vix; + +int main() +{ + App app; + + app.group("/api", [&](App::Group &api) + { + api.get("/ping", [](Request&, Response& res){ + res.json({"ok", true}); + }); + }); + + app.run(8080); +} +``` + +Now: +```bash + GET /api/ping +``` +--- + +# 3) Protecting a Route Inside a Group + +You can protect only one route: + +```cpp +api.protect("/secure", middleware::app::api_key_dev("secret")); + +api.get("/secure", [](Request &req, Response &res) +{ + auto &k = req.state(); + res.json({ + "ok", true, + "api_key", k.value + }); +}); +``` + +Test: +```bash + curl -i http://localhost:8080/api/secure + curl -i -H "x-api-key: secret" http://localhost:8080/api/secure +``` +--- + +# 4) Nested Groups (Admin Example) + +Groups can be nested: + +```cpp +api.group("/admin", [&](App::Group &admin) +{ + admin.use(middleware::app::jwt_auth("dev_secret")); + admin.use(middleware::app::rbac_admin()); + + admin.get("/dashboard", [](Request &req, Response &res) + { + auto &authz = req.state(); + res.json({ + "ok", true, + "sub", authz.subject, + "role", "admin" + }); + }); +}); +``` + +This creates: +```bash + /api/admin/dashboard +``` +All admin routes share the same middleware. + +--- + +# 5) Builder Style Group + +You can also create a group first: + +```cpp +auto api = app.group("/api"); + +api.get("/public", [](Request &, Response &res) +{ + res.send("Public endpoint"); +}); + +api.use(middleware::app::api_key_dev("secret")); + +api.get("/secure", [](Request &req, Response &res) +{ + auto &k = req.state(); + res.json({"ok", true}); +}); +``` + +Everything after `api.use(...)` is protected. + +--- + +# 6) When to Use Groups + +Use groups when: + +- You build REST APIs +- You want prefix-based protection +- You need nested areas (admin, internal, public) +- You want clean structure + +--- + +# 7) Recommended Production Pattern + +Typical structure: +```bash + /api + /public + /auth + /admin + /internal +``` +Example: +```cpp + app.group("/api", ...) + api.group("/admin", ...) + api.group("/auth", ...) +``` +Keep middleware close to the group it protects. + +--- + +# Summary + +Groups in Vix.cpp give you: + +- Clean URL organization +- Middleware inheritance +- Nested API design +- Scalable backend structure + +They are essential for real-world applications. + diff --git a/docs/examples/group-vs-prefix.md b/docs/examples/group-vs-prefix.md new file mode 100644 index 0000000..31467c9 --- /dev/null +++ b/docs/examples/group-vs-prefix.md @@ -0,0 +1,389 @@ +# Groups vs Prefix Middleware + +This guide explains the difference between: + +- Prefix middleware: `app.use("/api", mw)` and `middleware::app::install(app, "/api/", mw)` +- Route groups: `app.group("/api", ...)` and `api.use(...)` / `api.protect(...)` + +Both are valid. The best choice depends on how you want to structure your routes. + +------------------------------------------------------------------------ + +## 1) Mental model + +### Prefix middleware +You attach middleware to a path prefix. + +- The middleware runs for every route whose path starts with that prefix. +- You usually keep route registration at the top level `app.get(...)`, `app.post(...)`. + +Example: +- Apply API key to everything under `/api/` + +### Groups +A group is a scoped router builder. + +- You register routes inside a `group("/api", ...)` block. +- You can install middleware once on the group and it applies to routes in that group. +- You can nest groups to model your API structure. + +Example: +- `/api` group contains public routes +- `/api/admin` group contains protected admin routes + +------------------------------------------------------------------------ + +## 2) Prefix middleware patterns + +### 2.1) Protect a whole prefix with `app.use()` +This is the simplest prefix pattern. + +```cpp +#include +#include +using namespace vix; + +int main() +{ + App app; + + // Everything under /api is protected by API key + app.use("/api", middleware::app::api_key_dev("secret")); + + app.get("/", [](Request&, Response& res){ + res.send("home"); + }); + + app.get("/api/ping", [](Request&, Response& res){ + res.json({"ok", true, "msg", "pong"}); + }); + + app.run(8080); + return 0; +} +``` + +Try: + +```bash +curl -i http://127.0.0.1:8080/api/ping +curl -i -H "x-api-key: secret" http://127.0.0.1:8080/api/ping +``` + +### 2.2) Protect a prefix with `middleware::app::install()` +This is useful when you want to be explicit about routing helpers. + +```cpp +#include +#include +using namespace vix; + +int main() +{ + App app; + + middleware::app::install(app, "/api/", middleware::app::api_key_dev("secret")); + + app.get("/api/users", [](Request&, Response& res){ + res.json({"ok", true, "data", json::array({"u1","u2"})}); + }); + + app.run(8080); + return 0; +} +``` + +### 2.3) Protect only one exact route +Use `install_exact` if you want a single route protected, not the whole prefix. + +```cpp +#include +#include +using namespace vix; + +static vix::middleware::HttpMiddleware require_header(std::string h, std::string v) +{ + return [h = std::move(h), v = std::move(v)](Request& req, Response& res, vix::middleware::Next next) + { + if (req.header(h) != v) + { + res.status(401).json(vix::json::obj({"ok", false, "error", "unauthorized"})); + return; + } + next(); + }; +} + +int main() +{ + App app; + + middleware::app::install_exact(app, "/api/ping", + middleware::app::adapt(require_header("x-demo","1"))); + + app.get("/api/ping", [](Request&, Response& res){ + res.json({"ok", true}); + }); + + app.run(8080); + return 0; +} +``` + +------------------------------------------------------------------------ + +## 3) Group patterns + +### 3.1) Group with a protected sub-path using `protect()` +This makes the intent very clear: only that sub-path is protected. + +```cpp +#include +#include +using namespace vix; + +int main() +{ + App app; + + app.group("/api", [&](App::Group& api) + { + api.get("/public", [](Request&, Response& res){ + res.send("public"); + }); + + api.protect("/secure", middleware::app::api_key_dev("secret")); + + api.get("/secure", [](Request& req, Response& res){ + auto& k = req.state(); + res.json({"ok", true, "api_key", k.value}); + }); + }); + + app.run(8080); + return 0; +} +``` + +Try: + +```bash +curl -i http://127.0.0.1:8080/api/public +curl -i http://127.0.0.1:8080/api/secure +curl -i -H "x-api-key: secret" http://127.0.0.1:8080/api/secure +``` + +### 3.2) Group builder style (return value) +This is compact for simple APIs. + +```cpp +#include +#include +using namespace vix; + +int main() +{ + App app; + + auto api = app.group("/api"); + + api.get("/public", [](Request&, Response& res){ + res.send("public"); + }); + + api.use(middleware::app::api_key_dev("secret")); + + api.get("/secure", [](Request&, Response& res){ + res.json({"ok", true}); + }); + + app.run(8080); + return 0; +} +``` + +### 3.3) Nested groups for clean structure +This is where groups shine the most. + +- `/api` public stuff +- `/api/admin` secured stuff +- install JWT + RBAC once on the admin group + +```cpp +#include +#include +using namespace vix; + +int main() +{ + App app; + + app.group("/api", [&](App::Group& api) + { + api.get("/health", [](Request&, Response& res){ + res.json({"ok", true}); + }); + + api.group("/admin", [&](App::Group& admin) + { + admin.use(middleware::app::jwt_auth("dev_secret")); + admin.use(middleware::app::rbac_admin()); + + admin.get("/dashboard", [](Request& req, Response& res) + { + auto& authz = req.state(); + res.json({"ok", true, "sub", authz.subject, "role", "admin"}); + }); + }); + }); + + app.run(8080); + return 0; +} +``` + +------------------------------------------------------------------------ + +## 4) When to use which + +### Use prefix middleware when +- You already have a flat route list and want a quick protection layer. +- You want one line like `app.use("/api", ...)` and keep everything else unchanged. +- You want to attach cross cutting middleware globally or by prefix easily. + +### Use groups when +- You want your route tree to look like your API structure. +- You want nested scopes like `/api/v1`, `/api/admin`, `/api/internal`. +- You want to install auth once at the right level, not repeat `use("/api/admin", ...)` patterns. +- You want the code to read like: "inside admin, everything is protected". + +Practical rule: +- Small apps: prefix is fastest to write. +- Medium and large apps: groups scale better, especially with nesting. + +------------------------------------------------------------------------ + +## 5) Common mistakes + +1) Mixing prefix strings inconsistently +- `/api` vs `/api/` can lead to confusion. Pick one style and keep it consistent. + +2) Installing auth on the wrong level +- If you do `app.use("/api", jwt)` you may protect routes that should be public. +- With groups, you can isolate `admin.use(jwt)` only inside `/api/admin`. + +3) Duplicating middleware per route +- Prefer a group or prefix so you do not repeat the same install code everywhere. + +------------------------------------------------------------------------ + +# Complete Example (copy paste) + +This single file demonstrates both approaches side by side: +- `/p/*` uses prefix protection +- `/g/*` uses groups with protect and nested auth + +Save as: `group_vs_prefix_demo.cpp` + +```cpp +#include +#include +#include + +using namespace vix; + +static void install_prefix_routes(App& app) +{ + // Prefix protected section + app.use("/p/secure", middleware::app::api_key_dev("secret")); + + app.get("/p/public", [](Request&, Response& res){ + res.send("prefix public"); + }); + + app.get("/p/secure/who", [](Request& req, Response& res){ + auto& k = req.state(); + res.json({"ok", true, "mode", "prefix", "api_key", k.value}); + }); +} + +static void install_group_routes(App& app) +{ + app.group("/g", [&](App::Group& g) + { + g.get("/public", [](Request&, Response& res){ + res.send("group public"); + }); + + g.protect("/secure", middleware::app::api_key_dev("secret")); + + g.get("/secure/who", [](Request& req, Response& res){ + auto& k = req.state(); + res.json({"ok", true, "mode", "group", "api_key", k.value}); + }); + + g.group("/admin", [&](App::Group& admin) + { + admin.use(middleware::app::jwt_auth("dev_secret")); + admin.use(middleware::app::rbac_admin()); + + admin.get("/dashboard", [](Request& req, Response& res) + { + auto& authz = req.state(); + res.json({"ok", true, "mode", "group_admin", "sub", authz.subject}); + }); + }); + }); +} + +int main() +{ + App app; + + app.get("/", [](Request&, Response& res){ + res.send( + "Try:\n" + " /p/public\n" + " /p/secure/who (needs x-api-key: secret)\n" + " /g/public\n" + " /g/secure/who (needs x-api-key: secret)\n" + " /g/admin/dashboard (needs JWT admin token)\n" + ); + }); + + install_prefix_routes(app); + install_group_routes(app); + + std::cout + << "Running:\n" + << " http://localhost:8080/\n" + << " http://localhost:8080/p/public\n" + << " http://localhost:8080/p/secure/who\n" + << " http://localhost:8080/g/public\n" + << " http://localhost:8080/g/secure/who\n" + << " http://localhost:8080/g/admin/dashboard\n\n" + << "API key: secret\n"; + + app.run(8080); + return 0; +} +``` + +Run: + +```bash +vix run group_vs_prefix_demo.cpp +``` + +Quick tests: + +```bash +curl -i http://localhost:8080/p/public +curl -i http://localhost:8080/p/secure/who +curl -i -H "x-api-key: secret" http://localhost:8080/p/secure/who + +curl -i http://localhost:8080/g/public +curl -i http://localhost:8080/g/secure/who +curl -i -H "x-api-key: secret" http://localhost:8080/g/secure/who +``` + diff --git a/docs/examples/headers.md b/docs/examples/headers.md new file mode 100644 index 0000000..b9faf93 --- /dev/null +++ b/docs/examples/headers.md @@ -0,0 +1,287 @@ +# Security headers + +This guide shows how to add security headers in Vix.cpp, and how to combine them with CORS and CSRF safely. + +What you get from the preset: + +- X-Content-Type-Options: nosniff +- X-Frame-Options: DENY (or equivalent) +- Referrer-Policy +- Permissions-Policy +- Optional Strict-Transport-Security (HSTS) when enabled + +Notes: + +- Security headers are cheap and should usually be enabled for API responses. +- Order matters when stacking middleware. +- If you need cross-site cookies in browsers, you must use HTTPS and SameSite=None; Secure. + +--- + +## 1) Minimal security headers on /api + +This is the smallest pattern: apply headers only under a prefix. + +```cpp +/** + * + * @file security_headers_server.cpp - Security headers middleware example (Vix.cpp) + * + */ +// Run: +// vix run security_headers_server.cpp +// +// Tests: +// curl -i http://localhost:8080/api/ping +// curl -i http://localhost:8080/ + +#include +#include + +using namespace vix; + +int main() +{ + App app; + + // Apply security headers only on /api + app.use("/api", middleware::app::security_headers_dev()); // HSTS is OFF by default + + app.get("/api/ping", [](Request &, Response &res) + { + res.json({"ok", true, "message", "headers applied"}); + }); + + // Public route (no forced headers) + app.get("/", [](Request &, Response &res) + { + res.send("public route"); + }); + + app.run(8080); + return 0; +} +``` + +What to look for: + +```bash +curl -i http://localhost:8080/api/ping +``` + +You should see the security headers in the response. + +--- + +## 2) Realistic stack: Security headers + CORS + CSRF (same prefix) + +This is the common API pattern: + +1) Security headers first (so they also apply to errors) +2) CORS second (so preflight and browser rules work) +3) CSRF third (protect write operations) + +```cpp +/** + * + * @file security_cors_csrf_headers_server.cpp - CORS + CSRF + Security Headers (Vix.cpp) + * + */ +// Run: +// vix run security_cors_csrf_headers_server.cpp +// +// Quick tests are listed below. + +#include +#include + +using namespace vix; + +int main() +{ + App app; + + // Apply on ALL /api/* + // Order matters: headers first, then CORS, then CSRF. + app.use("/api", middleware::app::security_headers_dev()); // HSTS off by default + + app.use("/api", middleware::app::cors_dev({ + "http://localhost:5173", + "http://0.0.0.0:5173", + "https://example.com" + })); + + // CSRF expects: cookie "csrf_token" and header "x-csrf-token" by default + app.use("/api", middleware::app::csrf_dev("csrf_token", "x-csrf-token", false)); + + // Explicit OPTIONS routes (lets CORS middleware answer preflight) + app.options("/api/update", [](Request &, Response &res){ res.status(204).send(); }); + app.options("/api/csrf", [](Request &, Response &res){ res.status(204).send(); }); + + // Routes + app.get("/api/csrf", [](Request &, Response &res) + { + // For cross-origin cookie in browsers: + // - Use HTTPS + // - SameSite=None; Secure + // + // For local dev over HTTP: + // - SameSite=Lax is fine, but cookie might not be sent cross-site. + res.header("Set-Cookie", "csrf_token=abc; Path=/; SameSite=Lax"); + res.header("X-Request-Id", "req_csrf_1"); + res.json({"csrf_token", "abc"}); + }); + + app.post("/api/update", [](Request &, Response &res) + { + res.header("X-Request-Id", "req_update_1"); + res.json({"ok", true, "message", "CORS + CSRF + HEADERS"}); + }); + + app.get("/", [](Request &, Response &res) + { + res.send("public route"); + }); + + app.run(8080); + return 0; +} +``` + +### Terminal tests (curl) + +Preflight allowed (204 + CORS headers): + +```bash +curl -i -X OPTIONS http://localhost:8080/api/update \ + -H "Origin: https://example.com" \ + -H "Access-Control-Request-Method: POST" \ + -H "Access-Control-Request-Headers: Content-Type, X-CSRF-Token" +``` + +Preflight blocked (403): + +```bash +curl -i -X OPTIONS http://localhost:8080/api/update \ + -H "Origin: https://evil.com" \ + -H "Access-Control-Request-Method: POST" +``` + +Get CSRF cookie (sets csrf_token=abc): + +```bash +curl -i -c cookies.txt http://localhost:8080/api/csrf \ + -H "Origin: https://example.com" +``` + +Fail: missing CSRF header: + +```bash +curl -i -b cookies.txt -X POST http://localhost:8080/api/update \ + -H "Origin: https://example.com" \ + -d "x=1" +``` + +Fail: wrong token: + +```bash +curl -i -b cookies.txt -X POST http://localhost:8080/api/update \ + -H "Origin: https://example.com" \ + -H "X-CSRF-Token: wrong" \ + -d "x=1" +``` + +OK: correct token: + +```bash +curl -i -b cookies.txt -X POST http://localhost:8080/api/update \ + -H "Origin: https://example.com" \ + -H "X-CSRF-Token: abc" \ + -d "x=1" +``` + +--- + +## Common mistakes + +### 1) Wrong order + +If you put CSRF before CORS, browser preflight can fail in confusing ways. + +Recommended order on the same prefix: + +Security headers -> CORS -> CSRF -> Auth -> Rate limit -> Handlers + +### 2) Cross-site cookies on HTTP + +If your frontend is on another origin, browsers often require: + +- SameSite=None +- Secure +- HTTPS + +If you stay on HTTP locally, cookie behavior can differ from production. + +### 3) Forgetting OPTIONS routes + +Browsers send OPTIONS preflight requests. If you do not handle OPTIONS correctly, +your CORS middleware might not return the expected headers. + +--- + +## Full copy-paste example (recommended) + +This is the single-file version you can run immediately. + +Save as headers_stack_server.cpp: + +```cpp +#include +#include + +using namespace vix; + +static void register_routes(App &app) +{ + app.options("/api/csrf", [](Request &, Response &res){ res.status(204).send(); }); + app.options("/api/update", [](Request &, Response &res){ res.status(204).send(); }); + + app.get("/api/csrf", [](Request &, Response &res) + { + res.header("Set-Cookie", "csrf_token=abc; Path=/; SameSite=Lax"); + res.json({"csrf_token", "abc"}); + }); + + app.post("/api/update", [](Request &, Response &res) + { + res.json({"ok", true, "message", "protected update"}); + }); + + app.get("/", [](Request &, Response &res) + { + res.send("public route"); + }); +} + +int main() +{ + App app; + + // Stack (order matters) + app.use("/api", middleware::app::security_headers_dev()); + app.use("/api", middleware::app::cors_dev({"https://example.com"})); + app.use("/api", middleware::app::csrf_dev("csrf_token", "x-csrf-token", false)); + + register_routes(app); + + app.run(8080); + return 0; +} +``` + +Run: + +```bash +vix run headers_stack_server.cpp +``` + diff --git a/docs/examples/hello-http.md b/docs/examples/hello-http.md new file mode 100644 index 0000000..4746cff --- /dev/null +++ b/docs/examples/hello-http.md @@ -0,0 +1,298 @@ +# Hello HTTP + +This page shows **all the common "Hello" styles** in Vix.cpp, using the +smallest possible snippets. + +Each snippet is a complete `main()` you can copy into a real file (for +example `main.cpp`) and run. + +------------------------------------------------------------------------ + +## 1) Hello as plain text (explicit send) + +``` cpp +#include +using namespace vix; + +int main() +{ + App app; + + app.get("/", [](Request&, Response& res) + { + res.send("Hello, Vix!"); + }); + + app.run(8080); + return 0; +} +``` + +Try: + +``` bash +curl -i http://localhost:8080/ +``` + +------------------------------------------------------------------------ + +## 2) Hello as plain text (return style) + +If you return a value and you did not send anything explicitly, Vix +auto-sends it. + +``` cpp +#include +using namespace vix; + +int main() +{ + App app; + + app.get("/txt", [](Request&, Response&) + { + return "Hello, Vix!"; + }); + + app.run(8080); + return 0; +} +``` + +Try: + +``` bash +curl -i http://localhost:8080/txt +``` + +------------------------------------------------------------------------ + +## 3) Hello as JSON (explicit json) + +``` cpp +#include +using namespace vix; + +int main() +{ + App app; + + app.get("/json", [](Request&, Response& res) + { + res.json({ + "message", "Hello", + "framework", "Vix.cpp", + "ok", true + }); + }); + + app.run(8080); + return 0; +} +``` + +Try: + +``` bash +curl -s http://localhost:8080/json +``` + +------------------------------------------------------------------------ + +## 4) Hello as JSON (return object style) + +``` cpp +#include +#include +using namespace vix; + +int main() +{ + App app; + + app.get("/auto-json", [](Request&, Response&) + { + return vix::json::o( + "message", "Hello", + "mode", "auto-return", + "ok", true + ); + }); + + app.run(8080); + return 0; +} +``` + +Try: + +``` bash +curl -s http://localhost:8080/auto-json +``` + +------------------------------------------------------------------------ + +## 5) Hello with status code + JSON + +``` cpp +#include +using namespace vix; + +int main() +{ + App app; + + app.get("/created", [](Request&, Response& res) + { + res.status(201).json({ + "message", "Created", + "ok", true + }); + }); + + app.run(8080); + return 0; +} +``` + +Try: + +``` bash +curl -i http://localhost:8080/created +``` + +------------------------------------------------------------------------ + +## 6) Hello with headers + text + +``` cpp +#include +using namespace vix; + +int main() +{ + App app; + + app.get("/headers", [](Request&, Response& res) + { + res.header("X-Powered-By", "Vix.cpp"); + res.send("Hello with headers"); + }); + + app.run(8080); + return 0; +} +``` + +Try: + +``` bash +curl -i http://localhost:8080/headers +``` + +------------------------------------------------------------------------ + +## 7) Hello with path param + +``` cpp +#include +using namespace vix; + +int main() +{ + App app; + + app.get("/hello/{name}", [](Request& req, Response& res) + { + const auto name = req.param("name"); + res.json({ + "message", "Hello", + "name", name + }); + }); + + app.run(8080); + return 0; +} +``` + +Try: + +``` bash +curl -s http://localhost:8080/hello/Ada +``` + +------------------------------------------------------------------------ + +## 8) Hello with query param + +``` cpp +#include +using namespace vix; + +int main() +{ + App app; + + app.get("/hello", [](Request& req, Response& res) + { + const auto name = req.query_value("name", "world"); + res.send(std::string("Hello, ") + name + "!"); + }); + + app.run(8080); + return 0; +} +``` + +Try: + +``` bash +curl -i "http://localhost:8080/hello?name=Ada" +``` + +------------------------------------------------------------------------ + +## 9) Mixing send + return (return gets ignored) + +If you already sent a response (send/json), any returned value is +ignored. + +``` cpp +#include +#include +using namespace vix; + +int main() +{ + App app; + + app.get("/mix", [](Request&, Response& res) + { + res.status(200).send("Hello (explicit)"); + + // Already sent -> ignored + return vix::json::o("ignored", true); + }); + + app.run(8080); + return 0; +} +``` + +Try: + +``` bash +curl -i http://localhost:8080/mix +``` + +------------------------------------------------------------------------ + +## What this teaches + +- The two styles: **explicit send** vs **auto-send return** +- Text and JSON responses +- Status codes and headers +- Path and query parameters +- The "already sent" rule + diff --git a/docs/examples/hello_routes.md b/docs/examples/hello_routes.md deleted file mode 100644 index 607fbfc..0000000 --- a/docs/examples/hello_routes.md +++ /dev/null @@ -1,18 +0,0 @@ -# Example — hello_routes - -Minimal GET routes and path params. - -```cpp -#include -using namespace vix; - -int main() -{ - App app; - - app.get("/hello", [](Request &, Response &res) - { res.json({"message", "Hello, Vix!"}); }); - - app.run(8080); -} -``` diff --git a/docs/examples/index.md b/docs/examples/index.md new file mode 100644 index 0000000..c3c3943 --- /dev/null +++ b/docs/examples/index.md @@ -0,0 +1,642 @@ +# Auth and Middleware (Minimal Patterns) + +This page shows minimal auth and middleware patterns in Vix.cpp. + +Rule of this doc: +- one concept +- one minimal `main()` +- a quick curl test + +## 1) API key middleware (protect one route) + +A public route plus a secure route that requires `x-api-key`. + +```cpp +#include +#include +using namespace vix; + +int main() +{ + App app; + + app.get("/public", [](Request&, Response& res){ + res.json({ "ok", true, "scope", "public" }); + }); + + // Install API key middleware only on this prefix + middleware::app::install(app, "/secure/", middleware::app::api_key_dev("dev_key_123")); + + app.get("/secure/whoami", [](Request&, Response& res){ + res.json({ "ok", true, "scope", "secure", "message", "API key accepted" }); + }); + + app.run(8080); + return 0; +} +``` + +Try: + +```bash +curl -i http://127.0.0.1:8080/public +curl -i http://127.0.0.1:8080/secure/whoami +curl -i -H "x-api-key: dev_key_123" http://127.0.0.1:8080/secure/whoami +``` + +## 2) Prefix protection (protect all /api routes) + +Everything under `/api/` is protected. + +```cpp +#include +#include +using namespace vix; + +int main() +{ + App app; + + middleware::app::install(app, "/api/", middleware::app::api_key_dev("dev_key_123")); + + app.get("/api/ping", [](Request&, Response& res){ + res.json({ "ok", true, "pong", true }); + }); + + app.get("/api/users", [](Request&, Response& res){ + res.json({ "ok", true, "data", json::array({ "u1", "u2" }) }); + }); + + app.run(8080); + return 0; +} +``` + +Try: + +```bash +curl -i http://127.0.0.1:8080/api/ping +curl -i -H "x-api-key: dev_key_123" http://127.0.0.1:8080/api/ping +``` + +## 3) Custom middleware (context style + RequestState) + +Store data into request state and read it in the handler. + +```cpp +#include +#include +#include +#include +using namespace vix; + +struct RequestId { std::string value; }; + +static long long now_ms() +{ + using namespace std::chrono; + return (long long)time_point_cast(system_clock::now()) + .time_since_epoch().count(); +} + +static vix::middleware::MiddlewareFn mw_request_id() +{ + return [](vix::middleware::Context& ctx, vix::middleware::Next next) + { + RequestId rid; + rid.value = std::to_string(now_ms()); + + ctx.req().emplace_state(rid); + ctx.res().header("x-request-id", rid.value); + + next(); + }; +} + +int main() +{ + App app; + + app.use(vix::middleware::app::adapt_ctx(mw_request_id())); + + app.get("/who", [](Request& req, Response& res){ + res.json({ "ok", true, "request_id", req.state().value }); + }); + + app.run(8080); + return 0; +} +``` + +Try: + +```bash +curl -i http://127.0.0.1:8080/who +``` + +## 4) Role gating (fake auth + admin only) + +Minimal RBAC style gate using headers for the demo. + +```cpp +#include +#include +#include + +using namespace vix; +namespace J = vix::json; + +struct AuthInfo +{ + bool authed{false}; + std::string subject; + std::string role; +}; + +static vix::middleware::MiddlewareFn mw_fake_auth() +{ + return [](vix::middleware::Context& ctx, vix::middleware::Next next) + { + AuthInfo a; + const std::string user = ctx.req().header("x-user"); + const std::string role = ctx.req().header("x-role"); + + if (!user.empty()) + { + a.authed = true; + a.subject = user; + a.role = role.empty() ? "user" : role; + } + + ctx.req().emplace_state(a); + next(); + }; +} + +static vix::middleware::MiddlewareFn mw_require_admin() +{ + return [](vix::middleware::Context& ctx, vix::middleware::Next next) + { + if (!ctx.req().has_state_type() || !ctx.req().state().authed) + { + ctx.res().status(401).json(J::obj({ "ok", false, "error", "unauthorized" })); + return; + } + + if (ctx.req().state().role != "admin") + { + ctx.res().status(403).json(J::obj({ "ok", false, "error", "forbidden", "hint", "admin required" })); + return; + } + + next(); + }; +} + +int main() +{ + App app; + + app.use(vix::middleware::app::adapt_ctx(mw_fake_auth())); + + vix::middleware::app::install(app, "/admin/", vix::middleware::app::adapt_ctx(mw_require_admin())); + + app.get("/admin/stats", [](Request& req, Response& res) + { + const auto& a = req.state(); + res.json({ "ok", true, "admin", true, "subject", a.subject }); + }); + + app.run(8080); + return 0; +} +``` + +Try: + +```bash +curl -i http://127.0.0.1:8080/admin/stats +curl -i -H "x-user: gaspard" http://127.0.0.1:8080/admin/stats +curl -i -H "x-user: gaspard" -H "x-role: admin" http://127.0.0.1:8080/admin/stats +``` + +## 5) Legacy HttpMiddleware style (adapt) + +If you have an older middleware signature `(Request, Response, next)` you can adapt it. + +```cpp +#include +#include +#include + +using namespace vix; +namespace J = vix::json; + +static vix::middleware::HttpMiddleware require_header(std::string header, std::string expected) +{ + return [header = std::move(header), expected = std::move(expected)](Request& req, Response& res, vix::middleware::Next next) + { + const std::string got = req.header(header); + if (got != expected) + { + res.status(401).json(J::obj({ + "ok", false, + "error", "unauthorized", + "required_header", header + })); + return; + } + next(); + }; +} + +int main() +{ + App app; + + vix::middleware::app::install_exact( + app, + "/api/ping", + vix::middleware::app::adapt(require_header("x-demo", "1")) + ); + + app.get("/api/ping", [](Request&, Response& res){ + res.json({ "ok", true, "pong", true }); + }); + + app.run(8080); + return 0; +} +``` + +Try: + +```bash +curl -i http://127.0.0.1:8080/api/ping +curl -i -H "x-demo: 1" http://127.0.0.1:8080/api/ping +``` + +## 6) Chaining middleware + +Apply multiple middlewares on the same prefix. + +```cpp +#include +#include +#include + +using namespace vix; + +static vix::middleware::MiddlewareFn mw_mark() +{ + return [](vix::middleware::Context& ctx, vix::middleware::Next next) + { + ctx.res().header("x-mw", "on"); + next(); + }; +} + +int main() +{ + App app; + + vix::middleware::app::install( + app, + "/secure/", + vix::middleware::app::chain( + vix::middleware::app::api_key_dev("dev_key_123"), + vix::middleware::app::adapt_ctx(mw_mark()) + ) + ); + + app.get("/secure/hello", [](Request&, Response& res){ + res.json({ "ok", true, "message", "Hello secure" }); + }); + + app.run(8080); + return 0; +} +``` + +Try: + +```bash +curl -i -H "x-api-key: dev_key_123" http://127.0.0.1:8080/secure/hello +``` + +## What this teaches + +- Prefix install: protect a group of routes +- Exact install: protect one route +- Context middleware: state and headers +- Legacy middleware adaptation +- Basic RBAC style gating +- Middleware chaining + +--- + +# RBAC (Roles + Permissions) using JWT + +## What is RBAC + +RBAC means Role Based Access Control. + +You check: +- roles (admin, user, editor) +- permissions (products:write, orders:read) + +In Vix.cpp: +- JWT extracts claims +- RBAC builds `Authz` +- `require_role()` and `require_perm()` enforce rules + +## Request flow + +1. Client sends JWT: `Authorization: Bearer ` +2. JWT middleware validates signature +3. RBAC builds an `Authz` context +4. Role and permission middlewares run +5. Handler executes + +## Minimal RBAC pattern + +```cpp +App app; + +vix::middleware::auth::JwtOptions jwt_opt{}; +jwt_opt.secret = "dev_secret"; +jwt_opt.verify_exp = false; + +vix::middleware::auth::RbacOptions rbac_opt{}; +rbac_opt.require_auth = true; +rbac_opt.use_resolver = false; + +auto jwt_mw = vix::middleware::app::adapt_ctx(vix::middleware::auth::jwt(jwt_opt)); +auto ctx_mw = vix::middleware::app::adapt_ctx(vix::middleware::auth::rbac_context(rbac_opt)); +auto role_mw = vix::middleware::app::adapt_ctx(vix::middleware::auth::require_role("admin")); +auto perm_mw = vix::middleware::app::adapt_ctx(vix::middleware::auth::require_perm("products:write")); + +app.use(vix::middleware::app::when( + [](const Request& r){ return r.path() == "/admin"; }, std::move(jwt_mw))); +app.use(vix::middleware::app::when( + [](const Request& r){ return r.path() == "/admin"; }, std::move(ctx_mw))); +app.use(vix::middleware::app::when( + [](const Request& r){ return r.path() == "/admin"; }, std::move(role_mw))); +app.use(vix::middleware::app::when( + [](const Request& r){ return r.path() == "/admin"; }, std::move(perm_mw))); +``` + +## Reading `Authz` in a handler + +```cpp +app.get("/admin", [](Request& req, Response& res){ + auto& authz = req.state(); + res.json({ + "ok", true, + "sub", authz.subject, + "has_admin", authz.has_role("admin"), + "has_products_write", authz.has_perm("products:write") + }); +}); +``` + +## Common statuses + +- 401: missing token, invalid token, invalid signature +- 403: authenticated but missing required role or permission + +--- + +# Rate limiting (minimal) + +Rate limiting protects your API from brute force, spam, and bursts. + +The model is a token bucket: +- capacity: max burst +- refill_per_sec: tokens per second +- empty bucket returns 429 + +## Minimal limiter on /api + +```cpp +#include +#include +using namespace vix; + +int main() +{ + App app; + + app.use("/api", middleware::app::rate_limit_custom_dev(5.0, 0.0)); + + app.get("/api/ping", [](Request& req, Response& res){ + res.json({ "ok", true, "msg", "pong", "xff", req.header("x-forwarded-for") }); + }); + + app.run(8080); +} +``` + +Try: + +```bash +for i in $(seq 1 6); do + echo "---- $i" + curl -i http://localhost:8080/api/ping +done +``` + +--- + +# CSRF (cookie + header) + +CSRF is relevant mainly for browser sessions using cookies. + +Vix default: +- cookie: `csrf_token` +- header: `x-csrf-token` + +## Minimal CSRF on /api + +```cpp +#include +#include +using namespace vix; + +int main() +{ + App app; + + app.use("/api", middleware::app::csrf_dev()); + + app.get("/api/csrf", [](Request&, Response& res){ + res.header("Set-Cookie", "csrf_token=abc; Path=/; SameSite=Lax"); + res.json({ "csrf_token", "abc" }); + }); + + app.post("/api/update", [](Request&, Response& res){ + res.json({ "ok", true, "message", "CSRF passed" }); + }); + + app.run(8080); +} +``` + +Try: + +```bash +curl -i -c cookies.txt http://localhost:8080/api/csrf +curl -i -b cookies.txt -X POST http://localhost:8080/api/update -d "x=1" +curl -i -b cookies.txt -X POST http://localhost:8080/api/update -H "x-csrf-token: wrong" -d "x=1" +curl -i -b cookies.txt -X POST http://localhost:8080/api/update -H "x-csrf-token: abc" -d "x=1" +``` + +--- + +# Complete example (Session + RBAC + Rate limit) + +This is a single file that shows: +- cookie sessions for browser style auth +- RBAC protected admin API using JWT +- rate limit on /api + +Save as `security_complete.cpp`. + +```cpp +#include +#include +#include + +#include +#include + +#include +#include +#include + +using namespace vix; + +// Example admin token (HS256, secret=dev_secret) +// payload: {"sub":"user123","roles":["admin"],"perms":["products:write"]} +static const std::string TOKEN_OK = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." + "eyJzdWIiOiJ1c2VyMTIzIiwicm9sZXMiOlsiYWRtaW4iXSwicGVybXMiOlsicHJvZHVjdHM6d3JpdGUiXX0." + "w1y3nA2F1kq0oJ0x8wWc5wQx8zF4h2d6V7mYp0jYk3Q"; + +static void install_session(App& app) +{ + app.use(middleware::app::session_dev( + "dev_session_secret", + "sid", + std::chrono::hours(24 * 7), + false, + "Lax", + true, + "/", + true + )); +} + +static void install_api_security(App& app) +{ + // Rate limit all /api traffic + app.use("/api", middleware::app::rate_limit_dev(60, std::chrono::minutes(1))); + + // JWT + RBAC only for /api/admin + vix::middleware::auth::JwtOptions jwt_opt{}; + jwt_opt.secret = "dev_secret"; + jwt_opt.verify_exp = false; + + vix::middleware::auth::RbacOptions rbac_opt{}; + rbac_opt.require_auth = true; + rbac_opt.use_resolver = false; + + auto jwt_mw = vix::middleware::app::adapt_ctx(vix::middleware::auth::jwt(jwt_opt)); + auto ctx_mw = vix::middleware::app::adapt_ctx(vix::middleware::auth::rbac_context(rbac_opt)); + auto role_mw = vix::middleware::app::adapt_ctx(vix::middleware::auth::require_role("admin")); + auto perm_mw = vix::middleware::app::adapt_ctx(vix::middleware::auth::require_perm("products:write")); + + app.use(vix::middleware::app::when( + [](const Request& r){ return r.path().rfind("/api/admin", 0) == 0; }, + std::move(jwt_mw) + )); + app.use(vix::middleware::app::when( + [](const Request& r){ return r.path().rfind("/api/admin", 0) == 0; }, + std::move(ctx_mw) + )); + app.use(vix::middleware::app::when( + [](const Request& r){ return r.path().rfind("/api/admin", 0) == 0; }, + std::move(role_mw) + )); + app.use(vix::middleware::app::when( + [](const Request& r){ return r.path().rfind("/api/admin", 0) == 0; }, + std::move(perm_mw) + )); +} + +static void install_routes(App& app) +{ + app.get("/", [](Request&, Response& res){ + res.send( + "Vix security complete example:\n" + " GET /session increments a counter stored in a signed cookie\n" + " GET /api/ping rate limited\n" + " GET /api/admin/stats requires JWT + role admin + perm products:write\n" + ); + }); + + app.get("/session", [](Request& req, Response& res){ + auto& s = req.state(); + int n = s.get("n") ? std::stoi(*s.get("n")) : 0; + s.set("n", std::to_string(++n)); + res.text("n=" + std::to_string(n)); + }); + + app.get("/api/ping", [](Request&, Response& res){ + res.json({ "ok", true, "pong", true }); + }); + + app.get("/api/admin/stats", [](Request& req, Response& res){ + auto& authz = req.state(); + res.json({ + "ok", true, + "sub", authz.subject, + "is_admin", authz.has_role("admin"), + "can_write_products", authz.has_perm("products:write") + }); + }); +} + +int main() +{ + App app; + + install_session(app); + install_api_security(app); + install_routes(app); + + std::cout + << "Running:\n" + << " http://localhost:8080/\n" + << " http://localhost:8080/session\n" + << " http://localhost:8080/api/ping\n" + << " http://localhost:8080/api/admin/stats\n\n" + << "Admin token:\n " << TOKEN_OK << "\n\n" + << "Try:\n" + << " curl -i http://localhost:8080/session\n" + << " curl -i http://localhost:8080/api/ping\n" + << " curl -i http://localhost:8080/api/admin/stats\n" + << " curl -i -H \"Authorization: Bearer " << TOKEN_OK << "\" http://localhost:8080/api/admin/stats\n"; + + app.run(8080); + return 0; +} +``` + +Run: + +```bash +vix run security_complete.cpp +``` + + diff --git a/docs/examples/ip-filter.md b/docs/examples/ip-filter.md new file mode 100644 index 0000000..bd0737c --- /dev/null +++ b/docs/examples/ip-filter.md @@ -0,0 +1,164 @@ +# IP Filter (Beginner Guide) + +Welcome 👋\ +This page explains how to protect routes using the **IP filter +middleware** in Vix.cpp. + +With an IP filter you can: + +- allow only trusted IPs (allowlist) +- block known bad IPs (denylist) +- protect `/api/*` while keeping public routes open + +## What is an IP filter? + +An IP filter checks the client IP address **before** your route handler +runs. + +If the IP is not allowed, the middleware stops the request early and +returns: + +- **403 Forbidden** + +## Where does the server get the client IP? + +In real production deployments you usually have a reverse proxy (Nginx, +cloud load balancer).\ +That proxy sends the client IP through headers such as: + +- `X-Forwarded-For` (most common) +- `X-Real-IP` + +When `X-Forwarded-For` contains multiple values like: + + client, proxy1, proxy2 + +Vix uses the **first IP** (the real client). + +# 1) Minimal IP filter on `/api/*` + +This example: + +- keeps `/` public +- protects `/api/hello` +- allows only `10.0.0.1` and `127.0.0.1` +- explicitly denies `9.9.9.9` (deny wins) + +``` cpp +#include +#include + +using namespace vix; + +int main() +{ + App app; + + // Apply on /api/* + app.use("/api", middleware::app::ip_filter_allow_deny_dev( + "x-forwarded-for", + {"10.0.0.1", "127.0.0.1"}, // allow + {"9.9.9.9"}, // deny (priority) + true // fallback to x-real-ip, etc. + )); + + app.get("/", [](Request &, Response &res) { + res.send("public route"); + }); + + app.get("/api/hello", [](Request &req, Response &res) { + res.json({ + "ok", true, + "message", "Hello from /api/hello", + "x_forwarded_for", req.header("x-forwarded-for"), + "x_real_ip", req.header("x-real-ip") + }); + }); + + app.run(8080); +} +``` + +# 2) Test with curl + +Start: + +``` bash +vix run ip_filter_server.cpp +``` + +Public route (no middleware): + +``` bash +curl -i http://localhost:8080/ +``` + +Allowed IP: + +``` bash +curl -i http://localhost:8080/api/hello -H "X-Forwarded-For: 10.0.0.1" +``` + +Not allowed (not in allow list): + +``` bash +curl -i http://localhost:8080/api/hello -H "X-Forwarded-For: 1.2.3.4" +``` + +Denied explicitly (deny wins): + +``` bash +curl -i http://localhost:8080/api/hello -H "X-Forwarded-For: 9.9.9.9" +``` + +X-Forwarded-For chain: + +``` bash +curl -i http://localhost:8080/api/hello -H "X-Forwarded-For: 10.0.0.1, 127.0.0.1" +``` + +# 3) Common beginner mistakes + +## Mistake 1: trusting X-Forwarded-For directly on the internet + +If your server is directly exposed (no proxy), attackers can forge +headers. + +Production rule: + +- only trust `X-Forwarded-For` if it comes from a trusted proxy +- otherwise use remote address (socket IP) or `X-Real-IP` set by your + proxy + +## Mistake 2: forgetting that allowlist blocks everything else + +If you configure an allow list, any IP not in it is blocked. + +## Mistake 3: wrong header name + +Your middleware reads the header you give it. + +If you configured: + +- `"x-forwarded-for"` + +Then your requests must send: + +- `X-Forwarded-For: ...` + +Header names are case-insensitive in HTTP, but spelling must match. + +# 4) When to use allow, deny, or both + +- **Allow only**: admin dashboards, internal APIs, webhooks from known + providers +- **Deny only**: block a list of abusive IPs +- **Allow + deny**: allow trusted range but always block specific + offenders (deny priority) + +# Summary + +- Install IP filter on a prefix: `app.use("/api", ...)` +- Use allowlist for strict protection +- Use denylist for explicit blocks +- Prefer running behind a proxy that sets `X-Forwarded-For` safely diff --git a/docs/examples/json-basics.md b/docs/examples/json-basics.md new file mode 100644 index 0000000..4bac1b4 --- /dev/null +++ b/docs/examples/json-basics.md @@ -0,0 +1,448 @@ +# Simple JSON Model Guide + +`vix/json/Simple.hpp` defines a minimal JSON-like value model for lightweight internal Vix APIs. + +It is: +- header-only +- dependency-free (does not depend on `nlohmann::json`) +- designed for structured data exchange between modules without parsing text + +If you want to parse or serialize JSON text, use the regular Vix JSON helpers based on `nlohmann::json`. + +--- + +## Include + +```cpp +#include +using namespace vix::json; +``` + +--- + +## What is Simple? + +Simple is a tiny DOM-like value system that can represent: + +- null +- bool +- integer (stored as `int64`) +- floating point (stored as `double`) +- string +- arrays (`array_t`) +- objects (`kvs`) + +It is meant for: +- internal payloads between modules +- in-memory structured data +- building small trees of values for adapters and bridges + +It is not meant for: +- large untrusted JSON text parsing +- full JSON schema validation +- streaming or incremental parsing + +--- + +## Core types + +### `token` + +A `token` is a tagged variant that stores either: +- a primitive value, or +- a `shared_ptr` / `shared_ptr` for recursion + +Internally: + +```cpp +using value_t = std::variant< + std::monostate, // null + bool, // boolean + std::int64_t, // integer + double, // floating point + std::string, // string + std::shared_ptr, // array + std::shared_ptr // object +>; +``` + +Default token is null. + +### `array_t` + +A JSON-like array: + +- stored as `std::vector` +- provides convenience methods (`push_*`, `ensure(idx)`, `erase_at`, etc.) + +### `kvs` + +A JSON-like object. + +Important design choice: +- objects are stored as a flat vector: `key0, value0, key1, value1, ...` +- keys are typically string tokens +- this layout keeps the model minimal and predictable + +--- + +## Quick example + +```cpp +using namespace vix::json; + +kvs user = obj({ + "name", "Alice", + "age", 30, + "skills", array({"C++", "Networking"}) +}); + +token root = user; + +root.ensure_object().set_string("country", "UG"); +``` + +--- + +## Creating values + +### Null + +```cpp +token a; +token b = nullptr; +``` + +### Bool + +```cpp +token t = true; +``` + +### Integers + +All integral types (except bool) are accepted and stored as `int64`. + +```cpp +token a = 42; +token b = std::int64_t(42); +``` + +### Floating point + +```cpp +token t = 3.14; +``` + +### Strings + +```cpp +token a = "hello"; +token b = std::string("hello"); +``` + +### Arrays + +Using initializer list: + +```cpp +array_t xs = array({1, 2, 3, "hi"}); +token t = xs; +``` + +Using vector: + +```cpp +std::vector v = {1, 2, 3}; +array_t xs = array(v); +array_t ys = array(std::move(v)); +``` + +### Objects + +Objects are built with `obj({ ... })` where the initializer list is flattened key/value order: + +```cpp +kvs cfg = obj({ + "host", "127.0.0.1", + "port", 8080, + "tls", false +}); + +token t = cfg; +``` + +Important: +- the initializer list is not nested pairs +- it is: key, value, key, value, ... + +--- + +## Reading values + +### Type checks + +```cpp +token t = 42; + +t.is_null(); +t.is_bool(); +t.is_i64(); +t.is_f64(); +t.is_string(); +t.is_array(); +t.is_object(); +``` + +### Raw getters (pointer or nullptr) + +```cpp +if (const std::int64_t* x = t.as_i64()) +{ + // *x is available +} +``` + +Available raw getters: +- `as_bool()` +- `as_i64()` +- `as_f64()` +- `as_string()` +- `as_array_ptr()` +- `as_object_ptr()` + +### Convenience getters with default + +```cpp +bool ok = t.as_bool_or(false); +std::int64_t n = t.as_i64_or(0); +double d = t.as_f64_or(0.0); +std::string s = t.as_string_or("default"); +``` + +--- + +## Mutating values + +### Setters + +```cpp +token t; + +t.set_null(); +t.set_bool(true); +t.set_i64(123); +t.set_int(123); +t.set_ll(123LL); +t.set_ull(123ULL); +t.set_f64(3.14); +t.set_string("hi"); +t.set_cstr("hi"); +``` + +### Ensure array/object + +These helpers replace the token with an empty array/object if needed, then return a mutable reference. + +```cpp +token t; + +// becomes array if not already +array_t& xs = t.ensure_array(); +xs.push_int(1); +xs.push_string("two"); + +// becomes object if not already +kvs& o = t.ensure_object(); +o.set_string("name", "Ada"); +o.set_int("age", 42); +``` + +--- + +## `array_t` guide + +### Append elements + +```cpp +array_t xs; + +xs.push_null(); +xs.push_bool(true); +xs.push_int(1); +xs.push_i64(2); +xs.push_f64(3.14); +xs.push_cstr("hi"); +xs.push_string("hello"); +xs.push_back(obj({"k", "v"})); // token from object +``` + +### Ensure index + +`ensure(idx)` grows the array with nulls if needed, then returns element `idx`. + +```cpp +array_t xs; + +token& slot = xs.ensure(3); // size becomes 4, filled with null +slot = "value"; +``` + +### Remove at index + +```cpp +xs.erase_at(1); +``` + +--- + +## `kvs` guide + +### Find and contains + +```cpp +kvs o = obj({"a", 1, "b", 2}); + +o.contains("a"); +std::size_t i = o.find_key_index("b"); // raw index in flat storage +``` + +### Get pointer + +```cpp +if (const token* p = o.get_ptr("a")) +{ + // p is value token +} +``` + +### Get or create + +`operator[]` returns a reference to a value token. If missing, it creates key with null value. + +```cpp +kvs o; +o["x"].set_int(123); +``` + +### Set (insert or replace) + +```cpp +o.set("x", 123); +o.set("name", "Ada"); +``` + +### Typed getters + +```cpp +auto name = o.get_string("name"); // optional +auto age = o.get_i64("age"); // optional +auto ok = o.get_bool("active"); // optional +auto pi = o.get_f64("pi"); // optional +``` + +### Typed getters with defaults + +```cpp +std::string name = o.get_string_or("name", "unknown"); +std::int64_t age = o.get_i64_or("age", 0); +bool active = o.get_bool_or("active", false); +double pi = o.get_f64_or("pi", 0.0); +``` + +### Typed setters + +```cpp +o.set_string("name", "Ada"); +o.set_bool("active", true); +o.set_f64("pi", 3.14); +o.set_i64("age", 42); +o.set_int("count", 7); +o.set_ll("big", 999LL); +o.set_ull("ubig", 999ULL); +``` + +### Ensure nested array/object + +```cpp +kvs root; + +kvs& user = root.ensure_object("user"); +user.set_string("name", "Ada"); + +array_t& roles = user.ensure_array("roles"); +roles.push_cstr("admin"); +``` + +--- + +## Builders and aliases + +### Builders (existing API) + +- `array(...)` builds `array_t` +- `obj(...)` builds `kvs` + +These are the primary entry points. + +### Explicit aliases + +When you also include other JSON helpers, the names `obj` and `array` may feel ambiguous. + +These aliases keep intent explicit: + +- `simple_obj(...)` +- `simple_array(...)` + +They do not change behavior, they only forward to `obj()` and `array()`. + +--- + +## Important fix: integer ambiguity + +Some platforms define `std::int64_t` as `long` (not `long long`). + +In those cases, passing a `long long` into `token` inside an `initializer_list` could become ambiguous. + +This header adds explicit support for: +- `long long` +- `unsigned long long` + +So call sites like this remain stable and unambiguous: + +```cpp +kvs o = obj({ + "big", 123LL, + "ubig", 123ULL +}); +``` + +--- + +## When to use Simple vs nlohmann::json + +Use Simple when: +- you need lightweight in-memory payloads between modules +- you want a minimal value model without external dependency +- you want predictable structure and cheap copying + +Use `nlohmann::json` when: +- you parse or serialize JSON text +- you need integration with external JSON tooling +- you want richer conversion utilities + +--- + +## Summary + +`vix/json/Simple.hpp` provides a minimal JSON-like model for internal use: + +- `token` for values (primitive or recursive) +- `array_t` for arrays +- `kvs` for objects (flat storage) +- builders: `array(...)` and `obj(...)` +- explicit aliases: `simple_array(...)`, `simple_obj(...)` + +It keeps internal structured data easy, dependency-free, and predictable. + + diff --git a/docs/examples/json-parsers.md b/docs/examples/json-parsers.md new file mode 100644 index 0000000..1433d02 --- /dev/null +++ b/docs/examples/json-parsers.md @@ -0,0 +1,165 @@ +# JSON Parsers Guide + +Beginner-friendly guide to the JSON body parser middleware. + +This middleware: +- Validates Content-Type +- Parses request body into nlohmann::json +- Stores parsed JSON into request state +- Returns structured errors automatically + +## What the middleware does + +When installed: + +1) Checks body size (optional) +2) Validates Content-Type: application/json +3) Parses JSON +4) Stores result in: +```cpp + req.state() +``` +## Minimal Example + +Save as: json_app_simple.cpp + +```cpp +#include +#include + +using namespace vix; + +int main() +{ + App app; + + app.use("/json", middleware::app::json_dev( + 256, // max_bytes + true, // allow_empty + true // require_content_type + )); + + app.post("/json", [](Request& req, Response& res) + { + auto& jb = req.state(); + + res.json({ + "ok", true, + "received", jb.value + }); + }); + + app.run(8080); +} +``` + +Run: + +```bash +vix run json_app_simple.cpp +``` + +## Test with curl + +### Valid JSON + +```bash +curl -i -X POST http://localhost:8080/json -H "Content-Type: application/json" --data '{"x":1}' +``` + +Result: 200 OK + +### Invalid JSON + +```bash +curl -i -X POST http://localhost:8080/json -H "Content-Type: application/json" --data '{"x":}' +``` + +Result: 400 invalid_json + +### Wrong Content-Type + +```bash +curl -i -X POST http://localhost:8080/json -H "Content-Type: text/plain" --data '{"x":1}' +``` + +Result: 415 unsupported_media_type + +## Strict Mode Example + +Disallow empty body: + +```cpp +app.use("/json", middleware::app::json_dev( + 256, + false, // allow_empty = false + true +)); +``` + +Now: + +Empty body -> 400 empty_body + +## Error Codes + +400 empty_body +400 invalid_json +413 payload_too_large +415 unsupported_media_type + +## Production Tips + +✔ Always keep require_content_type = true +✔ Set max_bytes to prevent abuse +✔ Combine with body_limit middleware globally +✔ Combine with CSRF for browser APIs + +## Complete Copy-Paste Example + +```cpp +#include +#include + +using namespace vix; + +int main() +{ + App app; + + app.use("/api", middleware::app::json_dev( + 512, + false, + true + )); + + app.post("/api/data", [](Request& req, Response& res) + { + auto& jb = req.state(); + + if (!jb.value.contains("name")) + { + res.status(400).json({ + "ok", false, + "error", "name is required" + }); + return; + } + + res.json({ + "ok", true, + "name", jb.value["name"] + }); + }); + + app.run(8080); +} +``` + +Test: + +```bash +curl -i -X POST http://localhost:8080/api/data -H "Content-Type: application/json" --data '{"name":"Gaspard"}' +``` +You now understand how JSON parsing works internally in Vix.cpp. + diff --git a/docs/examples/json_builders_routes.md b/docs/examples/json_builders_routes.md deleted file mode 100644 index a4eae2a..0000000 --- a/docs/examples/json_builders_routes.md +++ /dev/null @@ -1,37 +0,0 @@ -# Example — json_builders_routes - -```cpp -#include -#include - -using namespace vix; -namespace J = vix::json; - -int main() -{ - App app; - - // GET /hello -> {"message": "Hello, World!"} - app.get("/hello", [](Request &, Response &res){ - res.json({"message", "Hello, World!"}); - }); - - // GET /users/{id} -> {"user": {"id": "...", "active": true}} - app.get("/users/{id}", [](Request &req, Response &res){ - const std::string id = req.param("id"); - res.json({ - "user", J::obj({ - "id", id, - "active", true - }) - }); - }); - - // GET /roles -> {"roles": ["admin", "editor", "viewer"]} - app.get("/roles", [](Request &, Response &res){ - res.json({"roles", J::array({"admin", "editor", "viewer"})}); - }); - - app.run(8080); -} -``` diff --git a/docs/examples/jwt.md b/docs/examples/jwt.md new file mode 100644 index 0000000..d4da981 --- /dev/null +++ b/docs/examples/jwt.md @@ -0,0 +1,164 @@ +# JWT (Beginner Guide) + +Welcome 👋\ +This page teaches you how to use **JWT authentication** in Vix.cpp with +minimal examples. + +JWT is useful when you want: + +- stateless auth (no server sessions) +- API tokens for mobile apps and CLIs +- simple role based access (admin, user, etc.) +- permissions like `products:write` + + +## What is a JWT? + +A JWT is just a string with 3 parts: + + header.payload.signature + +- **header** says which algorithm is used (example: HS256) +- **payload** contains claims (sub, roles, perms) +- **signature** proves the token was created using your secret + +In Vix.cpp, JWT middleware checks: + +- the token exists in `Authorization: Bearer ` +- signature is valid for your secret +- (optionally) exp is valid + +# 1) Smallest JWT protected route (App) + +This is the simplest pattern: + +- `/` is public +- `/secure` requires a Bearer token + +``` cpp +#include +#include + +#include +#include + +using namespace vix; + +static const std::string kToken = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." + "eyJzdWIiOiJ1c2VyMTIzIiwicm9sZXMiOlsiYWRtaW4iXX0." + "3HK5b1sXMbxkjC3Tllwtcuzxm-1OI0D184Fuav0-XQo"; // HS256, secret=dev_secret + +int main() +{ + App app; + + // Protect only /secure (verify_exp=false for beginner demo) + app.use("/secure", middleware::app::jwt_dev("dev_secret")); + + app.get("/", [](Request &, Response &res) + { + res.send( + "JWT example:\n" + " GET /secure requires Bearer token.\n" + "\n" + "Try:\n" + " curl -i http://localhost:8080/secure\n" + " curl -i -H \"Authorization: Bearer \" http://localhost:8080/secure\n" + ); + }); + + app.get("/secure", [](Request &req, Response &res) + { + auto &claims = req.state(); + + res.json({ + "ok", true, + "sub", claims.subject, + "roles", claims.roles + }); + }); + + std::cout + << "Server: http://localhost:8080/\n" + << "Secure: http://localhost:8080/secure\n\n" + << "Token:\n" << kToken << "\n"; + + app.run(8080); + return 0; +} +``` + +## Test with curl + +Run: + +``` bash +vix run jwt_app_simple.cpp +``` + +No token: + +``` bash +curl -i http://localhost:8080/secure +``` + +With token: + +``` bash +TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMTIzIiwicm9sZXMiOlsiYWRtaW4iXX0.3HK5b1sXMbxkjC3Tllwtcuzxm-1OI0D184Fuav0-XQo" +curl -i -H "Authorization: Bearer $TOKEN" http://localhost:8080/secure +``` + +# 2) Reading JWT claims inside your handler + +After JWT middleware succeeds, claims are stored in request state: + +``` cpp +auto &claims = req.state(); + +// Common fields: +claims.subject // "sub" +claims.roles // "roles" (array) +``` + +So you can do: + +``` cpp +app.get("/secure", [](Request &req, Response &res) +{ + auto &claims = req.state(); + res.json({ + "ok", true, + "sub", claims.subject, + "roles", claims.roles + }); +}); +``` + +# 3) Common beginner mistakes + +1) Secret mismatch\ + The token is signed with a secret. Your middleware must use the same + secret. + +2) Forgetting the Bearer prefix\ + It must be: + +`Authorization: Bearer ` + +3) Not enabling JWT in build\ + If your build has a flag like `VIX_ENABLE_JWT`, you must enable it, + otherwise middleware might not exist. + +4) Forgetting expiration handling\ + For beginner demos we use `verify_exp=false`. In production, you + usually want `verify_exp=true` and an `exp` claim. + +# Summary + +- Protect a prefix: `app.use("/secure", jwt_dev("secret"))` +- Send token in header: `Authorization: Bearer ` +- Read claims from request state after middleware success +- For dev, use a token generator tool + diff --git a/docs/examples/logging-json.md b/docs/examples/logging-json.md new file mode 100644 index 0000000..650c900 --- /dev/null +++ b/docs/examples/logging-json.md @@ -0,0 +1,245 @@ +# Logging (JSON) in Vix.cpp + +You are mixing two different things: + +- **`dumps()`** is like `JSON.stringify()` in JavaScript. It converts JSON to a string. +- **`console.*`** is like `console.log()` in JavaScript. It prints logs (with levels, filtering, stdout/stderr routing). + +So the answer is: + +- If you want a **string**, use `dumps()` or `dumps_compact()`. +- If you want to **log**, use `vix::console` (dev) or `vix::utils::Logger` (production). +- If you want **JSON logs**, log a single line JSON string (usually compact) so log collectors can parse it. + +--- + +## Include + +```cpp +#include +#include +``` + +```cpp +using namespace vix; +using vix::json::Json; +using vix::json::dumps; +using vix::json::dumps_compact; +``` + +--- + +## dumps vs console: mapping to JS + +| JavaScript | Vix.cpp equivalent | What it does | +|---|---|---| +| `JSON.stringify(obj)` | `dumps_compact(j)` | serialize JSON to one line | +| `JSON.stringify(obj, null, 2)` | `dumps(j)` | serialize JSON pretty | +| `console.log(...)` | `console.info(...)` | log to stdout | +| `console.error(...)` | `console.error(...)` | log to stderr | +| structured log line (JSON) | `console.info(dumps_compact(j))` | easy ingestion by ELK/Loki | + +--- + +## 1) Dev logging: console + compact JSON + +This is the simplest and most useful pattern for "logging-json". + +```cpp +#include +#include + +using namespace vix; +using vix::json::Json; +using vix::json::dumps_compact; + +int main() +{ + App app; + + app.get("/api/ping", [](Request& req, Response& res) + { + Json log = { + {"level", "info"}, + {"event", "ping"}, + {"method", req.method()}, + {"path", req.path()}, + }; + + console.info(dumps_compact(log)); + + res.json({ "ok", true }); + }); + + app.run(8080); + return 0; +} +``` + +Why compact? +- One request produces one log line. +- Easy parsing by tools. +- Fast and low overhead. + +--- + +## 2) Dev pretty logs: only when debugging + +Pretty JSON is human-friendly but it creates multi-line logs. + +```cpp +Json j = { + {"event", "debug_dump"}, + {"path", "/api/ping"} +}; + +console.debug(dumps(j)); // multi-line +``` + +Use it only during debugging. + +--- + +## 3) Log format: json-pretty vs one-line JSON + +In Vix, you have two layers: + +1) **Your log payload**: the JSON you decide to log +2) **The runtime log format** (CLI flags): how internal runtime logs are printed + +Recommended: +- Your payload: `dumps_compact(payload)` for stable one-line ingestion +- Runtime logs: `--log-format=json-pretty` during development + +Example run: + +```bash +vix run logging_json_server.cpp --log-level=debug --log-format=json-pretty --log-color=always +``` + +--- + +## 4) Production logging: use Logger, not console + +Console is great for development. +In production you usually want: +- sinks (file, stdout, syslog, collector) +- structured fields +- consistent schema +- rotation policies + +If you have `vix::utils::Logger`, use it as your main production logger. + +Pattern: + +```cpp +#include +#include +// include your logger header (depends on your module path) +#include + +using namespace vix; +using vix::json::Json; +using vix::json::dumps_compact; + +int main() +{ + App app; + + app.get("/api/ping", [](Request& req, Response& res) + { + Json evt = { + {"event", "ping"}, + {"method", req.method()}, + {"path", req.path()}, + }; + + // Option A: Logger can accept a string + vix::log().info(dumps_compact(evt)); + + res.json({ "ok", true }); + }); + + app.run(8080); + return 0; +} +``` + +--- + +## 5) A minimal JSON log schema (recommended) + +Keep a stable schema so dashboards and alerts are easy. + +Fields that work well: +- `ts` (timestamp) if your logger does not add it automatically +- `level` +- `event` +- `method` +- `path` +- `status` +- `request_id` (if you set one) +- `latency_ms` + +Example payload: + +```cpp +Json evt = { + {"level", "info"}, + {"event", "request_done"}, + {"method", req.method()}, + {"path", req.path()}, + {"status", 200}, + {"latency_ms", 3} +}; +``` + +--- + +## 6) Do not log secrets + +Never log these: +- Authorization header +- cookies +- tokens +- passwords +- personal data + +If you must log headers, redact: + +```cpp +auto ua = req.header("User-Agent"); +console.debug("ua=", ua); +``` + +Do not print Authorization. + +--- + +## 7) Quick test + +Run: + +```bash +vix run logging_json_server.cpp +``` + +Test: + +```bash +curl -i http://localhost:8080/api/ping +``` + +You should see: +- a JSON log line in your terminal +- a JSON response from the endpoint + +--- + +## Summary + +- `dumps()` and `dumps_compact()` are JSON serialization helpers, not logging. +- `console.*` is like JavaScript console logging. +- For JSON logs: `console.info(dumps_compact(payload))`. +- For production: prefer `vix::utils::Logger` with structured fields. + diff --git a/docs/examples/middleware.md b/docs/examples/middleware.md new file mode 100644 index 0000000..1164a3f --- /dev/null +++ b/docs/examples/middleware.md @@ -0,0 +1,206 @@ +# Middleware + +This section demonstrates how middleware works in Vix.cpp. + +Each example is minimal and self-contained. + +--- + +## 1. Global Middleware + +Runs before every route. + +```cpp +#include +#include + +using namespace vix; + +int main() +{ + App app; + + app.use(vix::middleware::app::adapt( + [](Request& req, Response& res, vix::middleware::Next next) + { + res.header("x-powered-by", "Vix"); + next(); + } + )); + + app.get("/", [](Request&, Response& res) + { + res.send("Hello with middleware"); + }); + + app.run(8080); + return 0; +} +``` + +--- + +## 2. Prefix Middleware + +Middleware only applied to a path prefix. + +```cpp +#include +#include + +using namespace vix; + +int main() +{ + App app; + + using namespace vix::middleware::app; + + install(app, "/api/", + rate_limit_dev(60, std::chrono::minutes(1)) + ); + + app.get("/api/ping", [](Request&, Response& res) + { + res.json({"ok", true}); + }); + + app.get("/", [](Request&, Response& res) + { + res.send("Public route"); + }); + + app.run(8080); + return 0; +} +``` + +--- + +## 3. Simple Auth Middleware + +Protect a route with a header check. + +```cpp +#include +#include + +using namespace vix; + +int main() +{ + App app; + + app.use(vix::middleware::app::adapt( + [](Request& req, Response& res, vix::middleware::Next next) + { + if (req.header("x-api-key") != "secret") + { + res.status(401).json({ + "ok", false, + "error", "Unauthorized" + }); + return; + } + next(); + } + )); + + app.get("/secure", [](Request&, Response& res) + { + res.json({"ok", true}); + }); + + app.run(8080); + return 0; +} +``` + +Test: + + curl -H "x-api-key: secret" http://localhost:8080/secure + +--- + +## 4. Chaining Middlewares + +```cpp +#include +#include + +using namespace vix; + +int main() +{ + App app; + + app.use(vix::middleware::app::adapt( + [](Request&, Response& res, vix::middleware::Next next) + { + res.header("x-a", "1"); + next(); + } + )); + + app.use(vix::middleware::app::adapt( + [](Request&, Response& res, vix::middleware::Next next) + { + res.header("x-b", "2"); + next(); + } + )); + + app.get("/", [](Request&, Response& res) + { + res.send("Check headers"); + }); + + app.run(8080); + return 0; +} +``` + +--- + +## 5. Context-Based Middleware + +Using adapt_ctx with Context API. + +```cpp +#include +#include + +using namespace vix; + +int main() +{ + App app; + + app.use(vix::middleware::app::adapt_ctx( + [](vix::middleware::Context& ctx, vix::middleware::Next next) + { + ctx.res().header("x-context", "true"); + next(); + } + )); + + app.get("/", [](Request&, Response& res) + { + res.send("Context middleware"); + }); + + app.run(8080); + return 0; +} +``` + +--- + +## What this teaches + +- Global middleware +- Prefix-based middleware +- Route protection +- Middleware chaining +- Context-based middleware + diff --git a/docs/examples/migrations.md b/docs/examples/migrations.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/examples/multipart.md b/docs/examples/multipart.md new file mode 100644 index 0000000..d552912 --- /dev/null +++ b/docs/examples/multipart.md @@ -0,0 +1,328 @@ +# Multipart Uploads + +This page gives small, copy-paste examples for **multipart/form-data** uploads in Vix.cpp. + +It covers two related middlewares: + +- `middleware::parsers::multipart()` + Validates `Content-Type: multipart/form-data` + extracts the boundary (no parsing, no saving). +- `middleware::parsers::multipart_save()` + Parses multipart parts and saves files to disk + collects text fields. + +Each section has: one concept, one minimal `main()`, a quick curl test. + +--- + +## 0) Mental model + +A multipart request contains many parts: + +- text fields: `name=value` +- file fields: `file=@path` + +The middleware builds a `MultipartForm`: + +- `form.fields["title"] = "..."` +- `form.files[i].saved_path = "uploads/..."` + +The handler reads `req.state()`. + +--- + +## 1) Minimal: save files + echo summary + +Save as: `multipart_save_app_simple.cpp` + +```cpp +/** + * + * @file multipart_save_app_simple.cpp - Multipart save (minimal) + * + * Run: + * vix run multipart_save_app_simple.cpp + * + * Test: + * curl -i -X POST http://localhost:8080/upload \ + * -F "title=hello" \ + * -F "image=@./pic.png" + * + */ +#include +#include + +using namespace vix; + +int main() +{ + App app; + + // Save files to ./uploads and store MultipartForm in request state + app.use("/upload", middleware::app::multipart_save_dev("uploads")); + + app.post("/upload", [](Request &req, Response &res) + { + auto &form = req.state(); + + // Helper that converts MultipartForm to JSON (from presets.hpp) + res.json(middleware::app::multipart_json(form, "uploads")); + }); + + app.get("/", [](Request &, Response &res) + { + res.send("POST /upload as multipart/form-data"); + }); + + app.run(8080); + return 0; +} +``` + +--- + +## 2) Limits: max request size, max files, max per-file size + +`multipart_save()` has strict limits in `MultipartSaveOptions`: + +- `max_bytes` total request body +- `max_files` number of file parts +- `max_file_bytes` size per file + +Save as: `multipart_save_limits.cpp` + +```cpp +#include +#include +#include + +using namespace vix; + +int main() +{ + App app; + + middleware::parsers::MultipartSaveOptions opt{}; + opt.upload_dir = "uploads"; + opt.max_bytes = 2 * 1024 * 1024; // 2 MB total + opt.max_files = 2; // at most 2 files + opt.max_file_bytes = 1 * 1024 * 1024; // 1 MB per file + opt.keep_original_filename = false; // safer default + opt.keep_extension = true; + + app.use("/upload", middleware::app::adapt_ctx( + middleware::parsers::multipart_save(opt))); + + app.post("/upload", [](Request &req, Response &res) + { + auto &form = req.state(); + res.json(middleware::app::multipart_json(form, "uploads")); + }); + + app.run(8080); + return 0; +} +``` + +Expected errors: + +- 413 `payload_too_large` if request too big +- 413 `too_many_files` if file count exceeds max +- 413 `file_too_large` if one file exceeds `max_file_bytes` +- 415 `unsupported_media_type` if not multipart/form-data +- 400 `missing_boundary` if boundary missing + +--- + +## 3) Filename policy: safest defaults + +Recommended defaults: + +- `keep_original_filename=false` (generate safe unique names) +- `keep_extension=true` (keep `.png`, `.zip`, ...) + +Save as: `multipart_save_filename_policy.cpp` + +```cpp +#include +#include +#include + +using namespace vix; + +int main() +{ + App app; + + middleware::parsers::MultipartSaveOptions opt{}; + opt.upload_dir = "uploads"; + opt.keep_original_filename = false; // recommended + opt.keep_extension = true; // keep .png, .jpg, ... + opt.default_basename = "upload"; // upload__.ext + + app.use(middleware::app::adapt_ctx(middleware::parsers::multipart_save(opt))); + + app.post("/upload", [](Request &req, Response &res) + { + auto &form = req.state(); + res.json(middleware::app::multipart_json(form, "uploads")); + }); + + app.run(8080); + return 0; +} +``` + +--- + +## 4) Read text fields + file paths (practical handler) + +Save as: `multipart_profile_update.cpp` + +```cpp +/** + * Upload avatar + username in one request. + * + * Test: + * curl -i -X POST http://localhost:8080/profile \ + * -F "username=gaspard" \ + * -F "avatar=@./pic.png" + */ +#include +#include + +using namespace vix; + +static std::string field_or_empty( + const middleware::parsers::MultipartForm &f, + const std::string &key) +{ + auto it = f.fields.find(key); + return it == f.fields.end() ? "" : it->second; +} + +int main() +{ + App app; + + app.use("/profile", middleware::app::multipart_save_dev("uploads")); + + app.post("/profile", [](Request &req, Response &res) + { + auto &form = req.state(); + + const std::string username = field_or_empty(form, "username"); + + std::string avatar_path; + for (const auto &file : form.files) + { + if (file.field_name == "avatar") + { + avatar_path = file.saved_path; + break; + } + } + + res.json({ + "ok", true, + "username", username, + "avatar_saved_path", avatar_path, + "files_count", (long long)form.files.size() + }); + }); + + app.run(8080); + return 0; +} +``` + +--- + +## 5) Lightweight probe: validate multipart only (no parsing, no saving) + +Use `middleware::parsers::multipart()` when you only want header validation. + +Save as: `multipart_probe.cpp` + +```cpp +#include +#include +#include + +using namespace vix; + +int main() +{ + App app; + + middleware::parsers::MultipartOptions opt{}; + opt.require_boundary = true; + opt.max_bytes = 512 * 1024; // 512 KB + opt.store_in_state = true; + + app.use("/probe", middleware::app::adapt_ctx(middleware::parsers::multipart(opt))); + + app.post("/probe", [](Request &req, Response &res) + { + auto &info = req.state(); + res.json({ + "ok", true, + "content_type", info.content_type, + "boundary", info.boundary, + "body_bytes", (long long)info.body_bytes + }); + }); + + app.run(8080); + return 0; +} +``` + +--- + +## 6) curl cheatsheet + +Upload file + fields: + +```bash +curl -i -X POST http://localhost:8080/upload \ + -F "title=hello" \ + -F "image=@./pic.png" +``` + +Multiple files: + +```bash +curl -i -X POST http://localhost:8080/upload \ + -F "images=@./a.png" \ + -F "images=@./b.png" +``` + +Force wrong content-type (should fail 415): + +```bash +curl -i -X POST http://localhost:8080/upload \ + -H "Content-Type: text/plain" \ + --data "x" +``` + +--- + +## 7) Production tips + +- Keep strict limits (`max_bytes`, `max_files`, `max_file_bytes`). +- Prefer generated names (`keep_original_filename=false`). +- Store only `saved_path` in DB (not raw body). +- Validate types after save: + - check `file.content_type` + - check extension + - optionally verify magic bytes + +--- + +## Summary + +- Use `multipart_save()` to parse and save files. +- Use `multipart()` to validate multipart headers only. +- Read results from request state: + - `req.state()` + - `req.state()` + diff --git a/docs/examples/notifications.md b/docs/examples/notifications.md new file mode 100644 index 0000000..a68832c --- /dev/null +++ b/docs/examples/notifications.md @@ -0,0 +1,143 @@ +# Notifications (HTTP + WebSocket) + +This example shows how to build a minimal real-time notification system in Vix.cpp. + +Goal: +- Send notifications from HTTP +- Receive them instantly via WebSocket +- Keep everything beginner-friendly + +## 1) Minimal Notification Server + +Description: +- HTTP route POST /notify sends a notification +- WebSocket broadcasts it to all connected clients + +main.cpp: + +```cpp +#include +#include + +using namespace vix; + +int main() +{ + vix::serve_http_and_ws([](App& app, auto& ws) + { + // Health check + app.get("/health", [](Request&, Response& res) + { + res.json({"ok", true, "service", "notifications"}); + }); + + // Send notification via HTTP + app.post("/notify", [&ws](Request& req, Response& res) + { + const auto& j = req.json(); + + std::string title = "Notification"; + std::string message = "Empty"; + + if (j.is_object()) + { + if (j.contains("title") && j["title"].is_string()) + title = j["title"].get(); + + if (j.contains("message") && j["message"].is_string()) + message = j["message"].get(); + } + + ws.broadcast_json("notification", { + "title", title, + "message", message + }); + + res.json({ + "sent", true, + "title", title + }); + }); + + }); + + return 0; +} +``` + +## 2) How To Compile & Run + +From your project folder: + +```bash +vix run main.cpp +``` + +By default: +- HTTP runs on: http://127.0.0.1:8080 +- WebSocket runs on: ws://127.0.0.1:9090 + +## 3) Test HTTP Endpoint + +Send a notification: + +```bash +curl -X POST http://127.0.0.1:8080/notify -H "Content-Type: application/json" -d '{"title":"System","message":"Server restarted"}' +``` + +Expected HTTP response: + +```json +{ + "sent": true, + "title": "System" +} +``` + +## 4) Test WebSocket Client + +Using websocat: + +```bash +websocat ws://127.0.0.1:9090/ +``` + +Now call the HTTP /notify endpoint again. +You will instantly receive: + +```json +{ + "type": "notification", + "payload": { + "title": "System", + "message": "Server restarted" + } +} +``` + +## 5) Beginner Mental Model + +HTTP: +- Used to trigger actions (create notification) + +WebSocket: +- Used to push real-time updates + +This separation is common in production systems. + +## 6) Extend This Example + +You can extend this by: + +- Adding user IDs to target specific clients +- Storing notifications in memory or database +- Adding "read" acknowledgements +- Adding authentication middleware + +What You Learned + +- How to mix HTTP and WebSocket +- How to broadcast typed messages +- How to test using curl + websocat +- How real-time notification systems are structured + diff --git a/docs/examples/openapi.md b/docs/examples/openapi.md new file mode 100644 index 0000000..4fe036c --- /dev/null +++ b/docs/examples/openapi.md @@ -0,0 +1,150 @@ +# OpenAPI and Docs UI (offline Swagger) + +This module generates an OpenAPI 3 document from your router and serves an offline Swagger UI that works without CDN assets. + +It is designed for Vix deployment environments where internet access is limited or fully offline. + +## What you get + +When enabled, these routes are registered: + +- `GET /openapi.json` + Generated OpenAPI 3.0.3 document (JSON) + +- `GET /docs` + Swagger UI HTML page (uses local assets) + +- `GET /docs/` + Same as `/docs` (no redirect) + +- `GET /docs/index.html` + Same as `/docs` (convenience) + +- `GET /docs/swagger-ui.css` + Embedded Swagger UI CSS (served locally) + +- `GET /docs/swagger-ui-bundle.js` + Embedded Swagger UI JS bundle (served locally) + +## Core types + +### RouteDoc + +`vix::router::RouteDoc` carries documentation metadata for one route: + +- `summary` and `description` +- `tags` +- `request_body` (OpenAPI requestBody payload) +- `responses` (OpenAPI responses map) +- `x` (vendor extensions, for example `x-vix-heavy`) + +Your router stores this doc next to the route record so OpenAPI generation can read it later. + +### OpenAPI Registry + +Some modules might not register routes through the core HTTP router, or they want to add docs entries for routes created elsewhere. +`vix::openapi::Registry` is a global registry for extra docs: + +- `Registry::add(method, path, doc)` +- `Registry::snapshot()` + +During generation, the builder merges: + +1) `router.routes()` +2) `Registry::snapshot()` + +Duplicate (method, path) entries are automatically ignored. + +## Build OpenAPI JSON + +Use `vix::openapi::build_from_router(router, title, version)` to generate the OpenAPI document: + +```cpp +#include + +auto spec = vix::openapi::build_from_router(router, "Vix API", "0.0.0"); +// spec is nlohmann::json +``` + +Notes: +- `operationId` is stable and derived from `method + path` +- if a route has no `responses`, a default `200: OK` response is added +- `RouteDoc::x` is copied as vendor fields into the OpenAPI operation object + +## Register the docs routes + +Call `register_openapi_and_docs(router, title, version)`: + +```cpp +#include + +vix::openapi::register_openapi_and_docs(*server.getRouter(), "Vix API", "0.0.0"); +``` + +This registers both: +- `/openapi.json` +- `/docs` and local assets under `/docs/*` + +## Offline Swagger UI behavior + +The HTML returned by `/docs` is generated by: + +```cpp +#include + +auto html = vix::openapi::swagger_ui_html("/openapi.json"); +``` + +Important implementation details: +- It uses `` so it works for `/docs` and `/docs/` +- CSS and JS are loaded as relative paths: `swagger-ui.css` and `swagger-ui-bundle.js` +- The page fetches `/openapi.json` to display the API title and version in the header +- It sets security headers like `X-Content-Type-Options: nosniff` +- Swagger assets are served with long cache headers (`max-age=31536000, immutable`) + +## Heavy routes metadata + +If a route is registered with `RouteOptions{ .heavy = true }`, the router injects: + +```json +"x-vix-heavy": true +``` + +into `RouteDoc::x`, so it appears in OpenAPI. +This is useful for tooling or client generation. + +## Disable auto docs + +If you want to disable docs and OpenAPI generation from the CLI: + +```bash +vix run api.cpp --no-docs +``` + +When `--no-docs` is used, your app should skip calling `register_openapi_and_docs(...)`. + +## Minimal integration example + +```cpp +#include +#include + +int main() +{ + vix::config::Config cfg; + auto exec = /* create executor */; + + vix::server::HTTPServer server(cfg, exec); + + // register your API routes here + auto r = server.getRouter(); + + // register docs + openapi + vix::openapi::register_openapi_and_docs(*r, "Vix API", "0.0.0"); + + server.run(); + return 0; +} +``` + + diff --git a/docs/examples/outbox.md b/docs/examples/outbox.md new file mode 100644 index 0000000..af20cd9 --- /dev/null +++ b/docs/examples/outbox.md @@ -0,0 +1,346 @@ +# Outbox Pattern + +This page shows how to use the Outbox pattern in Vix.cpp to make local writes durable before any network attempt. + +If you are building offline-first systems (mobile, edge, unstable networks), this is one of the safest building blocks you can start with. + +You will learn: + +- What the Outbox pattern is (in simple terms) +- How to enqueue operations (durable local writes) +- How to process operations with a transport +- How retries and permanent failures work +- How to test everything quickly on your machine + +--- + +## What is the Outbox pattern? + +When your app wants to do a remote action (send an HTTP request, publish an event, replicate to a peer), you have two choices: + +1. Do the network call immediately +2. Save the intent locally first, then do the network call + +The Outbox pattern is the second choice: + +- You first persist an Operation on disk (durable) +- A worker later sends it over the network +- If the network fails, the operation stays on disk and is retried + +This prevents a common failure: + +- "User clicked Save, we said OK, then the network dropped and the write was lost." + +With Outbox: + +- If the app accepted the write, it is stored locally and cannot disappear. + +--- + +## Concepts used in Vix sync + +- Operation: the unit of work you want to deliver +- Outbox: durable queue that stores operations and manages lifecycle +- OutboxStore: persistence backend (file store, database store, etc.) +- SyncEngine: drives workers that send operations using a transport +- Transport: pluggable sender (HTTP, WS, P2P, edge gateway, etc.) + +--- + +## 1) Minimal Outbox (enqueue + send + Done) + +This is the smallest complete workflow: + +- create a file-backed outbox store +- create an outbox +- enqueue one operation +- run one engine tick +- verify it becomes Done + +### Example + +```cpp +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +// A tiny rule-based transport used in examples/tests +#include "fake_http_transport.hpp" + +static std::int64_t now_ms() +{ + using namespace std::chrono; + return duration_cast(steady_clock::now().time_since_epoch()).count(); +} + +int main() +{ + using namespace vix::sync; + using namespace vix::sync::outbox; + using namespace vix::sync::engine; + + auto store = std::make_shared(FileOutboxStore::Config{ + .file_path = "./.vix_demo/outbox.json", + .pretty_json = true, + .fsync_on_write = false}); + + auto outbox = std::make_shared(Outbox::Config{ + .owner = "demo-engine", + .auto_generate_ids = true, + .auto_generate_idempotency_key = true}, store); + + auto probe = std::make_shared( + vix::net::NetworkProbe::Config{}, + [] { return true; }); // always online for demo + + auto transport = std::make_shared(); + transport->setDefault({.ok = true}); + + SyncEngine engine(SyncEngine::Config{.worker_count = 1, .batch_limit = 10}, + outbox, probe, transport); + + Operation op; + op.kind = "http.post"; + op.target = "/api/messages"; + op.payload = R"({"text":"hello from outbox"})"; + + const auto id = outbox->enqueue(op, now_ms()); + + const auto processed = engine.tick(now_ms()); + (void)processed; + + auto saved = store->get(id); + assert(saved.has_value()); + assert(saved->status == OperationStatus::Done); + + std::cout << "OK: operation was sent and marked Done\n"; + return 0; +} +``` + +### How to run this example + +Typical workflow (adapt to your project layout): + +```bash +# 1) Build +cmake -S . -B build && cmake --build build -j + +# 2) Run +./build/examples/outbox_minimal +``` + +### What to look for + +After running, you should see a local file created: + +- ./.vix_demo/outbox.json + +It represents durable state. Even if the process crashed, the operation record exists. + +--- + +## 2) Understanding Operation fields (beginner-friendly) + +An Operation has two parts: + +### A) Intent (what you want to do) + +- kind: category, ex: "http.post", "p2p.replicate" +- target: destination, ex: "/api/messages", "peer:abc123" +- payload: serialized data, ex: JSON string +- idempotency_key: used to deduplicate retries on remote side + +### B) Lifecycle (what happened so far) + +- status: Pending, InFlight, Done, Failed, PermanentFailed +- attempt: how many attempts were made +- next_retry_at_ms: when it can be retried +- last_error: last failure reason + +The key idea: + +- intent stays the same +- lifecycle evolves over time + +--- + +## 3) Retryable failure vs permanent failure + +Your transport returns: + +```cpp +struct SendResult +{ + bool ok{false}; + bool retryable{true}; + std::string error; +}; +``` + +Meaning: + +- ok = true -> operation becomes Done +- ok = false and retryable = true -> operation becomes Failed and will retry later +- ok = false and retryable = false -> operation becomes PermanentFailed and will not retry + +### Example: permanent failure + +```cpp +transport->setRuleForTarget("/api/messages", FakeHttpTransport::Rule{ + .ok = false, + .retryable = false, + .error = "bad request (permanent)" +}); +``` + +This is what you want for: + +- schema validation errors (400) +- forbidden (403) +- "this will never succeed if retried" + +--- + +## 4) Offline mode simulation (no network) + +A beginner way to understand offline-first is to simulate offline: + +```cpp +auto probe = std::make_shared( + vix::net::NetworkProbe::Config{}, + [] { return false; } // always offline +); +``` + +When offline: + +- the engine ticks, but does not deliver +- operations stay Pending (durable on disk) + +Later, when the probe returns true again, delivery resumes. + +--- + +## 5) Crash safety: stuck InFlight operations + +A classic crash scenario: + +1. Operation is claimed (status becomes InFlight) +2. Process crashes before marking Done/Failed +3. On restart, the operation is stuck InFlight forever if you do nothing + +Vix outbox stores support recovery: + +- requeue_inflight_older_than(now_ms, timeout_ms) + +This makes stuck InFlight operations eligible again. + +### Minimal recovery flow + +```cpp +// On startup (or periodically): +store->requeue_inflight_older_than(now_ms(), 10'000); // 10 seconds timeout +``` + +This is one of the simplest and most important reliability steps. + +--- + +## 6) Testing tips (beginner + expert) + +### Beginner checklist + +- Use a test directory like ./.vix_demo/ +- Set pretty_json = true so you can open the outbox file and understand it +- Use assert(...) in examples to fail fast and see what broke + +### Expert checklist + +- Enable fsync_on_write = true to reduce power-loss risk (slower but stronger) +- Add metrics: counts of Pending/Failed/PermanentFailed +- Log transitions: enqueue, claim, done, fail +- Ensure your remote endpoint uses idempotency_key to dedupe retries + +--- + +## Appendix: Minimal Fake Transport used by examples + +If you need a tiny transport for demos/tests, use this: + +```cpp +#ifndef VIX_FAKE_HTTP_TRANSPORT_HPP +#define VIX_FAKE_HTTP_TRANSPORT_HPP + +#include +#include + +#include + +namespace vix::sync::engine +{ + class FakeHttpTransport final : public ISyncTransport + { + public: + struct Rule + { + bool ok{true}; + bool retryable{true}; + std::string error{"simulated failure"}; + }; + + void setDefault(Rule r) { def_ = std::move(r); } + + void setRuleForKind(std::string kind, Rule r) + { + by_kind_[std::move(kind)] = std::move(r); + } + + void setRuleForTarget(std::string target, Rule r) + { + by_target_[std::move(target)] = std::move(r); + } + + std::size_t callCount() const noexcept { return calls_; } + + SendResult send(const vix::sync::Operation &op) override + { + ++calls_; + + if (auto it = by_target_.find(op.target); it != by_target_.end()) + return toResult(it->second); + + if (auto it = by_kind_.find(op.kind); it != by_kind_.end()) + return toResult(it->second); + + return toResult(def_); + } + + private: + static SendResult toResult(const Rule &r) + { + SendResult res; + res.ok = r.ok; + res.retryable = r.retryable; + res.error = r.ok ? "" : r.error; + return res; + } + + private: + Rule def_{}; + std::unordered_map by_kind_; + std::unordered_map by_target_; + std::size_t calls_{0}; + }; +} // namespace vix::sync::engine + +#endif +``` + diff --git a/docs/examples/overview.md b/docs/examples/overview.md deleted file mode 100644 index da37134..0000000 --- a/docs/examples/overview.md +++ /dev/null @@ -1,221 +0,0 @@ -# Vix – Request & Response Examples - -This document shows **complete, real-world examples** demonstrating how **simple, expressive, and powerful Vix** is. - -No low-level types. -No `vhttp::`. -No `ResponseWrapper`. -Just **Request** and **Response**. - ---- - -## 1. Hello World (JSON) - -```cpp -app.get("/", [](Request req, Response res) { - return json::o("message", "Hello from Vix"); -}); -``` - ---- - -## 2. Route Parameters - -```cpp -app.get("/users/{id}", [](Request req, Response res) { - auto id = req.param("id"); - return json::o("user_id", id); -}); -``` - ---- - -## 3. Query Parameters - -```cpp -app.get("/search", [](Request req, Response res) { - auto q = req.query_value("q", "none"); - auto page = req.query_value("page", "1"); - - return json::o( - "query", q, - "page", page - ); -}); -``` - ---- - -## 4. Automatic Status + Payload (FastAPI style) - -```cpp -app.get("/missing", [](Request req, Response res) { - return std::pair{ - 404, - json::o("error", "Not found") - }; -}); -``` - ---- - -## 5. Redirect - -```cpp -app.get("/go", [](Request req, Response res) { - res.redirect("https://vixcpp.com"); -}); -``` - ---- - -## 6. Automatic Status Message - -```cpp -app.get("/forbidden", [](Request req, Response res) { - res.status(403).send(); -}); -``` - ---- - -## 7. POST JSON Body - -```cpp -app.post("/echo", [](Request req, Response res) { - return json::o( - "received", req.json() - ); -}); -``` - ---- - -## 8. Typed JSON Parsing - -```cpp -struct UserInput { - std::string name; - int age; -}; - -app.post("/users", [](Request req, Response res) { - UserInput input = req.json_as(); - - return std::pair{ - 201, - json::o( - "name", input.name, - "age", input.age - ) - }; -}); -``` - ---- - -## 9. Headers - -```cpp -app.get("/headers", [](Request req, Response res) { - res.header("X-App", "Vix") - .type("text/plain") - .send("Hello headers"); -}); -``` - ---- - -## 10. Request-Scoped State - -```cpp -app.get("/state", [](Request req, Response res) { - req.set_state(42); - - return json::o( - "value", req.state() - ); -}); -``` - ---- - -## 11. Void Handler - -```cpp -app.get("/manual", [](Request req, Response res) { - res.status(200) - .json(json::o("ok", true)); -}); -``` - ---- - -## 12. Params Map Access - -```cpp -app.get("/items/{id}", [](Request req, Response res) { - const auto& params = req.params(); - return json::o("id", params.at("id")); -}); -``` - ---- - -## 13. 204 No Content - -```cpp -app.delete("/items/{id}", [](Request req, Response res) { - res.status(204).send(); -}); -``` - -```cpp -#include -using namespace vix; - -int main() -{ - App app; - - // Basic JSON response (auto send) - app.get("/", [](Request req, Response res) { - res.send("message", "Hello from Vix"); - }); - // Path params + return {status, payload} - app.get("/users/{id}", [](Request req, Response res) { - auto id = req.param("id"); - return std::pair{200, vix::json::o("id", id)}; - }); - // Plain text return (const char*) - app.get("/txt", [](const Request&, Response&) { - return "Hello world"; - }); - // Redirect - app.get("/go", [](Request req, Response res) { - res.redirect("https://vixcpp.com"); - }); - // Status only → auto message (like Express sendStatus) - app.get("/missing", [](Request req, Response res) { - res.status(404).send(); - }); - // JSON echo (body → json) - app.post("/echo", [](Request req, Response res) { - return vix::json::o("received", req.json()); - }); - - app.run(8080); -} -``` - ---- - -## Philosophy - -- Zero magic -- Zero runtime overhead -- Compile-time safety -- Expressive like FastAPI / Express -- Pure modern C++ - -Vix lets you write **business logic**, not plumbing. diff --git a/docs/examples/p2p-http.md b/docs/examples/p2p-http.md new file mode 100644 index 0000000..002f31b --- /dev/null +++ b/docs/examples/p2p-http.md @@ -0,0 +1,172 @@ +# p2p-http + +Architecture-first example for **Vix P2P** with an HTTP UI/API bridge. + +This example is meant to show how to expose a P2P runtime through a small HTTP surface: +- for debugging +- for admin tooling +- for a lightweight UI (served locally) + +It boots: +- a P2P node (`P2PRuntime`) +- an HTTP server (`App`) +- the built-in `p2p_http` routes under `/api/p2p` +- optional static UI pages (`index.html`, `connect.html`) + +## Architecture + +### Components + +1) **P2P runtime** +- Owns the node +- Manages peer connections and message/ping lifecycle +- Produces status/metrics/logs that can be bridged to HTTP + +2) **HTTP bridge** +- A thin adapter (`p2p_http`) that exposes P2P signals as HTTP routes +- Keeps your UI and tooling independent from the internal runtime + +3) **UI pages (optional)** +- Pure static HTML served locally by the HTTP server +- Talks to `/api/p2p/*` using `fetch()` + +### Data flow + +1) User opens `http://127.0.0.1:5178/` +2) UI calls: + - `GET /api/p2p/status` + - `GET /api/p2p/peers` +3) User clicks “connect” or “ping” +4) UI calls: + - `POST or GET /api/p2p/connect` (depending on your implementation) + - `GET /api/p2p/ping` +5) Runtime performs P2P work, then the bridge returns JSON back to the UI + +This is the pattern to keep: +- P2P logic stays in the runtime +- HTTP stays a thin control plane + +## Run + +```bash +vix run examples/p2p-ping.cpp +``` + +Then open: + +- `http://127.0.0.1:5178/` +- `http://127.0.0.1:5178/connect` + +Or test the API: + +```bash +curl -i http://127.0.0.1:5178/api/p2p/status +curl -i http://127.0.0.1:5178/api/p2p/peers +curl -i http://127.0.0.1:5178/api/p2p/ping +``` + +## Example + +### server.cpp + +```cpp +// vix run server.cpp +// http://127.0.0.1:5178/ + +#include +#include +#include +#include +#include + +using namespace vix; + +int main() +{ + App app; + + // 1) P2P node config + vix::p2p::NodeConfig cfg; + cfg.node_id = "A"; + cfg.listen_port = 9001; + + // 2) Boot runtime + auto node = vix::p2p::make_tcp_node(cfg); + vix::p2p::P2PRuntime runtime(node); + runtime.start(); + + // 3) Bridge runtime to HTTP routes + vix::p2p_http::P2PHttpOptions opt; + opt.prefix = "/api/p2p"; + opt.enable_ping = true; + opt.enable_status = true; + opt.enable_peers = true; + opt.enable_logs = true; + opt.enable_live_logs = true; + opt.stats_every_ms = 250; + + vix::p2p_http::registerRoutes(app, runtime, opt); + + // 4) Serve UI (optional) + app.static_dir("./public"); + + app.get("/", [](Request &, Response &res) + { + res.file("./public/index.html"); + }); + + app.get("/connect", [](Request &, Response &res) + { + res.file("./public/connect.html"); + }); + + // 5) Listen + app.listen(5178, []() + { + console.info("UI API listening on", 5178); + }); + + // 6) Wait + shutdown + app.wait(); + runtime.stop(); +} +``` + +## UI files (in repo) + +The HTML is intentionally kept out of this markdown because it is large. + +- `index.html`: + https://github.com/vixcpp/vix/blob/main/examples/p2p/public/index.html + +- `connect.html`: + https://github.com/vixcpp/vix/blob/main/examples/p2p/public/connect.html + +## Notes + +### Multi-node setup + +To run multiple nodes, change: +- `cfg.node_id` (example: `"B"`, `"C"`) +- `cfg.listen_port` (example: `9002`, `9003`) +- the HTTP port in `app.listen(...)` (example: `5179`, `5180`) + +Typical local layout: + +- Node A: P2P `9001`, HTTP `5178` +- Node B: P2P `9002`, HTTP `5179` +- Node C: P2P `9003`, HTTP `5180` + +### Endpoint names can differ + +The exact API paths depend on your `p2p_http` implementation. +If your build exposes different endpoints, keep the same structure but adjust the URLs in: +- the `curl` commands +- your UI `fetch()` calls + +### Security reminder + +If you expose this beyond localhost: +- protect `/api/p2p/*` with an auth middleware (API key, token, etc.) +- restrict dangerous operations (connect, kick, shutdown) to admin only + diff --git a/docs/examples/p2p-node.md b/docs/examples/p2p-node.md new file mode 100644 index 0000000..f7973ae --- /dev/null +++ b/docs/examples/p2p-node.md @@ -0,0 +1,128 @@ +# P2P Node + +This example shows how to start a minimal P2P node using the `vix::p2p` module. + +What you get: + +- TCP node (inbound + outbound) +- UDP discovery (LAN broadcast or multicast) +- Optional HTTP bootstrap (registry pull, optional announce) +- Secure handshake wiring (dev crypto by default) +- Ping/Pong heartbeats and stale peer cleanup + +--- + +## Requirements + +- C++20 +- Vix.cpp installed +- `modules/p2p` enabled + +--- + +## Minimal Example + +Create `p2p_node.cpp`: + +```cpp +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +static void log_line(std::string_view s) +{ + std::cerr << s << "\n"; +} + +int main(int argc, char** argv) +{ + using namespace vix::p2p; + + std::string nodeId = "node-A"; + std::uint16_t listenPort = 9001; + std::string bootstrapUrl; + + if (argc >= 2) nodeId = argv[1]; + if (argc >= 3) listenPort = static_cast(std::stoi(argv[2])); + if (argc >= 4) bootstrapUrl = argv[3]; + + NodeConfig cfg; + cfg.node_id = nodeId; + cfg.listen_port = listenPort; + cfg.max_peers = 64; + cfg.handshake_timeout_ms = 3000; + cfg.on_log = log_line; + + auto node = make_tcp_node(std::move(cfg)); + node->set_router(std::make_shared()); + node->set_crypto(std::make_shared()); + + if (!bootstrapUrl.empty()) + { + BootstrapConfig bcfg; + bcfg.self_node_id = nodeId; + bcfg.self_tcp_port = listenPort; + bcfg.registry_url = bootstrapUrl; + bcfg.mode = BootstrapMode::PullAndAnnounce; + + node->set_bootstrap(make_http_bootstrap(std::move(bcfg), nullptr)); + } + + auto runtime = std::make_shared(node); + runtime->start(); + + std::cerr << "[p2p] started node_id=" << nodeId + << " listen=" << listenPort << "\n"; + + runtime->wait(); + return 0; +} +``` + +--- + +## Run + +### Local LAN test + +Terminal 1: + +```bash +vix run p2p_node.cpp -- node-A 9001 +``` + +Terminal 2: + +```bash +vix run p2p_node.cpp -- node-B 9002 +``` + +Nodes discover each other via UDP broadcast. + +--- + +### With HTTP bootstrap registry + +If a registry is running on port 8089: + +```bash +vix run p2p_node.cpp -- node-A 9001 http://127.0.0.1:8089 +vix run p2p_node.cpp -- node-B 9002 http://127.0.0.1:8089 +``` + +--- + +## Notes + +- Default crypto is `NullCrypto` (development only). +- Transport is TCP. +- Ping/Pong is automatic. +- For production, replace crypto with a secure implementation. + diff --git a/docs/examples/p2p-sync.md b/docs/examples/p2p-sync.md new file mode 100644 index 0000000..5e13837 --- /dev/null +++ b/docs/examples/p2p-sync.md @@ -0,0 +1,385 @@ +# P2P Sync Integration (WAL + Outbox + HTTP Peers) + +This example shows how to combine: + +- A P2P node that can exchange messages over HTTP +- A WAL (write-ahead log) to guarantee durability +- An Outbox queue to retry delivery until an ack is received +- Idempotent apply on the receiver to ensure safe replays and duplicates + +Goal: local writes are accepted offline, persisted first, then propagated to peers when the network is available. + +## What you will build + +Two processes (Node A and Node B) that: + +1) Accept a local write: `PUT /kv/` with `{ "value": "..." }` +2) Append the operation to WAL before any side effects +3) Enqueue the operation into the outbox (pending replication) +4) Attempt delivery to each peer: `POST /p2p/ops` +5) Receiver applies the operation once (idempotent) and acks +6) Sender marks the outbox record as delivered once acked + +This is not a full CRDT. It is a minimal reliability pattern: durable local write + safe retry + idempotent apply. + +## Invariants you should keep in mind + +- Append before side effects: if the process crashes after appending, recovery can replay. +- At-least-once delivery: the sender may send the same op multiple times. +- Idempotent apply: receiver must treat duplicates safely. +- Durable acks: receiver acks only after the op is safely stored/applied. + +## API + +Local write: + +- `PUT /kv/` body: `{ "value": "" }` + +Replication endpoints: + +- `POST /p2p/ops` body: `{ "from": "", "ops": [ ... ] }` +- `POST /p2p/ack` body: `{ "from": "", "acked_ids": [ ... ] }` + +## Data model + +An operation is the unit replicated across peers: + +```cpp +struct Op +{ + std::string id; // unique op id (uuid) + std::string kind; // "put" + std::string key; + std::string value; + std::int64_t ts_ms; // timestamp (monotonic-ish) +}; +``` + +### Idempotency key + +The `id` is the idempotency key. +Receiver stores applied ids in a small persistent set (or a DB table) so replays are safe. + +## Single-file example (server.cpp) + +This is intentionally compact. +It demonstrates the flow and the responsibilities. +Adapt storage to your real DB. + +```cpp +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +using namespace vix; +using vix::json::Json; + +static std::int64_t now_ms() +{ + using namespace std::chrono; + return duration_cast(system_clock::now().time_since_epoch()).count(); +} + +struct Op +{ + std::string id; + std::string kind; + std::string key; + std::string value; + std::int64_t ts_ms{}; +}; + +// Minimal JSON helpers +static Json op_to_json(const Op& op) +{ + return Json::object({ + {"id", op.id}, + {"kind", op.kind}, + {"key", op.key}, + {"value", op.value}, + {"ts_ms", op.ts_ms}, + }); +} + +static Op op_from_json(const Json& j) +{ + Op op; + op.id = j.at("id").get(); + op.kind = j.at("kind").get(); + op.key = j.at("key").get(); + op.value = j.at("value").get(); + op.ts_ms = j.at("ts_ms").get(); + return op; +} + +// Very small "WAL" sketch. +// Replace with vix::sync::wal in your project. +struct Wal +{ + std::filesystem::path path; + + void append_line(const std::string& line) + { + std::ofstream f(path, std::ios::app); + f << line << "\n"; + } + + std::vector read_all_lines() const + { + std::vector out; + std::ifstream f(path); + std::string s; + while (std::getline(f, s)) out.push_back(s); + return out; + } +}; + +// Outbox record +struct OutboxRec +{ + std::string op_id; + std::string peer; // base url + bool delivered{false}; +}; + +// Minimal persistent sets (replace with DB) +struct Store +{ + std::unordered_map kv; + std::unordered_set applied_ids; // idempotency on receiver + std::vector outbox; +}; + +// Best effort HTTP POST JSON using Vix client (pseudo). +// Replace with your real http client in Vix. +static bool post_json(const std::string& url, const Json& body, Json* out_res) +{ + // Pseudocode placeholder: + // HttpClient c; + // auto r = c.post(url).json(body).send(); + // if (!r.ok()) return false; + // if (out_res) *out_res = r.json(); + // return true; + + (void)url; (void)body; (void)out_res; + return false; // replace with real implementation +} + +static void enqueue_to_all_peers(Store& st, const std::string& op_id, const std::vector& peers) +{ + for (const auto& p : peers) + st.outbox.push_back(OutboxRec{op_id, p, false}); +} + +static void try_flush_outbox(Store& st, const std::unordered_map& ops_by_id, const std::string& self_id) +{ + for (auto& rec : st.outbox) + { + if (rec.delivered) continue; + + auto it = ops_by_id.find(rec.op_id); + if (it == ops_by_id.end()) continue; + + const Op& op = it->second; + + Json req = Json::object({ + {"from", self_id}, + {"ops", Json::array({ op_to_json(op) })} + }); + + Json resp; + const bool ok = post_json(rec.peer + "/p2p/ops", req, &resp); + if (!ok) continue; + + // Expect receiver to respond with acked ids. + // Example: { "ok": true, "acked_ids": ["..."] } + if (resp.contains("acked_ids")) + { + for (const auto& x : resp["acked_ids"]) + { + if (x.get() == rec.op_id) + rec.delivered = true; + } + } + } +} + +static bool apply_op_idempotent(Store& st, const Op& op) +{ + if (st.applied_ids.find(op.id) != st.applied_ids.end()) + return false; // already applied + + if (op.kind == "put") + st.kv[op.key] = op.value; + + st.applied_ids.insert(op.id); + return true; +} + +int main() +{ + App app; + + // Config + const std::string self_id = "node-a"; + const std::vector peers = { + "http://127.0.0.1:8082", + }; + + // Storage + Store st; + Wal wal{ "./.vix/wal.log" }; + + // Map id -> op (in-memory index for demo) + std::unordered_map ops_by_id; + + // Recovery: replay WAL into state and rebuild ops index/outbox. + // In production: WAL replay reconstructs both state and outbox safely. + for (const auto& line : wal.read_all_lines()) + { + if (line.empty()) continue; + Json j = Json::parse(line); + Op op = op_from_json(j); + ops_by_id[op.id] = op; + apply_op_idempotent(st, op); + } + + // Local write endpoint + app.put("/kv/:key", [&](Request& req, Response& res) + { + const std::string key = req.param("key").str(); + Json body = Json::parse(req.body()); + + Op op; + op.id = vix::utils::uuid_v4(); + op.kind = "put"; + op.key = key; + op.value = body.value("value", ""); + op.ts_ms = now_ms(); + + // 1) WAL append first + wal.append_line(op_to_json(op).dump()); + + // 2) Apply locally (side effect) + apply_op_idempotent(st, op); + ops_by_id[op.id] = op; + + // 3) Outbox enqueue + enqueue_to_all_peers(st, op.id, peers); + + // 4) Best effort immediate flush + try_flush_outbox(st, ops_by_id, self_id); + + res.json({ + {"ok", true}, + {"op_id", op.id}, + {"key", key}, + {"value", op.value}, + }); + }); + + // Replication receive endpoint + app.post("/p2p/ops", [&](Request& req, Response& res) + { + Json body = Json::parse(req.body()); + std::vector acked; + + for (const auto& jop : body["ops"]) + { + Op op = op_from_json(jop); + + // Receiver durability rule: append to its WAL before apply + wal.append_line(op_to_json(op).dump()); + + (void)apply_op_idempotent(st, op); + ops_by_id[op.id] = op; + + acked.push_back(op.id); + } + + Json arr = Json::array(); + for (const auto& id : acked) arr.push_back(id); + + res.json({ + {"ok", true}, + {"acked_ids", arr}, + }); + }); + + // Read endpoint for demo + app.get("/kv/:key", [&](Request& req, Response& res) + { + const std::string key = req.param("key").str(); + auto it = st.kv.find(key); + if (it == st.kv.end()) + { + res.status(404).json({"ok", false, + "error", "not found" + }); + return; + } + res.json({"ok", true, "key", key, "value", it->second}); + }); + + app.run(8081); + return 0; +} +``` + +### Note about HTTP client + +The `post_json()` function is a placeholder. +Plug it into your real Vix HTTP client (or whatever transport you already have in your P2P module). +The important part is the protocol and the ordering: + +- Sender: WAL append -> local apply -> outbox enqueue -> retry +- Receiver: WAL append -> idempotent apply -> ack + +## Run two nodes locally + +Terminal A (Node A): + +```bash +vix run server.cpp -- -p 8081 +``` + +Terminal B (Node B): + +- copy the same file +- change `self_id`, port, and peers accordingly + +Example: + +- Node A on 8081 peers to 8082 +- Node B on 8082 peers to 8081 + +Then test: + +```bash +curl -X PUT http://127.0.0.1:8081/kv/name -d '{ "value": "Alice" }' +curl http://127.0.0.1:8082/kv/name +``` + +If the network is down, Node A still accepts the write. +When connectivity returns, outbox retries deliver it. + +## Next step + +If you want deterministic convergence, you will extend this into one of: + +- last-write-wins with a stable tie-breaker (node id + op id) +- CRDT register/map +- operation log with causal metadata and conflict resolution + +But the reliability core stays the same: WAL + outbox + idempotent apply. + diff --git a/docs/examples/post_create_user.md b/docs/examples/post_create_user.md deleted file mode 100644 index d9c633f..0000000 --- a/docs/examples/post_create_user.md +++ /dev/null @@ -1,43 +0,0 @@ -# Example — post_create_user.cpp - -```cpp -#include -#include -#include - -using namespace vix; -namespace J = vix::json; - -int main() -{ - App app; - - // POST /users - app.post("/users", [](Request &req, Response &res) - { - try { - auto body = json::Json::parse(req.body()); - - const std::string name = body.value("name", ""); - const std::string email = body.value("email", ""); - const int age = body.value("age", 0); - - res.status(200).json({ - "action", "create", - "status", "created", - "user", J::obj({ - "name", name, - "email", email, - "age", static_cast(age) - }) - }); - } - catch (...) { - res.status(400).json({ - "error", "Invalid JSON" - }); - } }); - - app.run(8080); -} -``` diff --git a/docs/examples/presence.md b/docs/examples/presence.md new file mode 100644 index 0000000..be1189a --- /dev/null +++ b/docs/examples/presence.md @@ -0,0 +1,172 @@ + +# Presence (Online / Offline Users) + +This example shows how to implement **presence tracking** in Vix.cpp. + +Presence means: + +- Knowing when a user connects +- Knowing when a user disconnects +- Broadcasting online / offline events +- Optionally tracking a simple in-memory user list + +This example is beginner-friendly and uses only: + +- WebSocket +- A simple std::unordered_set +- broadcast_json() + +## 1) Minimal Presence Server (Single File) + +main.cpp + +```cpp +#include +#include +#include + +using namespace vix; + +int main() +{ + static std::unordered_set online_users; + + vix::serve_http_and_ws([](App& app, auto& ws) + { + app.get("/", [](Request&, Response& res) + { + res.json({ + "message", "Presence example running", + "hint", "Connect via WebSocket and send chat.join" + }); + }); + + ws.on_open([&ws](auto&) + { + ws.broadcast_json("presence.system", { + "message", "A new connection opened" + }); + }); + + ws.on_typed_message([&ws](auto&, const std::string& type, + const vix::json::kvs& payload) + { + if (type == "presence.join") + { + std::string user; + + for (size_t i = 0; i + 1 < payload.flat.size(); i += 2) + { + if (std::get(payload.flat[i].v) == "user") + { + user = std::get(payload.flat[i+1].v); + } + } + + if (!user.empty()) + { + online_users.insert(user); + + ws.broadcast_json("presence.online", { + "user", user + }); + } + } + + if (type == "presence.leave") + { + std::string user; + + for (size_t i = 0; i + 1 < payload.flat.size(); i += 2) + { + if (std::get(payload.flat[i].v) == "user") + { + user = std::get(payload.flat[i+1].v); + } + } + + if (!user.empty()) + { + online_users.erase(user); + + ws.broadcast_json("presence.offline", { + "user", user + }); + } + } + + if (type == "presence.list") + { + std::vector arr; + + for (const auto& u : online_users) + { + arr.emplace_back(u); + } + + ws.broadcast_json("presence.current", { + "users", vix::json::array(std::move(arr)) + }); + } + }); + }); + + return 0; +} +``` + +## 2) How to Test (Step by Step) + +### 1) Build and run: +```bash + vix run main.cpp +``` +### 2) Open two terminals. + +### 3) Connect both with websocat: +```bash + websocat ws://127.0.0.1:9090/ +``` +### 4) In terminal A: +```bash + {"type":"presence.join","payload":{"user":"Alice"}} +``` +### 5) In terminal B: +```bash + {"type":"presence.join","payload":{"user":"Bob"}} +``` +Both terminals will receive: +```bash + presence.online +``` +### 6) Request current list: +```bash + {"type":"presence.list","payload":{}} +``` +You will receive: +```bash + presence.current +``` +### 7) Leave: +```bash + {"type":"presence.leave","payload":{"user":"Alice"}} +``` +## 3) What This Teaches + +- How to react to connection events +- How to track state in memory +- How to broadcast presence changes +- How to implement basic real-time features + +## 4) Production Notes + +For real systems: + +- Store presence in Redis or a distributed cache +- Use heartbeat timeouts to detect disconnects +- Clean users on connection close (ws.on_close) +- Avoid trusting user-provided names without authentication + +Presence is one of the most common real-time patterns: +chat apps, dashboards, multiplayer games, admin panels. + diff --git a/docs/examples/production-api-groups.md b/docs/examples/production-api-groups.md new file mode 100644 index 0000000..54fc7c4 --- /dev/null +++ b/docs/examples/production-api-groups.md @@ -0,0 +1,354 @@ +# Production API Architecture with Groups + +This guide shows a production-ready way to structure a Vix.cpp API using `group()` and `Group` middleware. +Goal: clean routing, consistent security, and predictable behavior under load. + +## What Groups give you + +A `Group` is a scoped router with: + +- a base prefix (example: `/api/v1`) +- scoped middleware via `group.use(...)` +- nested groups (example: `/api/v1/admin`) +- optional `protect()` for a sub-scope (example: protect only `/secure` inside `/api`) + +Think of `group()` as the API layout, and `use()` as the policy for that layout. + +## The production layout (recommended) + +Use this structure: + +- `/health` public health check +- `/api/v1` public + authenticated APIs +- `/api/v1/internal` internal endpoints (tight IP allowlist) +- `/api/v1/admin` admin endpoints (JWT + RBAC) +- `/api/v1/uploads` uploads (multipart + strict body limit) +- `/docs` optional docs routes (if you ship offline docs) + +Recommended versioning: `/api/v1` at the root, not in headers. +It keeps routing explicit and avoids client confusion. + +## Middleware order (the rule) + +Order matters. For a typical API scope, apply in this order: + +1) Security headers +2) CORS (if browser clients exist) +3) Body limit (reject early) +4) IP filter (reject early) +5) Rate limit (protect resources) +6) Auth (API key or JWT) +7) Authorization (RBAC) +8) Business routes + +Reason: reject bad requests as early as possible, before costly work. + +## Keys and identity + +Production key sources are usually: + +- IP: via `X-Forwarded-For` set by your reverse proxy +- API key: via `x-api-key` header +- JWT: via `Authorization: Bearer ` +- Session: cookie based for browser apps + +If you use `X-Forwarded-For`, do not trust client-provided values unless they come from a trusted proxy. +In local testing, you can set headers manually with curl. In production, your load balancer should set them. + +## Pattern 1: API root group with shared hardening + +```cpp +auto api = app.group("/api/v1"); + +api.use(middleware::app::security_headers_dev()); // baseline headers +api.use(middleware::app::cors_dev({"https://example.com"})); // if you serve browsers +api.use(middleware::app::body_limit_write_dev(64 * 1024)); // 64KB for writes +api.use(middleware::app::rate_limit_dev(120, std::chrono::minutes(1))); // 120/min +``` + +Notes: +- `body_limit_write_dev()` applies only to POST/PUT/PATCH in your presets. +- If you need strict chunked behavior, use `body_limit_dev(max, apply_to_get, allow_chunked)`. + +## Pattern 2: Public vs secure sub-scopes inside the same group + +Keep public routes inside the group, then protect a sub-prefix: + +```cpp +api.get("/public", ...); + +// Only secure scope needs API key +api.protect("/secure", middleware::app::api_key_dev("secret")); + +api.get("/secure/me", ...); +``` + +This keeps your API surface obvious: `/public` stays public, `/secure/*` is protected. + +## Pattern 3: Nested admin group with JWT + RBAC + +For admin endpoints, do: + +- JWT to authenticate +- RBAC to require role and optionally permissions + +```cpp +api.group("/admin", [&](App::Group& admin){ + admin.use(middleware::app::jwt_auth("dev_secret")); + admin.use(middleware::app::rbac_admin()); // requires role=admin + + admin.get("/dashboard", ...); +}); +``` + +If you need granular permissions, chain `require_perm("x:y")` after RBAC context. + +## Pattern 4: Internal endpoints (IP allowlist + optional API key) + +Internal is for metrics, maintenance, control-plane calls. + +Use IP allowlist first. Optionally add API key. + +```cpp +api.group("/internal", [&](App::Group& in){ + in.use(middleware::app::ip_allowlist_dev("x-forwarded-for", {"10.0.0.1", "127.0.0.1"})); + in.use(middleware::app::api_key_dev("internal_key")); + + in.get("/status", ...); +}); +``` + +## Pattern 5: Upload scope (multipart + strict limits) + +Uploads are where you must be strict: + +- set body limits +- set multipart limit and upload dir +- consider rate limiting separately for uploads + +```cpp +api.group("/uploads", [&](App::Group& up){ + up.use(middleware::app::body_limit_dev(5 * 1024 * 1024, false, false)); // 5MB, no chunked + up.use(middleware::app::multipart_save_dev("uploads", 5 * 1024 * 1024)); + + up.post("/image", ...); +}); +``` + +## Pattern 6: Response caching for GET APIs + +Cache only GET routes, under a prefix, with explicit bypass header. + +```cpp +api.use(middleware::app::http_cache({ + .ttl_ms = 30'000, + .allow_bypass = true, + .bypass_header = "x-vix-cache", + .bypass_value = "bypass", + .vary_headers = {"accept-language"}, + .add_debug_header = true, + .debug_header = "x-vix-cache-status", +})); +``` + +Use caching for read-heavy endpoints, not for user-specific endpoints unless you vary by user identity. + +## Pattern 7: Compression + +Compression is best applied globally, but keep a sane `min_size`. +It should also add `Vary: Accept-Encoding`. + +```cpp +app.use(vix::middleware::app::adapt_ctx( + vix::middleware::performance::compression({ + .min_size = 512, + .add_vary = true, + .enabled = true, + }) +)); +``` + +## Pattern 8: Sessions vs JWT in production + +Use JWT when: +- you have stateless API clients (mobile, CLI, services) +- you want horizontal scaling without shared session storage +- you want explicit RBAC claims in token + +Use sessions when: +- you have browser apps +- you need CSRF protection + cookie-based auth +- you want easy server-side invalidation (rotate secret or store session ids) + +Common production model: +- browser: session cookie + CSRF +- external API: JWT +- internal tools: API key + IP allowlist + +## Observability checklist + +At minimum: + +- Set and return a request id (header: `x-request-id`) +- Log: method, path, status, latency, client key +- Add debug headers only in dev (cache status, rate limit remaining, etc.) + +If your Logger supports request context, set it at the beginning of a request middleware. + +## Complete example (copy-paste) + +Save as: `production_groups_api.cpp` + +```cpp +/** + * + * @file production_groups_api.cpp + * @author Gaspard Kirira + * + * Vix.cpp - Production API Architecture with Groups + * + */ +#include +#include +#include +#include + +#include +#include +#include + +using namespace vix; + +static void register_health(App& app) +{ + app.get("/health", [](Request&, Response& res){ + res.json({ "ok", true, "status", "up" }); + }); +} + +int main() +{ + App app; + + // Global compression (optional) + // app.use(vix::middleware::app::adapt_ctx( + // vix::middleware::performance::compression({ .min_size = 512, .add_vary = true, .enabled = true }) + // )); + + register_health(app); + + // API root: /api/v1 + auto api = app.group("/api/v1"); + + // Baseline hardening on all API responses + api.use(middleware::app::security_headers_dev(false)); // HSTS off in dev + api.use(middleware::app::cors_dev({ + "http://localhost:5173", + "http://127.0.0.1:5173", + "https://example.com" + })); + + // Early rejection for write methods + api.use(middleware::app::body_limit_write_dev(64 * 1024)); // 64KB for writes + + // Rate limiting per client key + api.use(middleware::app::rate_limit_dev(120, std::chrono::minutes(1))); + + // Cache only GET endpoints under /api/v1/public-data + api.group("/public-data", [&](App::Group& pub){ + pub.use(middleware::app::http_cache({ + .ttl_ms = 10'000, + .allow_bypass = true, + .bypass_header = "x-vix-cache", + .bypass_value = "bypass", + .vary_headers = {"accept-language"}, + .add_debug_header = true, + .debug_header = "x-vix-cache-status", + })); + + pub.get("/users", [](Request& req, Response& res){ + const std::string lang = req.has_header("accept-language") ? req.header("accept-language") : "none"; + res.json(vix::json::obj({ + "ok", true, + "message", "users from origin", + "accept_language", lang + })); + }); + }); + + // Public API + api.get("/public", [](Request&, Response& res){ + res.json({ "ok", true, "scope", "public" }); + }); + + // Secure scope: API key + api.protect("/secure", middleware::app::api_key_dev("secret")); + + api.get("/secure/whoami", [](Request& req, Response& res){ + auto &k = req.state(); + res.json({ "ok", true, "scope", "secure", "api_key", k.value }); + }); + + // Admin scope: JWT + RBAC(role=admin) + api.group("/admin", [&](App::Group& admin){ + admin.use(middleware::app::jwt_auth("dev_secret")); + admin.use(middleware::app::rbac_admin()); + + admin.get("/dashboard", [](Request& req, Response& res){ + auto &authz = req.state(); + res.json({ "ok", true, "scope", "admin", "sub", authz.subject }); + }); + }); + + // Internal scope: IP allowlist + API key + api.group("/internal", [&](App::Group& in){ + in.use(middleware::app::ip_allowlist_dev("x-forwarded-for", {"127.0.0.1", "10.0.0.1"})); + in.use(middleware::app::api_key_dev("internal_key")); + + in.get("/status", [](Request&, Response& res){ + res.json({ "ok", true, "scope", "internal", "status", "green" }); + }); + }); + + std::cout + << "Running:\n" + << " http://localhost:8080/health\n" + << " http://localhost:8080/api/v1/public\n" + << " http://localhost:8080/api/v1/public-data/users\n" + << " http://localhost:8080/api/v1/secure/whoami\n" + << " http://localhost:8080/api/v1/admin/dashboard\n" + << " http://localhost:8080/api/v1/internal/status\n\n" + << "API key:\n" + << " secret\n" + << "Internal key:\n" + << " internal_key\n\n" + << "Try:\n" + << " curl -i http://localhost:8080/health\n" + << " curl -i http://localhost:8080/api/v1/public\n" + << " curl -i http://localhost:8080/api/v1/public-data/users\n" + << " curl -i -H \"x-vix-cache: bypass\" http://localhost:8080/api/v1/public-data/users\n" + << " curl -i http://localhost:8080/api/v1/secure/whoami\n" + << " curl -i -H \"x-api-key: secret\" http://localhost:8080/api/v1/secure/whoami\n" + << " curl -i http://localhost:8080/api/v1/admin/dashboard\n" + << " curl -i -H \"x-api-key: internal_key\" -H \"X-Forwarded-For: 127.0.0.1\" http://localhost:8080/api/v1/internal/status\n"; + + app.run(8080); + return 0; +} +``` + +## Run + +```bash +vix run production_groups_api.cpp +``` + +## Next step + +If you want to push this to a real deployment, the next upgrade is: +- strict trust of proxy headers (trusted proxies list) +- real JWT exp verification +- CSRF + session mode for browser apps +- structured logs + request id middleware +- per-scope rate limits (uploads lower, public higher) + diff --git a/docs/examples/put_update_user.md b/docs/examples/put_update_user.md deleted file mode 100644 index 06c2a7d..0000000 --- a/docs/examples/put_update_user.md +++ /dev/null @@ -1,46 +0,0 @@ -# Example — put_update_user.cpp - -```cpp -#include -#include -#include - -using namespace vix; -namespace J = vix::json; - -int main() -{ - App app; - - // PUT /users/{id} - app.put("/users/{id}", [](Request &req, Response &res) - { - const std::string id = req.param("id"); - - try { - auto body = json::Json::parse(req.body()); - - const std::string name = body.value("name", ""); - const std::string email = body.value("email", ""); - const int age = body.value("age", 0); - - res.json({ - "action", "update", - "status", "updated", - "user", J::obj({ - "id", id, - "name", name, - "email", email, - "age", static_cast(age) - }) - }); - } - catch (...) { - res.status(400).json({ - "error", "Invalid JSON" - }); - } }); - - app.run(8080); -} -``` diff --git a/docs/examples/querybuilder-update.md b/docs/examples/querybuilder-update.md new file mode 100644 index 0000000..f6988e1 --- /dev/null +++ b/docs/examples/querybuilder-update.md @@ -0,0 +1,190 @@ +# ORM Example Guide: QueryBuilder Update + +This guide explains the `querybuilder_update` example step by step. + +Goal: - Build an UPDATE query dynamically - Bind parameters safely - +Execute using a pooled connection - Keep SQL explicit and predictable + +This example demonstrates how `QueryBuilder` helps you construct queries +while still keeping full SQL control. + +# 1. What is QueryBuilder? + +`QueryBuilder` is a lightweight helper that: + +- Stores a SQL string +- Stores bound parameters +- Keeps SQL and parameters organized +- Avoids string concatenation mistakes + +It does NOT: - Abstract SQL away - Generate complex schema logic - +Replace repositories + +It is a structured way to manage dynamic SQL. + +# 2. Full Example Code + +``` cpp +#include + +#include +#include +#include + +using namespace vix::orm; + +int main(int argc, char **argv) +{ + const std::string host = (argc > 1 ? argv[1] : "tcp://127.0.0.1:3306"); + const std::string user = (argc > 2 ? argv[2] : "root"); + const std::string pass = (argc > 3 ? argv[3] : ""); + const std::string db = (argc > 4 ? argv[4] : "vixdb"); + + try + { + auto factory = make_mysql_factory(host, user, pass, db); + + PoolConfig cfg; + cfg.min = 1; + cfg.max = 8; + + ConnectionPool pool{factory, cfg}; + pool.warmup(); + + QueryBuilder qb; + qb.raw("UPDATE users SET age=? WHERE email=?") + .param(29) + .param(std::string("gaspardkirira@example.com")); + + PooledConn pc(pool); + auto st = pc.get().prepare(qb.sql()); + + const auto &ps = qb.params(); + for (std::size_t i = 0; i < ps.size(); ++i) + st->bind(i + 1, any_to_dbvalue_or_throw(ps[i])); + + const auto affected = st->exec(); + std::cout << "[OK] affected rows = " << affected << "\n"; + return 0; + } + catch (const DBError &e) + { + std::cerr << "[DBError] " << e.what() << "\n"; + return 1; + } + catch (const std::exception &e) + { + std::cerr << "[ERR] " << e.what() << "\n"; + return 1; + } +} +``` + +# 3. Step-by-Step Explanation + +## 3.1 Build the SQL + +``` cpp +qb.raw("UPDATE users SET age=? WHERE email=?") +``` + +- `raw()` sets the SQL string. +- You still control the full SQL syntax. +- This keeps the design explicit. + +## 3.2 Add parameters + +``` cpp +.param(29) +.param(std::string("gaspardkirira@example.com")); +``` + +Each `.param()`: + +- Appends a value to an internal parameter list +- Maintains correct binding order +- Avoids manual index tracking + +Internally, QueryBuilder stores parameters in a vector. + +## 3.3 Prepare the statement + +``` cpp +auto st = pc.get().prepare(qb.sql()); +``` + +`qb.sql()` returns the final SQL string. + +## 3.4 Bind parameters + +``` cpp +const auto &ps = qb.params(); +for (std::size_t i = 0; i < ps.size(); ++i) + st->bind(i + 1, any_to_dbvalue_or_throw(ps[i])); +``` + +Important rules: + +- Binding starts at index 1 +- Parameters must match `?` placeholders +- `any_to_dbvalue_or_throw` converts std::any safely + +## 3.5 Execute + +``` cpp +const auto affected = st->exec(); +``` + +- Returns number of affected rows +- For UPDATE, usually 0 or 1 (or more) + +# 4. When to Use QueryBuilder + +Use QueryBuilder when: + +- SQL is dynamic +- Filters are optional +- You build WHERE clauses conditionally +- You want clean parameter handling + +Example pattern: + +``` cpp +QueryBuilder qb; +qb.raw("UPDATE users SET age=? WHERE 1=1") + .param(newAge); + +if (hasEmail) +{ + qb.raw(" AND email=?").param(email); +} +``` + +# 5. Production Advice + +- Keep SQL readable +- Avoid over-dynamic query generation +- Log qb.sql() for debugging +- Validate user input before binding +- Use transactions if multiple updates must be atomic + +# 6. Difference vs Repository + +Repository: - Fixed CRUD structure - Ideal for standard entity +operations + +QueryBuilder: - Flexible - Manual control over SQL - Useful for complex +or custom queries + +They complement each other. + +# Summary + +You learned: + +- How to use QueryBuilder for updates +- How parameters are managed safely +- How pooled connections execute queries +- When to use QueryBuilder vs Repository + + diff --git a/docs/examples/rate-limit.md b/docs/examples/rate-limit.md new file mode 100644 index 0000000..2619678 --- /dev/null +++ b/docs/examples/rate-limit.md @@ -0,0 +1,224 @@ +# Rate Limiting (Beginner Guide) + +Welcome 👋\ +This page teaches you **rate limiting** in Vix.cpp with **very small +examples**. + +Rate limiting helps you protect your API against: + +- spam and brute-force attacks +- abusive clients +- accidental traffic bursts +- expensive endpoints being called too often + +## What is rate limiting? + +Think of a bucket of tokens: + +- The bucket has a maximum size = **capacity** (burst) +- Every request consumes **1 token** +- Tokens can refill over time = **refill_per_sec** +- If the bucket is empty, the server returns **429 Too Many Requests** + +Vix middleware implements this idea for you. + +# 1) Minimal rate limit on `/api` + +This is the smallest server: only `/api/*` is rate limited. + +``` cpp +#include +#include + +using namespace vix; + +int main() +{ + App app; + + // capacity=5, refill=0 => the 6th request gets 429 (easy demo) + app.use("/api", middleware::app::rate_limit_custom_dev(5.0, 0.0)); + + app.get("/", [](Request &, Response &res) { + res.send("public route"); + }); + + app.get("/api/ping", [](Request &req, Response &res) { + res.json({ + "ok", true, + "msg", "pong", + "xff", req.header("x-forwarded-for") + }); + }); + + app.run(8080); +} +``` + +### Test with curl + +Run the server: + +``` bash +vix run rate_limit_server.cpp +``` + +Call it 6 times: + +``` bash +for i in $(seq 1 6); do + echo "---- $i" + curl -i http://localhost:8080/api/ping +done +``` + +Expected: + +- first 5 requests: **200** +- request 6: **429 Too Many Requests** + +# 2) Production-like preset: `rate_limit_dev(capacity, window)` + +If you want a simpler mental model: + +- **capacity** = how many requests you allow +- **window** = time window + +Example: 60 requests per minute. + +``` cpp +#include +#include +#include + +using namespace vix; + +int main() +{ + App app; + + // 60 requests per minute per client key (default key_header: x-forwarded-for) + app.use("/api", middleware::app::rate_limit_dev( + 60, + std::chrono::minutes(1) + )); + + app.get("/api/ping", [](Request &, Response &res) { + res.json({"ok", true, "msg", "pong"}); + }); + + app.run(8080); +} +``` + +### Important + +By default the limiter uses **x-forwarded-for** as client key. + +In dev you can simulate client IPs by sending that header: + +``` bash +curl -i http://localhost:8080/api/ping -H "X-Forwarded-For: 9.9.9.9" +``` + +# 3) Custom key: limit by your own header + +Sometimes you want to limit by: + +- user id +- API key +- session id +- tenant id + +You can pass a header name as key: + +``` cpp +#include +#include + +using namespace vix; + +int main() +{ + App app; + + // limit per "x-user-id" + app.use("/api", middleware::app::rate_limit_custom_dev( + 10.0, // capacity + 1.0, // refill per second + "x-user-id" + )); + + app.get("/api/ping", [](Request &req, Response &res) { + res.json({ + "ok", true, + "user", req.header("x-user-id") + }); + }); + + app.run(8080); +} +``` + +Test: + +``` bash +curl -i http://localhost:8080/api/ping -H "x-user-id: alice" +curl -i http://localhost:8080/api/ping -H "x-user-id: bob" +``` + +Each user has their own bucket. + +# 4) Real production pattern: CORS + IP filter + rate limit (order matters) + +This is your combined example (short explanation): + +- CORS runs first (blocks bad origins) +- IP filter runs second (blocks denied IPs) +- Rate limit runs third (protects the allowed traffic) + +``` cpp +app.use("/api", middleware::app::cors_ip_demo()); +app.use("/api", middleware::app::ip_filter_dev("x-vix-ip", {"1.2.3.4"})); +app.use("/api", middleware::app::rate_limit_custom_dev( + 5.0, 0.0, "x-vix-ip" +)); +``` + +### Why explicit OPTIONS routes? + +Browsers send a **preflight** request (OPTIONS).\ +If OPTIONS is auto-handled before middleware runs, you may miss CORS +headers. + +So you add: + +``` cpp +app.options("/api/ping", [](Request&, Response& res){ + res.status(204).send(); +}); +``` + +# Common beginner mistakes + +1) Forgetting that rate limiting needs a stable key\ + If your key header changes on every request, the limiter becomes + useless. + +2) Using `X-Forwarded-For` without a reverse proxy\ + In real deployments, your reverse proxy (nginx, cloud LB) sets this + header. In local tests, you can set it manually with curl. + +3) Putting rate limit before IP filter (or auth)\ + Usually you want to reject denied requests early, then rate limit + what is allowed. + +# Summary + +- Use `rate_limit_custom_dev(capacity, refill_per_sec, key_header)` + when you want full control. +- Use `rate_limit_dev(capacity, window)` for the simple "N requests + per time window" model. +- Always think about the **key** (IP, user id, api key, etc.). + + diff --git a/docs/examples/rbac-session.md b/docs/examples/rbac-session.md new file mode 100644 index 0000000..bcfeb36 --- /dev/null +++ b/docs/examples/rbac-session.md @@ -0,0 +1,369 @@ +# RBAC + Session (Beginner Guide) + +This guide shows how to combine **Sessions** (cookie-based login state) with **RBAC** (roles + permissions). + +Goal: +- A user "logs in" and we store their identity and authorization in the **session**. +- Protected routes build an `Authz` object from the session. +- Then we enforce: + - `require_role("admin")` + - `require_perm("products:write")` + +You will get: +- `GET /` (help) +- `POST /login/admin` (creates a session with admin + products:write) +- `POST /login/user` (creates a session with user only) +- `POST /logout` (destroys the session) +- `GET /me` (shows session values) +- `GET /admin` (requires role + perm) + +--- + +## 1) Run + +```bash +vix run rbac_session_app.cpp +``` + +--- + +## 2) Quick tests (curl) + +### 2.1 Public help +```bash +curl -i http://localhost:8080/ +``` + +### 2.2 Login as admin (creates cookie) +Save cookies to a file so next requests reuse the session: + +```bash +curl -i -c jar.txt -X POST http://localhost:8080/login/admin +``` + +### 2.3 Access protected route as admin (OK) +```bash +curl -i -b jar.txt http://localhost:8080/admin +``` + +### 2.4 Login as normal user (no permission) +```bash +curl -i -c jar.txt -X POST http://localhost:8080/login/user +curl -i -b jar.txt http://localhost:8080/admin +``` + +Expected: **403** (missing permission). + +### 2.5 Inspect current session +```bash +curl -i -b jar.txt http://localhost:8080/me +``` + +### 2.6 Logout +```bash +curl -i -b jar.txt -X POST http://localhost:8080/logout +curl -i -b jar.txt http://localhost:8080/admin +``` + +Expected: **401** (not authenticated). + +--- + +## How it works (simple mental model) + +- **Session middleware** reads a cookie (example: `sid=...`) and loads a small key/value store. +- We store: + - `sub` (subject / user id) + - `roles` (comma-separated, ex: `admin,user`) + - `perms` (comma-separated, ex: `products:write,orders:read`) +- A small middleware `mw_authz_from_session()` creates an `Authz` object and puts it into request state. +- Then we reuse the existing RBAC middlewares: + - `require_role("admin")` + - `require_perm("products:write")` + +--- + +## Full example (copy/paste) + +Save as `rbac_session_app.cpp`: + +```cpp +/** + * + * @file rbac_session_app.cpp - RBAC + Session combined (Vix.cpp) + * @author Gaspard Kirira + * + * Vix.cpp + * + * Run: + * vix run rbac_session_app.cpp + * + * Tests: + * curl -i http://localhost:8080/ + * curl -i -c jar.txt -X POST http://localhost:8080/login/admin + * curl -i -b jar.txt http://localhost:8080/admin + * curl -i -c jar.txt -X POST http://localhost:8080/login/user + * curl -i -b jar.txt http://localhost:8080/admin + * curl -i -b jar.txt http://localhost:8080/me + * curl -i -b jar.txt -X POST http://localhost:8080/logout + * + */ + +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +using namespace vix; +namespace J = vix::json; + +// ----------------------- tiny helpers (no heavy magic) ------------------------ + +static std::vector split_csv(const std::string &s) +{ + std::vector out; + std::string cur; + + for (char c : s) + { + if (c == ',') + { + if (!cur.empty()) + out.push_back(cur); + cur.clear(); + continue; + } + // trim simple spaces + if (c != ' ' && c != '\t' && c != '\n' && c != '\r') + cur.push_back(c); + } + + if (!cur.empty()) + out.push_back(cur); + + // remove empty items + out.erase(std::remove_if(out.begin(), out.end(), + [](const std::string &x) + { return x.empty(); }), + out.end()); + return out; +} + +static J::kvs ok(std::string_view msg) +{ + return J::obj({"ok", true, "message", std::string(msg)}); +} + +static J::kvs err(std::string_view code, int status, std::string_view hint = {}) +{ + auto o = J::obj({ + "ok", false, + "error", std::string(code), + "status", (long long)status, + }); + + if (!hint.empty()) + { + // append fields by rebuilding (Simple.hpp style) + return J::obj({ + "ok", false, + "error", std::string(code), + "status", (long long)status, + "hint", std::string(hint), + }); + } + + return o; +} + +// --------------------- middleware: Authz from Session ------------------------- +// Reads session keys and populates vix::middleware::auth::Authz for RBAC checks. + +static vix::middleware::MiddlewareFn mw_authz_from_session(bool require_auth = true) +{ + using vix::middleware::auth::Authz; + using vix::middleware::auth::Session; + + return [require_auth](vix::middleware::Context &ctx, vix::middleware::Next next) + { + auto *s = ctx.req().try_state(); + if (!s) + { + // Session middleware missing + ctx.res().status(500).json(err("session_missing", 500, + "Install session middleware before RBAC")); + return; + } + + const auto sub = s->get("sub"); + const auto roles = s->get("roles"); + const auto perms = s->get("perms"); + + if (require_auth && (!sub || sub->empty())) + { + ctx.res().status(401).json(err("unauthorized", 401, + "Login first to create a session")); + return; + } + + Authz a; + a.subject = sub.value_or(""); + + if (roles) + for (auto &r : split_csv(*roles)) + a.roles.insert(std::move(r)); + + if (perms) + for (auto &p : split_csv(*perms)) + a.perms.insert(std::move(p)); + + ctx.req().emplace_state(std::move(a)); + next(); + }; +} + +// ------------------------------ routes ---------------------------------------- + +static void register_routes(App &app) +{ + // Home: explains how to test quickly + app.get("/", [](Request &, Response &res) + { res.text( + "RBAC + Session demo\n\n" + "POST /login/admin (sets session: role=admin, perm=products:write)\n" + "POST /login/user (sets session: role=user, no products:write)\n" + "POST /logout (destroy session)\n" + "GET /me (shows current session)\n" + "GET /admin (requires role=admin + perm=products:write)\n\n" + "Quick start:\n" + " curl -i -c jar.txt -X POST http://localhost:8080/login/admin\n" + " curl -i -b jar.txt http://localhost:8080/admin\n" + " curl -i -b jar.txt http://localhost:8080/me\n"); }); + + // Create an admin session + app.post("/login/admin", [](Request &req, Response &res) + { + auto *s = req.try_state(); + if (!s) + { + res.status(500).json(err("session_missing", 500)); + return; + } + + s->set("sub", "user123"); + s->set("roles", "admin"); + s->set("perms", "products:write,orders:read"); + + res.json(ok("logged_in_as_admin")); }); + + // Create a normal user session (no products:write) + app.post("/login/user", [](Request &req, Response &res) + { + auto *s = req.try_state(); + if (!s) + { + res.status(500).json(err("session_missing", 500)); + return; + } + + s->set("sub", "user123"); + s->set("roles", "user"); + s->set("perms", "orders:read"); + + res.json(ok("logged_in_as_user")); }); + + // Destroy session + app.post("/logout", [](Request &req, Response &res) + { + auto *s = req.try_state(); + if (!s) + { + res.status(500).json(err("session_missing", 500)); + return; + } + + s->destroy(); + res.json(ok("logged_out")); }); + + // Inspect session values + app.get("/me", [](Request &req, Response &res) + { + auto *s = req.try_state(); + if (!s) + { + res.status(500).json(err("session_missing", 500)); + return; + } + + res.json(J::obj({ + "ok", true, + "sid", s->id, + "is_new", s->is_new, + "sub", s->get("sub").value_or(""), + "roles", s->get("roles").value_or(""), + "perms", s->get("perms").value_or(""), + })); }); + + // Protected route: requires role + perm + app.get("/admin", [](Request &req, Response &res) + { + auto &a = req.state(); + + res.json(J::obj({ + "ok", true, + "protected", true, + "sub", a.subject, + "has_admin", a.has_role("admin"), + "has_products_write", a.has_perm("products:write"), + })); }); +} + +// ------------------------------ install --------------------------------------- + +static void install_middlewares(App &app) +{ + using namespace vix::middleware::app; + using vix::middleware::auth::require_perm; + using vix::middleware::auth::require_role; + + // 1) Session (global) - cookie-based state + // Use the preset (dev-friendly). Cookie name defaults to "sid". + app.use(session_dev("dev_session_secret")); + + // 2) RBAC protection for /admin + // Build Authz from session, then enforce rules. + app.use("/admin", chain( + adapt_ctx(mw_authz_from_session(true)), + adapt_ctx(require_role("admin")), + adapt_ctx(require_perm("products:write")))); +} + +int main() +{ + App app; + + install_middlewares(app); + register_routes(app); + + app.run(8080); + return 0; +} +``` + +--- + +## Notes for real projects + +- Do not store sensitive data in session as plain text unless it is protected (signed + secure cookie, HTTPS). +- Behind HTTPS, set `secure=true` and consider `SameSite` based on your frontend setup. +- In production, roles/perms are usually loaded from a database, not hard-coded in `/login/*`. + + diff --git a/docs/examples/rbac.md b/docs/examples/rbac.md new file mode 100644 index 0000000..b0fa040 --- /dev/null +++ b/docs/examples/rbac.md @@ -0,0 +1,272 @@ +# RBAC Middleware Guide + +## What is RBAC? + +RBAC means **Role-Based Access Control**. + +Instead of only checking if a user is authenticated, you also check: + +- What **ROLE** they have (admin, user, editor…) +- What **PERMISSIONS** they have (products:write, orders:read…) + +In Vix.cpp: + +JWT → extracts claims +RBAC → enforces roles + permissions + +--- + +## Architecture Overview + +Request flow: + +1. Client sends JWT (`Authorization: Bearer `) +2. JWT middleware validates signature +3. RBAC builds an `Authz` context +4. `require_role()` checks role +5. `require_perm()` checks permission +6. Route handler executes + +--- + +## Minimal pattern (middleware only) + +```cpp +App app; + +// 1) JWT middleware +JwtOptions jwt_opt{}; +jwt_opt.secret = "dev_secret"; +jwt_opt.verify_exp = false; + +// 2) RBAC middleware +RbacOptions rbac_opt{}; +rbac_opt.require_auth = true; +rbac_opt.use_resolver = false; + +auto jwt_mw = vix::middleware::app::adapt_ctx(vix::middleware::auth::jwt(jwt_opt)); +auto ctx_mw = vix::middleware::app::adapt_ctx(vix::middleware::auth::rbac_context(rbac_opt)); +auto role_mw = vix::middleware::app::adapt_ctx(vix::middleware::auth::require_role("admin")); +auto perm_mw = vix::middleware::app::adapt_ctx(vix::middleware::auth::require_perm("products:write")); + +// Apply only to /admin +app.use(vix::middleware::app::when( + [](const Request& r){ return r.path() == "/admin"; }, std::move(jwt_mw))); + +app.use(vix::middleware::app::when( + [](const Request& r){ return r.path() == "/admin"; }, std::move(ctx_mw))); + +app.use(vix::middleware::app::when( + [](const Request& r){ return r.path() == "/admin"; }, std::move(role_mw))); + +app.use(vix::middleware::app::when( + [](const Request& r){ return r.path() == "/admin"; }, std::move(perm_mw))); +``` + +--- + +## Using `Authz` in your handler + +After RBAC succeeds, you can read `Authz` from request state: + +```cpp +app.get("/admin", [](Request& req, Response& res) +{ + auto& authz = req.state(); + + res.json({ + "ok", true, + "sub", authz.subject, + "has_admin", authz.has_role("admin"), + "has_products_write", authz.has_perm("products:write") + }); +}); +``` + +--- + +## Test with curl + +### 1) No token + +```bash +curl -i http://localhost:8080/admin +``` + +Expected: **401 Unauthorized** + +### 2) Valid token (admin + products:write) + +```bash +curl -i -H "Authorization: Bearer " http://localhost:8080/admin +``` + +Expected: **200 OK** + +### 3) Missing permission + +```bash +curl -i -H "Authorization: Bearer " http://localhost:8080/admin +``` + +Expected: **403 Forbidden** + +--- + +## What happens internally? + +JWT payload example: + +```json +{ + "sub": "user123", + "roles": ["admin"], + "perms": ["products:write", "orders:read"] +} +``` + +RBAC builds: + +- `Authz.subject = "user123"` +- `Authz.roles = ["admin"]` +- `Authz.perms = ["products:write", "orders:read"]` + +Then: + +- `require_role("admin")` passes +- `require_perm("products:write")` passes + +--- + +## Common errors + +- **401** = no token, invalid token, invalid signature +- **403** = authenticated, but missing a required role or permission + +--- + +## Best practice + +Production order: + +JWT → RBAC Context → Role check → Permission check + +Avoid doing role checks manually inside handlers. + +--- + +# Complete Example (copy-paste) + +This is a full working file you can run directly. + +Save as: `rbac_app_simple.cpp` + +```cpp +/** + * + * @file rbac_app_simple.cpp — RBAC (roles + perms) example (Vix.cpp) + * + */ +#include +#include + +#include + +#include +#include +#include + +using namespace vix; + +// HS256, secret=dev_secret +// payload: {"sub":"user123","roles":["admin"],"perms":["products:write","orders:read"]} +static const std::string TOKEN_OK = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." + "eyJzdWIiOiJ1c2VyMTIzIiwicm9sZXMiOlsiYWRtaW4iXSwicGVybXMiOlsicHJvZHVjdHM6d3JpdGUiLCJvcmRlcnM6cmVhZCJdfQ." + "w1y3nA2F1kq0oJ0x8wWc5wQx8zF4h2d6V7mYp0jYk3Q"; + +// HS256, secret=dev_secret +// payload: {"sub":"user123","roles":["admin"],"perms":["orders:read"]} (missing products:write) +static const std::string TOKEN_NO_PERM = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." + "eyJzdWIiOiJ1c2VyMTIzIiwicm9sZXMiOlsiYWRtaW4iXSwicGVybXMiOlsib3JkZXJzOnJlYWQiXX0." + "qVqWmQmHf4yqPzvYzGf9m3jv9oGzW0Q8c8qkQkqkQkQ"; + +int main() +{ + App app; + + // 1) JWT auth (puts JwtClaims into request state) + vix::middleware::auth::JwtOptions jwt_opt{}; + jwt_opt.secret = "dev_secret"; + jwt_opt.verify_exp = false; + + // 2) RBAC: build Authz from JwtClaims, then enforce rules + vix::middleware::auth::RbacOptions rbac_opt{}; + rbac_opt.require_auth = true; + rbac_opt.use_resolver = false; // keep the example simple + + auto jwt_mw = vix::middleware::app::adapt_ctx(vix::middleware::auth::jwt(jwt_opt)); + auto ctx_mw = vix::middleware::app::adapt_ctx(vix::middleware::auth::rbac_context(rbac_opt)); + auto role_mw = vix::middleware::app::adapt_ctx(vix::middleware::auth::require_role("admin")); + auto perm_mw = vix::middleware::app::adapt_ctx(vix::middleware::auth::require_perm("products:write")); + + // Protect only /admin + app.use(vix::middleware::app::when( + [](const Request &req){ return req.path() == "/admin"; }, + std::move(jwt_mw))); + + app.use(vix::middleware::app::when( + [](const Request &req){ return req.path() == "/admin"; }, + std::move(ctx_mw))); + + app.use(vix::middleware::app::when( + [](const Request &req){ return req.path() == "/admin"; }, + std::move(role_mw))); + + app.use(vix::middleware::app::when( + [](const Request &req){ return req.path() == "/admin"; }, + std::move(perm_mw))); + + // Public route + app.get("/", [](Request &, Response &res) + { + res.send("RBAC example: /admin requires role=admin + perm=products:write"); + }); + + // Protected route + app.get("/admin", [](Request &req, Response &res) + { + auto &authz = req.state(); + + res.json({ + "ok", true, + "sub", authz.subject, + "has_admin", authz.has_role("admin"), + "has_products_write", authz.has_perm("products:write") + }); + }); + + std::cout + << "Vix RBAC example running:\n" + << " http://localhost:8080/\n" + << " http://localhost:8080/admin\n\n" + << "TOKEN_OK:\n " << TOKEN_OK << "\n\n" + << "TOKEN_NO_PERM:\n " << TOKEN_NO_PERM << "\n\n" + << "Try:\n" + << " curl -i http://localhost:8080/admin\n" + << " curl -i -H \"Authorization: Bearer " << TOKEN_OK << "\" http://localhost:8080/admin\n" + << " curl -i -H \"Authorization: Bearer " << TOKEN_NO_PERM << "\" http://localhost:8080/admin\n"; + + app.run(8080); + return 0; +} +``` + +## Run + +```bash +vix run rbac_app_simple.cpp +``` + + diff --git a/docs/examples/realtime.md b/docs/examples/realtime.md new file mode 100644 index 0000000..c27035a --- /dev/null +++ b/docs/examples/realtime.md @@ -0,0 +1,355 @@ +# Real-Time (HTTP + WebSocket) + +This guide shows beginner-friendly real-time examples with Vix.cpp. + +It focuses on: + +- Running HTTP and WebSocket together +- A simple typed message protocol: `{ "type": "...", "payload": { ... } }` +- Broadcasting messages to all connected clients +- Testing quickly with tools you can install in 2 minutes + +All examples are single-file and keep logic inside `main()`. + +--- + +## What you need + +- Vix.cpp installed and available in your PATH (`vix` command) +- A terminal + +Optional but recommended: + +- `curl` (usually already installed) +- `websocat` (for WebSocket testing) + +### Check Vix is installed + +```bash +vix --version +``` + +--- + +## 1) Minimal HTTP + WebSocket server (single file) + +This is the smallest real-time server you can build: + +- HTTP route: `GET /` +- WebSocket: echoes `chat.message` to everyone + +Create a file `main.cpp`: + +```cpp +#include +#include + +using namespace vix; + +int main() +{ + vix::serve_http_and_ws([](App& app, auto& ws) + { + // HTTP: normal route + app.get("/", [](Request&, Response& res) + { + res.json({ + "message", "Hello from Vix.cpp", + "realtime", true + }); + }); + + // WebSocket: typed messages { "type": "...", "payload": {...} } + ws.on_typed_message([&ws](auto&, const std::string& type, const vix::json::kvs& payload) + { + if (type == "chat.message") + { + // Broadcast to everyone connected + ws.broadcast_json("chat.message", payload); + } + }); + }); + + return 0; +} +``` + +### Run it + +If your Vix install supports `vix run`: + +```bash +vix run main.cpp +``` + +If you build with CMake in your project, compile and run your binary as usual. The example is still the same. + +### Test HTTP + +Open a new terminal: + +```bash +curl http://127.0.0.1:8080/ +``` + +Expected output is JSON similar to: + +```json +{"message":"Hello from Vix.cpp","realtime":true} +``` + +--- + +## 2) WebSocket testing with websocat + +### Install websocat + +On Ubuntu/Debian: + +```bash +sudo apt-get update +sudo apt-get install -y websocat +``` + +### Connect to the server + +In a new terminal: + +```bash +websocat ws://127.0.0.1:9090/ +``` + +Now paste this message and press Enter: + +```json +{"type":"chat.message","payload":{"user":"Alice","text":"Hello from WS"}} +``` + +If you open two websocat terminals connected at the same time, both will receive the broadcast. + +--- + +## 3) The message format (typed protocol) + +Vix WebSocket examples use a simple convention: + +```json +{ + "type": "chat.message", + "payload": { + "user": "Alice", + "text": "Hello!" + } +} +``` + +Rules of thumb: + +- `type` tells the server what kind of event it is +- `payload` is the data of the event +- Keep payload small and consistent +- Always handle unknown types (for debugging) + +--- + +## 4) Add join + typing events (still minimal) + +This example adds two extra message types: + +- `chat.join` (someone joined) +- `chat.typing` (typing indicator) +- `chat.message` (real message) +- `chat.unknown` (debug fallback) + +Create `main.cpp`: + +```cpp +#include +#include + +using namespace vix; + +int main() +{ + vix::serve_http_and_ws("config/config.json", 8080, [](App& app, auto& ws) + { + app.get("/health", [](Request&, Response& res) + { + res.json({ "ok", true, "service", "realtime" }); + }); + + ws.on_open([&ws](auto&) + { + ws.broadcast_json("chat.system", { + "text", "A new client connected" + }); + }); + + ws.on_typed_message([&ws](auto&, const std::string& type, const vix::json::kvs& payload) + { + if (type == "chat.join") + { + ws.broadcast_json("chat.system", payload); + return; + } + + if (type == "chat.typing") + { + ws.broadcast_json("chat.typing", payload); + return; + } + + if (type == "chat.message") + { + ws.broadcast_json("chat.message", payload); + return; + } + + ws.broadcast_json("chat.unknown", { + "type", type + }); + }); + }); + + return 0; +} +``` + +### Run it + +```bash +vix run main.cpp +``` + +### Test HTTP + +```bash +curl http://127.0.0.1:8080/health +``` + +### Test WebSocket + +```bash +websocat ws://127.0.0.1:9090/ +``` + +Paste these lines (one by one): + +```json +{"type":"chat.join","payload":{"user":"Alice"}} +{"type":"chat.typing","payload":{"user":"Alice"}} +{"type":"chat.message","payload":{"user":"Alice","text":"hello"}} +{"type":"something.weird","payload":{"debug":true}} +``` + +--- + +## 5) Config examples (config/config.json) + +These examples cover common setups. + +### A) One port for HTTP and WebSocket + +```json +{ + "server": { + "port": 8080, + "request_timeout": 5000 + }, + "websocket": { + "port": 8080, + "max_message_size": 65536, + "idle_timeout": 600 + } +} +``` + +### B) Separate ports (most common) + +```json +{ + "server": { + "port": 8080, + "request_timeout": 5000 + }, + "websocket": { + "port": 9090, + "max_message_size": 65536, + "idle_timeout": 600, + "ping_interval": 30, + "auto_ping_pong": true + } +} +``` + +### C) Dev mode (smaller limits) + +```json +{ + "server": { + "port": 8080, + "request_timeout": 3000 + }, + "websocket": { + "port": 9090, + "max_message_size": 16384, + "idle_timeout": 120, + "ping_interval": 15, + "auto_ping_pong": true + } +} +``` + +### D) Production style (bigger limits) + +```json +{ + "server": { + "port": 8080, + "request_timeout": 10000 + }, + "websocket": { + "port": 9090, + "max_message_size": 262144, + "idle_timeout": 1800, + "ping_interval": 30, + "enable_deflate": true, + "auto_ping_pong": true + } +} +``` + +--- + +## 6) Debug checklist + +### HTTP works but WS does not + +- Check `websocket.port` in your config +- Connect explicitly: + - `websocat ws://127.0.0.1:9090/` + +### WS connects but no messages appear + +- Make sure you send the typed format with `type` and `payload` +- Start two clients to see broadcast behavior + +### Port already in use + +- Change `server.port` and/or `websocket.port` +- Or stop the process using the port: + - `lsof -i :8080` + - `kill ` + +--- + +## 7) Practical mental model + +- Use HTTP for bootstrapping: + - `/`, `/health`, `/me`, `/config` +- Use WebSocket for live events: + - chat messages, presence, typing, notifications +- Keep the protocol small: + - a few `type` values, consistent `payload` fields +- Start with broadcast, then add rooms, auth, and persistence later + diff --git a/docs/examples/repository-crud-full.md b/docs/examples/repository-crud-full.md new file mode 100644 index 0000000..6d3d365 --- /dev/null +++ b/docs/examples/repository-crud-full.md @@ -0,0 +1,311 @@ +# ORM Example Guide: Repository CRUD (Full) + +This guide explains the `repository_crud_full` example step by step. + +Goal: - Define a domain model (`User`) - Teach the ORM how to map +database rows to `User` - Use `BaseRepository` to perform full +CRUD: - Create - Read - Update - Delete + +This is the core pattern for building real applications with Vix ORM. + +# 1. What this example demonstrates + +You will learn: + +- How to define a model struct (`User`) +- How to specialize `vix::orm::Mapper` for your model +- How `fromRow()` works (row -\> object) +- How `toInsertParams()` / `toUpdateParams()` work (object -\> params) +- How to use `BaseRepository` with a connection pool +- How CRUD operations return ids and results + +# 2. Full Example Code + +``` cpp +#include + +#include +#include +#include +#include +#include +#include + +struct User +{ + std::int64_t id{}; + std::string name; + std::string email; + int age{}; +}; + +namespace vix::orm +{ + template <> + struct Mapper + { + static User fromRow(const ResultRow &row) + { + User u{}; + u.id = row.getInt64Or(0, 0); + u.name = row.getStringOr(1, ""); + u.email = row.getStringOr(2, ""); + u.age = static_cast(row.getInt64Or(3, 0)); + return u; + } + + static std::vector> + toInsertParams(const User &u) + { + return { + {"name", u.name}, + {"email", u.email}, + {"age", u.age}, + }; + } + + static std::vector> + toUpdateParams(const User &u) + { + return { + {"name", u.name}, + {"email", u.email}, + {"age", u.age}, + }; + } + }; +} // namespace vix::orm + +int main(int argc, char **argv) +{ + using namespace vix::orm; + + const std::string host = (argc > 1 ? argv[1] : "tcp://127.0.0.1:3306"); + const std::string user = (argc > 2 ? argv[2] : "root"); + const std::string pass = (argc > 3 ? argv[3] : ""); + const std::string db = (argc > 4 ? argv[4] : "vixdb"); + + try + { + auto factory = make_mysql_factory(host, user, pass, db); + + PoolConfig cfg; + cfg.min = 1; + cfg.max = 8; + + ConnectionPool pool{factory, cfg}; + pool.warmup(); + + BaseRepository repo{pool, "users"}; + + // Create + const std::int64_t id = static_cast( + repo.create(User{0, "Bob", "gaspardkirira@example.com", 30})); + std::cout << "[OK] create -> id=" << id << "\n"; + + // Update + (void)repo.updateById(id, User{id, "Adastra", "adastra@example.com", 31}); + std::cout << "[OK] update -> id=" << id << "\n"; + + // (Optional) Read back + if (auto u = repo.findById(id)) + { + std::cout << "[OK] findById -> name=" << u->name + << " email=" << u->email + << " age=" << u->age << "\n"; + } + + // Delete + (void)repo.removeById(id); + std::cout << "[OK] delete -> id=" << id << "\n"; + + return 0; + } + catch (const DBError &e) + { + std::cerr << "[DBError] " << e.what() << "\n"; + return 1; + } + catch (const std::exception &e) + { + std::cerr << "[ERR] " << e.what() << "\n"; + return 1; + } +} +``` + +# 3. Step by Step Explanation + +## 3.1 Define your model + +``` cpp +struct User +{ + std::int64_t id{}; + std::string name; + std::string email; + int age{}; +}; +``` + +This is your domain object. It represents one row in the database table. + +## 3.2 Teach ORM how to map `User` + +Vix ORM uses a `Mapper` specialization. This is how the ORM becomes +type-aware. + +### fromRow() (DB -\> C++) + +``` cpp +static User fromRow(const ResultRow &row) +{ + User u{}; + u.id = row.getInt64Or(0, 0); + u.name = row.getStringOr(1, ""); + u.email = row.getStringOr(2, ""); + u.age = static_cast(row.getInt64Or(3, 0)); + return u; +} +``` + +- Column index 0 -\> id +- Column index 1 -\> name +- Column index 2 -\> email +- Column index 3 -\> age + +The `Or` methods provide defaults if the value is NULL or missing. + +Production tip: If you require strict schema, use the non-`Or` getters +(if available) and fail fast. + +### toInsertParams() (C++ -\> DB) + +``` cpp +static std::vector> +toInsertParams(const User &u) +{ + return { + {"name", u.name}, + {"email", u.email}, + {"age", u.age}, + }; +} +``` + +This returns column/value pairs used by `repo.create()`. + +Important: - id is not included because it is auto-generated by MySQL. + +### toUpdateParams() (C++ -\> DB) + +``` cpp +static std::vector> +toUpdateParams(const User &u) +{ + return { + {"name", u.name}, + {"email", u.email}, + {"age", u.age}, + }; +} +``` + +This returns column/value pairs used by `repo.updateById()`. + +## 3.3 Create the pool (reusable DB connections) + +``` cpp +ConnectionPool pool{factory, cfg}; +pool.warmup(); +``` + +- Pool reduces overhead +- Warmup avoids slow first query + +## 3.4 Create the repository + +``` cpp +BaseRepository repo{pool, "users"}; +``` + +This binds the repository to: + +- type: `User` +- table: `users` +- pool: `pool` + +Now you can call CRUD methods directly. + +# 4. CRUD Operations Explained + +## 4.1 Create + +``` cpp +auto id = repo.create(User{0, "Bob", "gaspardkirira@example.com", 30}); +``` + +- Uses `toInsertParams()` internally +- Executes an INSERT +- Returns the inserted id + +## 4.2 Update + +``` cpp +repo.updateById(id, User{id, "Adastra", "adastra@example.com", 31}); +``` + +- Uses `toUpdateParams()` internally +- Updates the row with that id +- Returns how many rows were updated (commonly 1) + +## 4.3 Read (Optional) + +``` cpp +if (auto u = repo.findById(id)) { ... } +``` + +- Returns `std::optional` +- Uses `fromRow()` internally +- If row missing -\> empty optional + +## 4.4 Delete + +``` cpp +repo.removeById(id); +``` + +- Deletes the row by id +- Returns affected rows (commonly 1) + +# 5. Required SQL Table + +``` sql +CREATE TABLE IF NOT EXISTS users ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + name VARCHAR(255) NOT NULL, + email VARCHAR(255) NOT NULL, + age INT NOT NULL +); +``` + +# 6. Production Notes + +Recommended improvements for real apps: + +- Add a unique index on email +- Add validation before create/update +- Add transactions when doing multiple operations +- Use UnitOfWork for multi-table business operations +- Avoid using `std::any` in hot paths if you need extreme performance + (use typed binders when available) + +# Summary + +You learned: + +- How to define a model (User) +- How Mapper``{=html} makes ORM type-safe +- How BaseRepository``{=html} provides CRUD APIs +- How create/update/find/delete work internally + diff --git a/docs/examples/rest-api.md b/docs/examples/rest-api.md new file mode 100644 index 0000000..9b6f508 --- /dev/null +++ b/docs/examples/rest-api.md @@ -0,0 +1,217 @@ +# REST API (Minimal Patterns) + +This page shows small, focused REST-style examples in Vix.cpp. + +Each section is: - One concept - One minimal `main()` - One clear +purpose + +------------------------------------------------------------------------ + +## 1) Basic GET (JSON) + +Simple REST endpoint returning JSON. + +``` cpp +#include +using namespace vix; + +int main() +{ + App app; + + app.get("/users", [](Request&, Response& res) + { + res.json({ + "data", json::array({ + json::obj({"id", 1, "name", "Ada"}), + json::obj({"id", 2, "name", "Bob"}) + }) + }); + }); + + app.run(8080); + return 0; +} +``` + +------------------------------------------------------------------------ + +## 2) Path Parameter + +Access resource by ID. + +``` cpp +#include +using namespace vix; + +int main() +{ + App app; + + app.get("/users/{id}", [](Request& req, Response& res) + { + const auto id = req.param("id"); + + res.json({ + "id", id, + "name", "User#" + id + }); + }); + + app.run(8080); + return 0; +} +``` + +------------------------------------------------------------------------ + +## 3) Query Parameters (Pagination) + +Typical REST pagination pattern. + +``` cpp +#include +using namespace vix; + +int main() +{ + App app; + + app.get("/users", [](Request& req, Response& res) + { + auto page = req.query_value("page", "1"); + auto limit = req.query_value("limit", "10"); + + res.json({ + "page", page, + "limit", limit + }); + }); + + app.run(8080); + return 0; +} +``` + +------------------------------------------------------------------------ + +## 4) POST with JSON Body + +Create resource. + +``` cpp +#include +using namespace vix; + +int main() +{ + App app; + + app.post("/users", [](Request& req, Response& res) + { + const auto& body = req.json(); + + res.status(201).json({ + "created", true, + "payload", body + }); + }); + + app.run(8080); + return 0; +} +``` + +------------------------------------------------------------------------ + +## 5) Status Codes + +Return custom HTTP status. + +``` cpp +#include +using namespace vix; + +int main() +{ + App app; + + app.get("/not-found", [](Request&, Response& res) + { + res.status(404).json({ + "error", "Resource not found" + }); + }); + + app.run(8080); + return 0; +} +``` + +------------------------------------------------------------------------ + +## 6) Headers + +Read request headers. + +``` cpp +#include +using namespace vix; + +int main() +{ + App app; + + app.get("/headers", [](Request& req, Response& res) + { + res.json({ + "user_agent", req.header("User-Agent"), + "host", req.header("Host") + }); + }); + + app.run(8080); + return 0; +} +``` + +------------------------------------------------------------------------ + +## 7) Auto-send Return Style + +Return value instead of calling `res.json()`. + +``` cpp +#include +#include +using namespace vix; + +int main() +{ + App app; + + app.get("/auto", [](Request&, Response&) + { + return vix::json::o( + "message", "Auto-sent JSON", + "ok", true + ); + }); + + app.run(8080); + return 0; +} +``` + +------------------------------------------------------------------------ + +## What This Teaches + +- REST routing structure +- Path parameters +- Query parameters +- JSON request body +- Status codes +- Header access +- Two response styles (explicit vs auto-send) + diff --git a/docs/examples/retry-policy.md b/docs/examples/retry-policy.md new file mode 100644 index 0000000..41b3bc3 --- /dev/null +++ b/docs/examples/retry-policy.md @@ -0,0 +1,133 @@ +# Retry Policy Example + +This page demonstrates how to use the RetryPolicy in Vix.cpp sync +module. + +It explains: + +- What exponential backoff means +- How retry delay is computed +- How to test retry logic +- How retry interacts with Outbox and SyncEngine + +## 1) Minimal RetryPolicy Usage + +``` cpp +#include +#include + +int main() +{ + vix::sync::RetryPolicy policy; + + for (std::uint32_t attempt = 0; attempt < 6; ++attempt) + { + if (policy.can_retry(attempt)) + { + auto delay = policy.compute_delay_ms(attempt); + std::cout << "Attempt " << attempt + << " -> delay = " << delay << " ms\n"; + } + else + { + std::cout << "Attempt " << attempt + << " -> no more retries\n"; + } + } + + return 0; +} +``` + +What this shows: + +- Delay grows exponentially +- Delay is clamped by max_delay_ms +- Retries stop after max_attempts + +## 2) Custom Retry Configuration + +``` cpp +#include + +int main() +{ + vix::sync::RetryPolicy policy; + policy.max_attempts = 5; + policy.base_delay_ms = 1000; // 1 second + policy.max_delay_ms = 10000; // 10 seconds + policy.factor = 2.0; + + for (std::uint32_t attempt = 0; attempt < 7; ++attempt) + { + if (policy.can_retry(attempt)) + { + auto delay = policy.compute_delay_ms(attempt); + } + } + + return 0; +} +``` + +This configuration means: + +Attempt 0 -\> 1s\ +Attempt 1 -\> 2s\ +Attempt 2 -\> 4s\ +Attempt 3 -\> 8s\ +Attempt 4 -\> 10s (clamped)\ +Attempt 5 -\> stop + +## 3) How Retry Works with Outbox + +When an operation fails: + +1) SyncWorker calls Outbox::fail() +2) RetryPolicy computes delay +3) next_retry_at_ms is set +4) Operation becomes eligible again later + +Minimal flow example: + +``` cpp +if (!send_result.ok) +{ + if (send_result.retryable) + { + outbox->fail(id, send_result.error, now_ms, true); + } + else + { + outbox->fail(id, send_result.error, now_ms, false); + } +} +``` + +## 4) How to Test Retry Logic (Beginner-Friendly) + +You can simulate failure using a fake transport: + +- Return ok = false +- Return retryable = true +- Call engine.tick(now_ms) +- Advance time manually +- Call engine.tick(later_time) + +If attempt \< max_attempts: Operation status -\> Failed (retry +scheduled) + +If attempt \>= max_attempts: Operation status -\> PermanentFailed + +## 5) Important Rules + +- RetryPolicy is deterministic +- Delay is recomputable during recovery +- Retry never mutates payload intent +- Only lifecycle state changes + +RetryPolicy ensures: + +Network failures do not cause retry storms. Recovery is predictable. +Offline-first systems remain stable. + diff --git a/docs/examples/routing.md b/docs/examples/routing.md new file mode 100644 index 0000000..3ed297e --- /dev/null +++ b/docs/examples/routing.md @@ -0,0 +1,196 @@ +# Routing + +This section demonstrates how routing works in Vix.cpp. + +Each example is minimal and self-contained. + +--- + +## 1. Basic GET Route + +```cpp +#include + +using namespace vix; + +int main() +{ + App app; + + app.get("/hello", [](Request&, Response& res) + { + res.send("Hello"); + }); + + app.run(8080); + return 0; +} +``` + +--- + +## 2. Multiple HTTP Methods + +```cpp +#include + +using namespace vix; + +int main() +{ + App app; + + app.get("/users", [](Request&, Response& res) + { + res.send("GET users"); + }); + + app.post("/users", [](Request&, Response& res) + { + res.send("POST users"); + }); + + app.put("/users", [](Request&, Response& res) + { + res.send("PUT users"); + }); + + app.del("/users", [](Request&, Response& res) + { + res.send("DELETE users"); + }); + + app.run(8080); + return 0; +} +``` + +--- + +## 3. Path Parameters + +```cpp +#include + +using namespace vix; + +int main() +{ + App app; + + app.get("/users/{id}", [](Request& req, Response& res) + { + auto id = req.param("id"); + res.json({ + "user_id", id + }); + }); + + app.run(8080); + return 0; +} +``` + +Test: + + curl http://localhost:8080/users/42 + +--- + +## 4. Query Parameters + +```cpp +#include + +using namespace vix; + +int main() +{ + App app; + + app.get("/search", [](Request& req, Response& res) + { + auto q = req.query_value("q", ""); + auto page = req.query_value("page", "1"); + + res.json({ + "query", q, + "page", page + }); + }); + + app.run(8080); + return 0; +} +``` + +Test: + + curl "http://localhost:8080/search?q=vix&page=2" + +--- + +## 5. Heavy Routes + +Use get_heavy when the handler performs blocking or heavy work. + +```cpp +#include + +using namespace vix; + +int main() +{ + App app; + + app.get_heavy("/report", [](Request&, Response& res) + { + // Simulated heavy work + res.send("Heavy report generated"); + }); + + app.run(8080); + return 0; +} +``` + +--- + +## 6. Returning Values Directly + +If you return a value instead of calling res.send(), +Vix automatically sends it. + +```cpp +#include +#include + +using namespace vix; + +int main() +{ + App app; + + app.get("/auto", [](Request&, Response&) + { + return vix::json::o( + "ok", true, + "message", "Auto-sent response" + ); + }); + + app.run(8080); + return 0; +} +``` + +--- + +## What this teaches + +- Defining routes with different HTTP methods +- Using path parameters +- Using query parameters +- Heavy vs light routes +- Auto-send return style + diff --git a/docs/examples/session-advanced.md b/docs/examples/session-advanced.md new file mode 100644 index 0000000..1ee2fd0 --- /dev/null +++ b/docs/examples/session-advanced.md @@ -0,0 +1,212 @@ +# Advanced Security Section --- Sessions & JWT + +This guide expands on advanced security considerations for +**Session-based** and **JWT-based** authentication. + +This is for developers building real production systems. + +# 1️⃣ Transport Security (MANDATORY) + +## Always use HTTPS + +Without HTTPS: + +- Cookies can be intercepted +- JWT tokens can be stolen +- Session IDs can be hijacked + +In production: + +- Enable TLS +- Set Secure cookies +- Use HSTS + +Example (session strict preset): + +``` cpp +app.use(middleware::app::session_strict("strong_secret")); +``` + +# 2️⃣ Cookie Security (Session) + +When using Sessions: + +## Enable: + +- HttpOnly → prevents JS access +- Secure → HTTPS only +- SameSite → Lax or Strict + +Example: + +``` cpp +middleware::app::session_dev( + "secret", + "sid", + std::chrono::hours(24), + true, // secure + "Strict", // same_site + true // http_only +); +``` + +## Protect Against: + +- XSS +- CSRF +- Session fixation + +# 3️⃣ CSRF Protection (Session) + +Sessions are vulnerable to CSRF. + +Use CSRF middleware: + +``` cpp +app.use(middleware::app::csrf_dev()); +``` + +Why? + +Because browsers automatically send cookies. + +JWT (in Authorization header) is less vulnerable to CSRF. + +# 4️⃣ JWT Security Best Practices + +## Use Strong Secret + +Do NOT use: + +"dev" "1234" "secret" + +Use: + +- 256-bit random secret +- Or environment variable + +## Set Expiration + +Always set: + +- exp claim +- Short TTL for access tokens + +Enable expiration verification: + +``` cpp +middleware::app::jwt_dev("secret", true); +``` + +## Avoid Storing JWT in LocalStorage + +Why? + +LocalStorage is accessible to JavaScript. If XSS occurs → token stolen. + +Safer option: + +- Store JWT in HttpOnly Secure cookie +- Or use short-lived tokens + +# 5️⃣ Token Revocation Strategy + +JWT problem: + +You cannot easily revoke tokens. + +Solutions: + +- Short expiration (15 min) +- Refresh tokens +- Blacklist storage +- Rotate signing keys + +# 6️⃣ Session Storage Hardening + +If using sessions at scale: + +- Use Redis +- Use expiration +- Limit session size +- Rotate session ID on login + +Prevent session fixation: + +Generate new session ID after authentication. + +# 7️⃣ RBAC Hardening + +Never trust client roles. + +Always verify on server: + +``` cpp +require_role("admin"); +require_perm("products:write"); +``` + +Never rely only on frontend checks. + +# 8️⃣ Rate Limiting + Auth + +Protect authentication endpoints: + +- Rate limit login +- Rate limit token generation + +Example: + +``` cpp +app.use(middleware::app::rate_limit_dev(10)); +``` + +# 9️⃣ Defense in Depth + +Combine: + +- HTTPS +- Security headers +- CSRF +- Rate limit +- RBAC +- Logging +- Monitoring + +Security is layered. + +# 🔟 Production Checklist + +For Sessions: + +- HTTPS +- Secure + HttpOnly +- SameSite configured +- CSRF enabled +- Session TTL +- Session ID rotation + +For JWT: + +- Strong secret +- exp enabled +- verify_exp = true +- Short TTL +- Secure storage +- RBAC enforcement + +# Final Advice + +Authentication is not just middleware. + +It is: + +- Transport security +- Storage security +- Token lifecycle management +- Authorization enforcement +- Monitoring and logging + +Build systems assuming attackers exist. + +Security is architecture, not configuration. diff --git a/docs/examples/session-jwt.md b/docs/examples/session-jwt.md new file mode 100644 index 0000000..192b923 --- /dev/null +++ b/docs/examples/session-jwt.md @@ -0,0 +1,170 @@ +# Session vs JWT --- Beginner Friendly Comparison + +This guide explains the difference between **Session-based +authentication** and **JWT (JSON Web Token)** in a simple way. + +If you are building APIs with Vix.cpp, this will help you decide which +one to use. + +# 1️⃣ What is a Session? + +A **Session** is server-side authentication. + +Flow: + +1. User logs in +2. Server creates a session (stored in memory or DB) +3. Server sends a cookie (e.g., `sid=abc123`) +4. Browser automatically sends that cookie on every request +5. Server reads session data using that cookie + +### Key Idea: + +State is stored **on the server**. + +# 2️⃣ What is JWT? + +JWT is **token-based authentication**. + +Flow: + +1. User logs in +2. Server creates a signed token +3. Server sends token to client +4. Client sends token in `Authorization: Bearer ` +5. Server verifies signature and extracts claims + +### Key Idea: + +State is stored **inside the token** (on the client side). + +# 3️⃣ Architecture Difference + +## Session + +Client → Cookie → Server → Lookup session in storage + +## JWT + +Client → Bearer Token → Server → Verify signature → Extract claims + +# 4️⃣ Comparison Table + + Feature Session JWT + -------------------------- ---------------------- ---------------------------- + State location Server Client + Requires storage Yes No + Easy logout Yes (delete session) Hard (token remains valid) + Scales easily Needs shared store Yes (stateless) + Works well with browsers Excellent Good + Works well for APIs Good Excellent + Token size Small Larger + Revocation Easy Complex + +# 5️⃣ When to Use Session + +Use Session if: + +- You build a web app with cookies +- You need easy logout +- You want simple security +- You control the backend tightly + +Example: + +``` cpp +app.use(middleware::app::session_dev("secret")); +``` + +# 6️⃣ When to Use JWT + +Use JWT if: + +- You build a public API +- You have multiple services (microservices) +- You want stateless scaling +- You build mobile apps or SPAs + +Example: + +``` cpp +app.use("/api", middleware::app::jwt_dev("secret")); +``` + +# 7️⃣ Security Considerations + +## Session Risks + +- Session hijacking +- CSRF (needs CSRF protection) +- Server memory growth + +## JWT Risks + +- Cannot easily revoke token +- If stolen, valid until expiration +- Must protect signing secret + +# 8️⃣ Performance + +Session: - Needs storage lookup - Slightly slower at scale + +JWT: - Only signature verification - Faster in distributed systems + +# 9️⃣ Logout Behavior + +Session: + +``` cpp +// Destroy session +``` + +JWT: You cannot "destroy" a token easily. You must: - Wait for +expiration - Or maintain a blacklist + +# 🔟 Best Practice in Real Systems + +Large systems often use: + +- Session for admin panels +- JWT for APIs +- Or Session + RBAC +- Or JWT + RBAC + +# 1️⃣1️⃣ Which Should YOU Use? + +If you are a beginner: + +👉 Start with **Session** (simpler). + +If you build APIs or distributed systems: + +👉 Use **JWT**. + +# 1️⃣2️⃣ Vix.cpp Recommendation + +For local dev: + +``` cpp +middleware::app::session_dev("dev_secret"); +middleware::app::jwt_dev("dev_secret"); +``` + +For production: + +- Use strong secret +- Enable HTTPS +- Enable Secure + HttpOnly cookies +- Use expiration +- Combine with RBAC + +# Final Advice + +There is no "better" universally. + +Session = Simple + Server-Controlled\ +JWT = Scalable + Stateless + +Choose based on your architecture. + + diff --git a/docs/examples/session.md b/docs/examples/session.md new file mode 100644 index 0000000..7d2bea7 --- /dev/null +++ b/docs/examples/session.md @@ -0,0 +1,188 @@ +# Session Middleware Guide (Beginner) + +## 1. What is a Session? + +A session allows the server to store user-specific data across multiple +HTTP requests. + +HTTP is stateless. That means every request is independent. Sessions +solve this by attaching a session ID to the client (usually via a +cookie). + +The server then: + +- Reads the session ID from the cookie +- Loads session data +- Allows you to read/write values +- Sends the updated session back (signed) + +------------------------------------------------------------------------ + +## 2. Basic Example + +``` cpp +#include +#include +using namespace vix; + +int main() +{ + App app; + + app.use(middleware::app::adapt_ctx( + middleware::auth::session({.secret = "dev"}))); + + app.get("/session", [](Request &req, Response &res) + { + auto &s = req.state(); + + int n = s.get("n") ? std::stoi(*s.get("n")) : 0; + s.set("n", std::to_string(++n)); + + res.text("n=" + std::to_string(n)); + }); + + app.run(8080); +} +``` + +Run: +```bash + vix run session_app.cpp +``` +Test: +```bash + curl -i http://localhost:8080/session + curl -i http://localhost:8080/session + curl -i http://localhost:8080/session +``` +You will see: +```bash + n=1 + n=2 + n=3 +``` +The value persists because it is stored in the session. + +------------------------------------------------------------------------ + +## 3. Using the Preset (Recommended) + +Instead of manually adapting, use: + +``` cpp +app.use(middleware::app::session_dev("dev_secret")); +``` + +Default behavior: + +- Cookie name: sid +- Signed session cookie +- HttpOnly enabled +- SameSite=Lax +- TTL: 7 days + +------------------------------------------------------------------------ + +## 4. How Session Works Internally + +1. Client sends request. +2. Middleware checks cookie (sid). +3. If missing, creates new session (auto_create=true). +4. Session data is signed with secret. +5. Updated session is written back to cookie. + +Security note: Always use a strong secret in production. + +------------------------------------------------------------------------ + +## 5. Strict Production Mode + +``` cpp +app.use(middleware::app::session_strict("super_strong_secret")); +``` + +This enables: + +- Secure=true +- HttpOnly=true +- SameSite=None +- HTTPS required + +------------------------------------------------------------------------ + +## 6. Reading and Writing Values + +``` cpp +auto &s = req.state(); + +s.set("user_id", "42"); + +auto uid = s.get("user_id"); +if (uid) +{ + res.text("User = " + *uid); +} +``` + +------------------------------------------------------------------------ + +## 7. Full Example (Complete App) + +``` cpp +#include +#include + +using namespace vix; + +int main() +{ + App app; + + // Install session middleware + app.use(middleware::app::session_dev("dev_secret")); + + app.get("/", [](Request &, Response &res) + { + res.text("Visit /login, /me, /logout"); + }); + + app.get("/login", [](Request &req, Response &res) + { + auto &s = req.state(); + s.set("user", "alice"); + res.text("Logged in as alice"); + }); + + app.get("/me", [](Request &req, Response &res) + { + auto &s = req.state(); + auto user = s.get("user"); + + if (!user) + { + res.status(401).text("Not logged in"); + return; + } + + res.text("Current user: " + *user); + }); + + app.get("/logout", [](Request &req, Response &res) + { + auto &s = req.state(); + s.clear(); + res.text("Logged out"); + }); + + app.run(8080); +} +``` + +Test: +```bash + curl -i http://localhost:8080/login + curl -i http://localhost:8080/me + curl -i http://localhost:8080/logout + curl -i http://localhost:8080/me +``` diff --git a/docs/examples/static-files.md b/docs/examples/static-files.md new file mode 100644 index 0000000..5c90e80 --- /dev/null +++ b/docs/examples/static-files.md @@ -0,0 +1,219 @@ +# Static Files + +This guide explains how to serve static assets (HTML, CSS, JS, images) in Vix.cpp. + +You have two valid approaches: + +1) `app.static_dir("./public")` (high-level App API) +2) `middleware::performance::static_files(...)` (explicit middleware, more control) + +Both can be used in production. Prefer `app.static_dir()` for the simplest setup, and the middleware when you want explicit options or to mount multiple static roots. + +--- + +## When to serve static files from Vix + +Static files are useful for: +- Landing pages +- Simple dashboards +- OAuth connect pages +- Offline docs UI assets (Swagger UI offline) +- SPA bundles (React/Vue build output) +- Serving uploads in a controlled way (careful) + +If you run a separate CDN or object storage (S3, R2, etc.), you often serve static assets there. But Vix can still serve them directly for small or offline deployments. + +--- + +## Option A: The simplest way (recommended first) + +### Serve a directory + +```cpp +#include +using namespace vix; + +int main() +{ + App app; + + // Serve ./public under / + app.static_dir("./public"); + + // API example + app.get("/api/ping", [](Request&, Response& res){ + res.json({"ok", true}); + }); + + app.run(8080); +} +``` + +### Serve individual files + +```cpp +app.get("/", [](Request&, Response& res) +{ + res.file("./public/index.html"); +}); + +app.get("/connect", [](Request&, Response& res) +{ + res.file("./public/connect.html"); +}); +``` + +What happens: +- `static_dir("./public")` mounts the directory on `/` by default. +- `res.file(path)` reads and returns a file, sets a best-effort Content-Type, and adds safe defaults like `X-Content-Type-Options: nosniff`. + +--- + +## Option B: Explicit middleware (more control) + +Your example uses the performance middleware: + +```cpp +#include + +#include +#include + +using namespace vix; + +int main() +{ + App app; + + app.use(vix::middleware::app::adapt_ctx( + vix::middleware::performance::static_files( + "./public", + { + .mount = "/", + .index_file = "index.html", + .add_cache_control = true, + .cache_control = "public, max-age=3600", + .fallthrough = true, + }))); + + app.get("/api/ping", [](Request&, Response& res){ res.json({"ok", true}); }); + + app.run(8080); +} +``` + +Meaning of the options: +- `mount`: URL prefix where static assets are exposed. +- `index_file`: returned when the path points to a directory. +- `add_cache_control`: whether to add Cache-Control when missing. +- `cache_control`: Cache-Control value to set. +- `fallthrough`: if true, missing static file continues to the next route (useful for SPA routing). + +--- + +## SPA routing pattern (React/Vue) + +For a SPA, routes like `/app/settings` should usually return `index.html` so the frontend router can handle it. + +Use this clean pattern: + +- Serve static assets under `/` (or `/app`) +- Keep API under `/api` +- Enable fallthrough so unknown paths can be handled by a final route that serves `index.html` + +Example shape: + +```cpp +App app; + +// or mount "/app" depending on your app layout +app.static_dir("./public", "/"); + +app.get("/api/ping", [](Request&, Response& res){ + res.json({"ok", true}); +}); + +// Final SPA fallback (only if your static serving does not already do it) +app.get("/app/*", [](Request&, Response& res){ + res.file("./public/index.html"); +}); + +app.run(8080); +``` + +If your static middleware already supports index fallback with `fallthrough=true`, +you can keep the fallback route minimal or skip it depending on behavior. + +--- + +## Caching and production defaults + +### Good defaults for static assets +- HTML: low cache or no-cache (because it changes and references hashed assets) +- Hashed assets (app.9f3a2.js): long cache (`max-age=31536000, immutable`) +- Images/fonts: moderate to long cache depending on naming strategy + +If you do not use hashed filenames, keep cache short to avoid clients getting stale files. + +### Recommended approach +- Use `Cache-Control` for static assets +- Use `ETag` middleware if you want conditional GET and 304 responses + +ETag is especially nice when you keep cache short but still want bandwidth savings. + +--- + +## Security notes + +Static serving is a common source of bugs. Keep these rules: + +- Do not allow path traversal (`..`) to escape your root directory. +- If you serve uploads, put them in a dedicated directory with strict rules. +- Consider disabling directory listing (should be disabled by default). +- Set `X-Content-Type-Options: nosniff`. +- In production, add security headers (CSP, etc.) for HTML pages if you serve app UI. + +If you serve API and static from the same origin, +you can still apply security headers only on `/api` or only on UI routes depending on your needs. + +--- + +## Quick curl tests + +```bash +# Fetch index +curl -i http://localhost:8080/ + +# Fetch a text file +curl -i http://localhost:8080/hello.txt + +# API is still available +curl -i http://localhost:8080/api/ping +``` + +--- + +## What to choose + +Use `app.static_dir()` when: +- You want the most beginner-friendly setup +- One static root is enough +- You want to keep code short + +Use `static_files(...)` middleware when: +- You want explicit caching options per mount +- You want multiple mounts (ex: /docs, /assets, /app) +- You want clear fallthrough behavior for SPA routing + +--- + +## Next production step + +If your app serves both UI and API, the production structure usually looks like: + +- `/api/*` protected by middleware (auth, CORS, CSRF, security headers) +- `/docs/*` (optional) offline Swagger UI +- `/` (or `/app`) served as static SPA assets + +This keeps your server architecture predictable and clean. + diff --git a/docs/examples/streaming.md b/docs/examples/streaming.md new file mode 100644 index 0000000..118a5b3 --- /dev/null +++ b/docs/examples/streaming.md @@ -0,0 +1,186 @@ + +# Streaming (HTTP + WebSocket) + +This guide explains streaming patterns in Vix.cpp. + +Streaming means sending data progressively instead of sending one single response. + +You will learn: + +- HTTP chunk-style streaming +- Server-Sent style behavior (SSE-like pattern) +- WebSocket streaming +- How to test everything step by step + +1) HTTP Streaming (Chunked Response Style) + +Description: +Send multiple chunks over one HTTP request. + +main.cpp: + +```cpp +#include +#include +#include + +using namespace vix; + +int main() +{ + App app; + + app.get("/stream", [](Request&, Response& res) + { + res.set_header("Content-Type", "text/plain"); + + for (int i = 1; i <= 5; ++i) + { + res.write("Chunk " + std::to_string(i) + "\n"); + res.flush(); + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + } + + res.end(); + }); + + app.run(8080); +} +``` + +How to test: + +Terminal 1: +```bash + vix run main.cpp +``` +Terminal 2: +```bash + curl http://127.0.0.1:8080/stream +``` + +You should see chunks appearing progressively. + +2) Server-Sent Events (SSE-like Pattern) + +Description: +Push events over HTTP continuously. + +main.cpp: + +```cpp +#include +#include +#include + +using namespace vix; + +int main() +{ + App app; + + app.get("/events", [](Request&, Response& res) + { + res.set_header("Content-Type", "text/event-stream"); + res.set_header("Cache-Control", "no-cache"); + + for (int i = 0; i < 5; ++i) + { + res.write("data: event " + std::to_string(i) + "\n\n"); + res.flush(); + std::this_thread::sleep_for(std::chrono::seconds(1)); + } + + res.end(); + }); + + app.run(8080); +} +``` + +Test: +```bash + curl http://127.0.0.1:8080/events +``` +3) WebSocket Streaming (Live Broadcast) + +Description: +Push real-time data continuously to connected clients. + +main.cpp: + +```cpp +#include +#include +#include +#include + +using namespace vix; + +int main() +{ + vix::serve_http_and_ws([](App& app, auto& ws) + { + app.get("/", [](Request&, Response& res) + { + res.send("WebSocket streaming server running"); + }); + + ws.on_open([&ws](auto&) + { + std::thread([&ws]() + { + for (int i = 0; i < 5; ++i) + { + ws.broadcast_json("stream.tick", { + "value", i + }); + + std::this_thread::sleep_for(std::chrono::seconds(1)); + } + }).detach(); + }); + }); + + return 0; +} +``` + +How to test: + +Terminal 1: +```bash + vix run main.cpp +``` +Terminal 2: +```bash + websocat ws://127.0.0.1:9090/ +``` + +You should receive JSON messages every second. + +4) When to Use Each Streaming Type + +HTTP chunk streaming: +- Large file generation +- Report generation +- Progressive output + +SSE-like streaming: +- Live logs +- Monitoring dashboards +- One-direction event push + +WebSocket streaming: +- Chat +- Multiplayer state sync +- Real-time notifications +- Presence systems + +Key Takeaways + +- Streaming avoids blocking until full data is ready. +- WebSocket is best for bi-directional live systems. +- HTTP streaming works well for simple progressive output. +- Always test using curl or websocat first. + diff --git a/docs/examples/sync.md b/docs/examples/sync.md new file mode 100644 index 0000000..aea7ecf --- /dev/null +++ b/docs/examples/sync.md @@ -0,0 +1,648 @@ +# Offline-first Sync + +This page is a practical, beginner-friendly guide for the Vix.cpp offline-first sync engine. + +Goal: You can enqueue operations locally, stay offline, crash/restart, then safely converge when the network comes back. + +You will learn: + +- What the SyncEngine does +- How Outbox makes writes durable before any network attempt +- How RetryPolicy schedules retries +- How to test your setup quickly (copy-paste + run) +- How to simulate failures (offline, retryable errors, permanent errors, crash) + +--- + +## 0) Mental model (what happens at runtime) + +When you call `outbox.enqueue(op)`: + +1. The operation is persisted to disk (OutboxStore). +2. The engine can try to deliver it later (now or when online). + +When the engine ticks: + +1. It asks Outbox for ready operations. +2. It claims them (InFlight). +3. It sends them through a transport (HTTP, WS, P2P, edge). +4. It marks them Done or Failed (and schedules retry). + +Key offline-first rule: + +- No network attempt happens unless the operation was persisted first. + +--- + +## Headers + +Engine: + +```cpp +#include +#include +``` + +Outbox: + +```cpp +#include +#include +#include +``` + +Network: + +```cpp +#include +``` + +--- + +## How to run these examples + +You have two common ways: + +### Option A) Use Vix CLI (fastest) + +If you already use `vix run`: + +```bash +vix run main.cpp +``` + +This compiles and runs the file. + +### Option B) Build with CMake + +If your project is a normal CMake project, create a small target and link Vix. Then: + +```bash +cmake -S . -B build +cmake --build build -j +./build/sync_example +``` + +Notes for beginners: + +- These examples write files under `./.vix_test_*`. You can delete that folder anytime. +- They use `assert(...)` to keep checks minimal. + +--- + +## Helper: now_ms() (used by all examples) + +The sync engine uses millisecond timestamps. + +```cpp +#include +#include + +static std::int64_t now_ms() +{ + using namespace std::chrono; + return duration_cast( + steady_clock::now().time_since_epoch() + ).count(); +} +``` + +--- + +## Example 1: Minimal smoke test (enqueue + tick + Done) + +This is the smallest complete setup: file-backed outbox + always-online probe + transport + engine. + +What you should see: + +- The engine processes at least 1 operation. +- The operation ends as `Done`. + +```cpp +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +// Copy from Appendix at the end of this file +#include "fake_http_transport.hpp" + +static std::int64_t now_ms() +{ + using namespace std::chrono; + return duration_cast(steady_clock::now().time_since_epoch()).count(); +} + +static void reset_dir(const std::filesystem::path& dir) +{ + std::error_code ec; + std::filesystem::remove_all(dir, ec); + std::filesystem::create_directories(dir, ec); +} + +static void run_example_1() +{ + using namespace vix::sync; + using namespace vix::sync::engine; + using namespace vix::sync::outbox; + + const std::filesystem::path dir = "./.vix_test_smoke"; + reset_dir(dir); + + auto store = std::make_shared(FileOutboxStore::Config{ + .file_path = dir / "outbox.json", + .pretty_json = true, + .fsync_on_write = false + }); + + auto outbox = std::make_shared( + Outbox::Config{ .owner = "example-1" }, + store + ); + + auto probe = std::make_shared( + vix::net::NetworkProbe::Config{}, + [] { return true; } // always online + ); + + auto transport = std::make_shared(); + transport->setDefault({ .ok = true }); + + SyncEngine engine( + SyncEngine::Config{ .worker_count = 1, .batch_limit = 10 }, + outbox, probe, transport + ); + + Operation op; + op.kind = "http.post"; + op.target = "/api/messages"; + op.payload = R"({"text":"hello offline"})"; + + const auto id = outbox->enqueue(op, now_ms()); + + const auto processed = engine.tick(now_ms()); + assert(processed >= 1); + + auto saved = store->get(id); + assert(saved.has_value()); + assert(saved->status == OperationStatus::Done); + + std::cout << "[example-1] OK: operation is Done\n"; +} + +int main() +{ + run_example_1(); + return 0; +} +``` + +Beginner tip: + +- Open `./.vix_test_smoke/outbox.json` to see the durable state. + +--- + +## Example 2: Retryable failure (fails first, succeeds later) + +This simulates a flaky network or a temporary server error. + +What you should see: + +- First tick: operation becomes Failed and a retry is scheduled. +- Later tick: operation becomes Done. + +```cpp +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "fake_http_transport.hpp" + +static std::int64_t now_ms() +{ + using namespace std::chrono; + return duration_cast(steady_clock::now().time_since_epoch()).count(); +} + +static void reset_dir(const std::filesystem::path& dir) +{ + std::error_code ec; + std::filesystem::remove_all(dir, ec); + std::filesystem::create_directories(dir, ec); +} + +static void run_example_2() +{ + using namespace vix::sync; + using namespace vix::sync::engine; + using namespace vix::sync::outbox; + + const std::filesystem::path dir = "./.vix_test_retry"; + reset_dir(dir); + + auto store = std::make_shared(FileOutboxStore::Config{ + .file_path = dir / "outbox.json", + .pretty_json = true, + .fsync_on_write = false + }); + + auto outbox = std::make_shared( + Outbox::Config{ .owner = "example-2" }, + store + ); + + auto probe = std::make_shared( + vix::net::NetworkProbe::Config{}, + [] { return true; } + ); + + auto transport = std::make_shared(); + + // First attempt fails (retryable), then succeed. + transport->setRuleForTarget("/api/messages", FakeHttpTransport::Rule{ + .ok = false, + .retryable = true, + .error = "temporary error (retryable)" + }); + + SyncEngine engine( + SyncEngine::Config{ .worker_count = 1, .batch_limit = 10 }, + outbox, probe, transport + ); + + Operation op; + op.kind = "http.post"; + op.target = "/api/messages"; + op.payload = R"({"text":"retry me"})"; + + const auto t0 = now_ms(); + const auto id = outbox->enqueue(op, t0); + + // Tick 1: should fail (retryable) + engine.tick(t0); + + { + auto saved = store->get(id); + assert(saved.has_value()); + assert(saved->status == OperationStatus::Failed || saved->status == OperationStatus::Pending); + } + + // Switch transport to success for next attempt + transport->setRuleForTarget("/api/messages", FakeHttpTransport::Rule{ .ok = true }); + + // Tick 2: run in the future so retry window can pass. + const auto t1 = t0 + 10'000; + engine.tick(t1); + + auto final = store->get(id); + assert(final.has_value()); + assert(final->status == OperationStatus::Done); + + std::cout << "[example-2] OK: retryable failure eventually becomes Done\n"; +} + +int main() +{ + run_example_2(); + return 0; +} +``` + +Beginner tip: + +- Retry scheduling is computed using RetryPolicy. +- You can tune delays and max attempts in `Outbox::Config{ .retry = ... }`. + +--- + +## Example 3: Permanent failure (do not retry) + +This simulates a permanent error such as invalid request data. + +What you should see: + +- The operation becomes PermanentFailed. +- The transport is not called again for that operation. + +```cpp +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "fake_http_transport.hpp" + +static std::int64_t now_ms() +{ + using namespace std::chrono; + return duration_cast(steady_clock::now().time_since_epoch()).count(); +} + +static void reset_dir(const std::filesystem::path& dir) +{ + std::error_code ec; + std::filesystem::remove_all(dir, ec); + std::filesystem::create_directories(dir, ec); +} + +static void run_example_3() +{ + using namespace vix::sync; + using namespace vix::sync::engine; + using namespace vix::sync::outbox; + + const std::filesystem::path dir = "./.vix_test_perm"; + reset_dir(dir); + + auto store = std::make_shared(FileOutboxStore::Config{ + .file_path = dir / "outbox.json", + .pretty_json = true, + .fsync_on_write = false + }); + + auto outbox = std::make_shared( + Outbox::Config{ .owner = "example-3" }, + store + ); + + auto probe = std::make_shared( + vix::net::NetworkProbe::Config{}, + [] { return true; } + ); + + auto transport = std::make_shared(); + transport->setRuleForTarget("/api/messages", FakeHttpTransport::Rule{ + .ok = false, + .retryable = false, + .error = "bad request (permanent)" + }); + + SyncEngine engine( + SyncEngine::Config{ .worker_count = 1, .batch_limit = 10 }, + outbox, probe, transport + ); + + Operation op; + op.kind = "http.post"; + op.target = "/api/messages"; + op.payload = R"({"text":"invalid data"})"; + + const auto t0 = now_ms(); + const auto id = outbox->enqueue(op, t0); + + engine.tick(t0); + + { + auto saved = store->get(id); + assert(saved.has_value()); + assert(saved->status == OperationStatus::PermanentFailed); + } + + const auto calls_after_first = transport->callCount(); + engine.tick(t0 + 10'000); + assert(transport->callCount() == calls_after_first); + + std::cout << "[example-3] OK: permanent failure is not retried\n"; +} + +int main() +{ + run_example_3(); + return 0; +} +``` + +--- + +## Example 4: Crash simulation (InFlight timeout requeue) + +This simulates a crash after claiming an operation but before completing it. + +What you should see: + +- The operation is InFlight. +- After inflight_timeout_ms, the engine requeues it. +- Next tick delivers it and marks Done. + +```cpp +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "fake_http_transport.hpp" + +static std::int64_t now_ms() +{ + using namespace std::chrono; + return duration_cast(steady_clock::now().time_since_epoch()).count(); +} + +static void reset_dir(const std::filesystem::path& dir) +{ + std::error_code ec; + std::filesystem::remove_all(dir, ec); + std::filesystem::create_directories(dir, ec); +} + +static void run_example_4() +{ + using namespace vix::sync; + using namespace vix::sync::engine; + using namespace vix::sync::outbox; + + const std::filesystem::path dir = "./.vix_test_inflight"; + reset_dir(dir); + + auto store = std::make_shared(FileOutboxStore::Config{ + .file_path = dir / "outbox.json", + .pretty_json = true, + .fsync_on_write = false + }); + + auto outbox = std::make_shared( + Outbox::Config{ .owner = "example-4" }, + store + ); + + auto probe = std::make_shared( + vix::net::NetworkProbe::Config{}, + [] { return true; } + ); + + auto transport = std::make_shared(); + transport->setDefault({ .ok = true }); + + SyncEngine::Config cfg; + cfg.worker_count = 1; + cfg.batch_limit = 10; + cfg.idle_sleep_ms = 0; + cfg.offline_sleep_ms = 0; + cfg.inflight_timeout_ms = 50; + + SyncEngine engine(cfg, outbox, probe, transport); + + Operation op; + op.kind = "http.post"; + op.target = "/api/messages"; + op.payload = R"({"text":"recover me"})"; + + const auto t0 = now_ms(); + const auto id = outbox->enqueue(op, t0); + + // Simulate crash: claim without completing. + const bool claimed = outbox->claim(id, t0); + assert(claimed); + + { + auto saved = store->get(id); + assert(saved.has_value()); + assert(saved->status == OperationStatus::InFlight); + } + + // Engine sees inflight timeout and requeues + const auto t1 = t0 + 60; + engine.tick(t1); + + // Next tick should deliver + engine.tick(t1 + 1); + + auto final = store->get(id); + assert(final.has_value()); + assert(final->status == OperationStatus::Done); + + std::cout << "[example-4] OK: inflight timeout requeues and completes\n"; +} + +int main() +{ + run_example_4(); + return 0; +} +``` + +--- + +## Beginner debugging checklist + +If an operation does not become Done: + +1. Open your outbox file (example: `./.vix_test_smoke/outbox.json`). +2. Check status, attempt, last_error, and next_retry_at_ms. +3. Confirm your NetworkProbe returns true (online). +4. Confirm your transport returns { ok=true }. +5. Confirm your engine tick uses a now_ms that is increasing. + +--- + +## Appendix: Minimal Fake Transport (copy-paste) + +Save this as `fake_http_transport.hpp` next to your `main.cpp`. + +```cpp +#ifndef VIX_FAKE_HTTP_TRANSPORT_HPP +#define VIX_FAKE_HTTP_TRANSPORT_HPP + +#include +#include + +#include + +namespace vix::sync::engine +{ + class FakeHttpTransport final : public ISyncTransport + { + public: + struct Rule + { + bool ok{true}; + bool retryable{true}; + std::string error{"simulated failure"}; + }; + + void setDefault(Rule r) { def_ = std::move(r); } + + void setRuleForKind(std::string kind, Rule r) + { + by_kind_[std::move(kind)] = std::move(r); + } + + void setRuleForTarget(std::string target, Rule r) + { + by_target_[std::move(target)] = std::move(r); + } + + std::size_t callCount() const noexcept { return calls_; } + + SendResult send(const vix::sync::Operation& op) override + { + ++calls_; + + if (auto it = by_target_.find(op.target); it != by_target_.end()) + return toResult(it->second); + + if (auto it = by_kind_.find(op.kind); it != by_kind_.end()) + return toResult(it->second); + + return toResult(def_); + } + + private: + static SendResult toResult(const Rule& r) + { + SendResult res; + res.ok = r.ok; + res.retryable = r.retryable; + res.error = r.ok ? "" : r.error; + return res; + } + + private: + Rule def_{}; + std::unordered_map by_kind_; + std::unordered_map by_target_; + std::size_t calls_{0}; + }; +} // namespace vix::sync::engine + +#endif +``` + diff --git a/docs/examples/tx-unit-of-work.md b/docs/examples/tx-unit-of-work.md new file mode 100644 index 0000000..1b3a739 --- /dev/null +++ b/docs/examples/tx-unit-of-work.md @@ -0,0 +1,220 @@ +# ORM Example Guide: Unit of Work (Transaction Pattern) + +This guide explains the `tx_unit_of_work` example step by step. + +Goal: - Create multiple related records (User + Order) - Ensure atomic +consistency - Commit everything together - Automatically rollback if +something fails + +This is a more structured and domain-oriented pattern compared to raw +Transaction usage. + +# 1. What is Unit of Work + +Unit of Work is a higher-level abstraction over transactions. + +It groups multiple operations that must succeed together. + +In this example: - Insert a user - Insert an order linked to that user - +Commit both at once + +If anything fails → everything is rolled back. + +# 2. Full Example Code + +``` cpp +#include + +#include +#include +#include + +using namespace vix::orm; + +int main(int argc, char **argv) +{ + const std::string host = (argc > 1 ? argv[1] : "tcp://127.0.0.1:3306"); + const std::string user = (argc > 2 ? argv[2] : "root"); + const std::string pass = (argc > 3 ? argv[3] : ""); + const std::string db = (argc > 4 ? argv[4] : "vixdb"); + + try + { + auto factory = make_mysql_factory(host, user, pass, db); + + PoolConfig cfg; + cfg.min = 1; + cfg.max = 8; + + ConnectionPool pool{factory, cfg}; + pool.warmup(); + + UnitOfWork uow{pool}; + auto &c = uow.conn(); + + { + auto st = c.prepare("INSERT INTO users(name,email,age) VALUES(?,?,?)"); + st->bind(1, "Alice"); + st->bind(2, "alice@example.com"); + st->bind(3, 27); + st->exec(); + } + + const std::uint64_t userId = c.lastInsertId(); + + { + auto st = c.prepare("INSERT INTO orders(user_id,total) VALUES(?,?)"); + st->bind(1, userId); + st->bind(2, 199.99); + st->exec(); + } + + uow.commit(); + std::cout << "[OK] user+order committed. user_id=" << userId << "\n"; + return 0; + } + catch (const std::exception &e) + { + std::cerr << "[ERR] " << e.what() << "\n"; + return 1; + } +} +``` + +# 3. Step by Step Explanation + +## 3.1 Create the factory and pool + +``` cpp +auto factory = make_mysql_factory(host, user, pass, db); +ConnectionPool pool{factory, cfg}; +pool.warmup(); +``` + +The pool manages reusable database connections. + +## 3.2 Create UnitOfWork + +``` cpp +UnitOfWork uow{pool}; +auto &c = uow.conn(); +``` + +What happens internally: + +- A connection is acquired from the pool +- A transaction begins +- All operations run on the same connection + +RAII rule: If `uow.commit()` is not called, rollback happens +automatically. + +## 3.3 Insert User + +``` cpp +auto st = c.prepare("INSERT INTO users(name,email,age) VALUES(?,?,?)"); +st->bind(1, "Alice"); +st->bind(2, "alice@example.com"); +st->bind(3, 27); +st->exec(); +``` + +Prepared statement: - Safe binding - No SQL injection - Reusable pattern + +## 3.4 Retrieve last insert id + +``` cpp +const std::uint64_t userId = c.lastInsertId(); +``` + +This retrieves the auto-generated primary key of the inserted user. + +Important: This works because: - We are still inside the same +transaction - We are using the same connection + +## 3.5 Insert Order linked to User + +``` cpp +auto st = c.prepare("INSERT INTO orders(user_id,total) VALUES(?,?)"); +st->bind(1, userId); +st->bind(2, 199.99); +st->exec(); +``` + +This creates a dependent record. + +If this fails: - The user insert will also be rolled back. + +## 3.6 Commit + +``` cpp +uow.commit(); +``` + +Commit makes both operations permanent. + +Without commit: - Everything is rolled back automatically. + +# 4. Required Schema + +``` sql +CREATE TABLE users ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + name VARCHAR(255) NOT NULL, + email VARCHAR(255) NOT NULL, + age INT NOT NULL +); + +CREATE TABLE orders ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + user_id BIGINT NOT NULL, + total DOUBLE NOT NULL, + FOREIGN KEY (user_id) REFERENCES users(id) +); +``` + +# 5. Why Use Unit of Work + +Use UnitOfWork when: + +- Multiple inserts must succeed together +- You create parent + child records +- You update multiple tables in one business operation +- You implement domain-level logic + +It is cleaner than manually handling transactions everywhere. + +# 6. Production Advice + +Keep unit of work small. + +Do not: + +- Call external APIs inside it +- Perform slow operations +- Hold locks for long time + +Do: + +- Keep DB operations grouped +- Commit quickly +- Handle errors properly + +# 7. Difference: Transaction vs UnitOfWork + +Transaction: - Lower level - Manual grouping + +UnitOfWork: - Domain-oriented - Business-logic grouping - Cleaner +architecture for complex systems + +# Summary + +You learned: + +- How to use UnitOfWork +- How to insert related records safely +- How to retrieve last insert ID +- How automatic rollback works +- Why this pattern is useful in production systems + + diff --git a/docs/examples/user_crud_with_validation.md b/docs/examples/user_crud_with_validation.md deleted file mode 100644 index ecc62f2..0000000 --- a/docs/examples/user_crud_with_validation.md +++ /dev/null @@ -1,248 +0,0 @@ -# Example — user_crud_with_validation - -```cpp -// ============================================================================ -// user_crud_with_validation.cpp — Full CRUD + Validation (Vix.cpp, nouvelle API) -// ---------------------------------------------------------------------------- -// Routes: -// POST /users → Create user (with validation) -// GET /users/{id} → Read user -// PUT /users/{id} → Update user -// DELETE /users/{id} → Delete user -// ============================================================================ - -#include // App, http, ResponseWrapper, etc. -#include // Vix::json::token, obj(), array() -#include // required(), num_range(), match(), validate_map -#include - -#include -#include -#include -#include -#include -#include - -using namespace vix; -namespace J = vix::json; -using njson = nlohmann::json; -using namespace vix::utils; - -// --------------------------- Data Model ------------------------------------- -struct User -{ - std::string id; - std::string name; - std::string email; - int age{}; -}; - -static std::mutex g_mtx; -static std::unordered_map g_users; - -// --------------------------- Helpers ---------------------------------------- -static J::kvs user_to_kvs(const User &u) -{ - return J::obj({"id", u.id, - "name", u.name, - "email", u.email, - "age", static_cast(u.age)}); -} - -static std::string to_string_safe(const njson &j) -{ - if (j.is_string()) - return j.get(); - if (j.is_number_integer()) - return std::to_string(j.get()); - if (j.is_number_unsigned()) - return std::to_string(j.get()); - if (j.is_number_float()) - return std::to_string(j.get()); - if (j.is_boolean()) - return j.get() ? "true" : "false"; - return {}; -} - -static bool parse_user(const njson &j, User &out) -{ - try - { - out.name = j.value("name", std::string{}); - out.email = j.value("email", std::string{}); - - if (j.contains("age")) - { - if (j["age"].is_string()) - out.age = std::stoi(j["age"].get()); - else if (j["age"].is_number_integer()) - out.age = static_cast(j["age"].get()); - else if (j["age"].is_number_unsigned()) - out.age = static_cast(j["age"].get()); - else if (j["age"].is_number_float()) - out.age = static_cast(j["age"].get()); - else - out.age = 0; - } - else - { - out.age = 0; - } - return true; - } - catch (...) - { - return false; - } -} - -static std::string gen_id_from_email(const std::string &email) -{ - const auto h = std::hash{}(email) & 0xFFFFFFull; - std::ostringstream oss; - oss << h; - return oss.str(); -} - -// --------------------------- Main ------------------------------------------- -int main() -{ - App app; - - // CREATE (POST /users) - app.post("/users", [](Request &req, Response &res) - { - njson body; - try { - body = njson::parse(req.body()); - } catch (...) { - res.status(http::status::bad_request).json({ - "error", "Invalid JSON" - }); - return; - } - - // Prépare les champs pour la validation (map) - std::unordered_map data{ - {"name", body.value("name", std::string{})}, - {"email", body.value("email", std::string{})}, - {"age", body.contains("age") ? to_string_safe(body["age"]) : std::string{}} - }; - - Schema sch{ - {"name", required("name")}, - {"age", num_range(1, 150, "Age")}, - {"email", match(R"(^[^@\s]+@[^@\s]+\.[^@\s]+$)", "Invalid email")} - }; - - auto r = validate_map(data, sch); - if (r.is_err()) { - // Construire {"errors": { field: message, ... }} SANS nlohmann - std::vector flat; - flat.reserve(r.error().size() * 2); - for (const auto& kv : r.error()) { - flat.emplace_back(kv.first); // clé - flat.emplace_back(kv.second); // valeur - } - - res.status(http::status::bad_request).json({ - "errors", J::obj(std::move(flat)) - }); - return; - } - - User u; - if (!parse_user(body, u)) { - res.status(http::status::bad_request).json({ - "error", "Invalid fields" - }); - return; - } - - u.id = gen_id_from_email(u.email); - - { - std::lock_guard lock(g_mtx); - g_users[u.id] = u; - } - - res.status(http::status::created).json({ - "status", "created", - "user", user_to_kvs(u) - }); }); - - // READ (GET /users/{id}) - app.get("/users/{id}", [](Request &req, Response &res) - { - const std::string id = req.param("id"); - std::lock_guard lock(g_mtx); - auto it = g_users.find(id); - if (it == g_users.end()) { - res.status(http::status::not_found).json({ - "error", "User not found" - }); - return; - } - res.json({ - "user", user_to_kvs(it->second) - }); }); - - // UPDATE (PUT /users/{id}) - app.put("/users/{id}", [](Request &req, Response &res) - { - const std::string id = req.param("id"); - - njson body; - try { - body = njson::parse(req.body()); - } catch (...) { - res.status(http::status::bad_request).json({ - "error", "Invalid JSON" - }); - return; - } - - std::lock_guard lock(g_mtx); - auto it = g_users.find(id); - if (it == g_users.end()) { - res.status(http::status::not_found).json({ - "error", "User not found" - }); - return; - } - - if (body.contains("name")) it->second.name = body["name"].get(); - if (body.contains("email")) it->second.email = body["email"].get(); - if (body.contains("age")) { - if (body["age"].is_string()) it->second.age = std::stoi(body["age"].get()); - else if (body["age"].is_number_integer()) it->second.age = static_cast(body["age"].get()); - else if (body["age"].is_number_unsigned()) it->second.age = static_cast(body["age"].get()); - else if (body["age"].is_number_float()) it->second.age = static_cast(body["age"].get()); - } - - res.json({ - "status", "updated", - "user", user_to_kvs(it->second) - }); }); - - // DELETE (DELETE /users/{id}) - app.del("/users/{id}", [](Request &req, Response &res) - { - const std::string id = req.param("id"); - std::lock_guard lock(g_mtx); - const auto n = g_users.erase(id); - if (!n) { - res.status(http::status::not_found).json({ - "error", "User not found" - }); - return; - } - res.json({ - "status", "deleted", - "user_id", id - }); }); - - // Lancement - app.run(8080); -} -``` diff --git a/docs/examples/users-crud.md b/docs/examples/users-crud.md new file mode 100644 index 0000000..4aba16d --- /dev/null +++ b/docs/examples/users-crud.md @@ -0,0 +1,370 @@ + +# Users CRUD over HTTP (ORM + Repository) + +This page shows a minimal HTTP CRUD API for a `users` table using: + +- Vix.cpp HTTP server (`App`) +- Vix ORM (`ConnectionPool`, `BaseRepository`, `Mapper`) +- JSON request and JSON responses +- One simple auth layer: API key on `/api/` + +Rules of this doc: - one concept - one minimal main() - quick curl tests + +## What you get + +Routes (all under `/api`): + +- POST `/api/users` create user +- GET `/api/users/:id` read user +- PUT `/api/users/:id` update user +- DELETE `/api/users/:id` delete user + +Auth: - Requires header `x-api-key: dev_key_123` for all `/api/*` + +## Database schema + +Create the table first: + +``` sql +CREATE TABLE IF NOT EXISTS users ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + name VARCHAR(255) NOT NULL, + email VARCHAR(255) NOT NULL, + age INT NOT NULL +); +``` + +Recommended in production: + +``` sql +CREATE UNIQUE INDEX users_email_uq ON users(email); +``` + +## Single file example: users_crud_http.cpp + +Save as `users_crud_http.cpp`: + +``` cpp +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +using namespace vix; +using namespace vix::orm; +namespace J = vix::json; + +// Model +struct User +{ + std::int64_t id{}; + std::string name; + std::string email; + int age{}; +}; + +// Mapper +namespace vix::orm +{ + template <> + struct Mapper + { + static User fromRow(const ResultRow &row) + { + User u{}; + u.id = row.getInt64Or(0, 0); + u.name = row.getStringOr(1, ""); + u.email = row.getStringOr(2, ""); + u.age = static_cast(row.getInt64Or(3, 0)); + return u; + } + + static std::vector> toInsertParams(const User &u) + { + return { {"name", u.name}, {"email", u.email}, {"age", u.age}}; + } + + static std::vector> toUpdateParams(const User &u) + { + return { {"name", u.name}, {"email", u.email}, {"age", u.age}}; + } + }; +} // namespace vix::orm + +// Simple JSON helpers (Simple.hpp) +static J::kvs user_to_kvs(const User &u) +{ + using J::obj; + return obj({ + "id", + u.id, + "name", + u.name, + "email", + u.email, + "age", + u.age, + }); +} + +static void json_send(Response &res, const J::kvs &payload) +{ + // convert Simple -> nlohmann::json + res.json(J::to_json(payload)); +} + +static void json_send(Response &res, int status, const J::kvs &payload) +{ + res.status(status); + // convert Simple -> nlohmann::json + res.json(J::to_json(payload)); +} + +// Parse helpers (nlohmann::json) +static std::optional json_string(const J::Json &body, const char *k) +{ + if (!body.is_object() || !body.contains(k) || !body[k].is_string()) + return std::nullopt; + return body[k].get(); +} + +static std::optional json_i64(const J::Json &body, const char *k) +{ + if (!body.is_object() || !body.contains(k)) + return std::nullopt; + + if (body[k].is_number_integer()) + return static_cast(body[k].get()); + + return std::nullopt; +} + +static void register_routes(App &app, ConnectionPool &pool) +{ + auto repo = std::make_shared>(pool, "users"); + + app.get("/", [](Request &, Response &res) + { res.send( + "Users CRUD HTTP example:\n" + " POST /api/users\n" + " GET /api/users/{id}\n" + " PUT /api/users/{id}\n" + " DELETE /api/users/{id}\n" + "All /api requires: x-api-key: dev_key_123\n"); }); + + // Create + app.post("/api/users", [repo](Request &req, Response &res) + { + const auto body = req.json(); + + const auto name = json_string(body, "name"); + const auto email = json_string(body, "email"); + const auto age64 = json_i64(body, "age"); + + if (!name || !email || !age64) + { + json_send(res, 400, J::obj({ + "ok", false, + "error", "bad_request", + "hint", "expected JSON: {name:string, email:string, age:int}" + })); + return; + } + + User u{}; + u.name = *name; + u.email = *email; + u.age = static_cast(*age64); + + u.id = static_cast(repo->create(u)); + + json_send(res, 201, J::obj({ + "ok", true, + "data", user_to_kvs(u) + })); }); + + // Read + app.get("/api/users/{id}", [repo](Request &req, Response &res) + { + const auto id = std::stoll(req.param("id")); + + auto u = repo->findById(static_cast(id)); + if (!u) + { + json_send(res, 404, J::obj({ + "ok", false, + "error", "not_found" + })); + return; + } + + json_send(res, J::obj({ + "ok", true, + "data", user_to_kvs(*u) + })); }); + + // Update + app.put("/api/users/{id}", [repo](Request &req, Response &res) + { + const auto id = std::stoll(req.param("id")); + const auto body = req.json(); + + const auto name = json_string(body, "name"); + const auto email = json_string(body, "email"); + const auto age64 = json_i64(body, "age"); + + if (!name || !email || !age64) + { + json_send(res, 400, J::obj({ + "ok", false, + "error", "bad_request", + "hint", "expected JSON: {name:string, email:string, age:int}" + })); + return; + } + + User u{}; + u.id = static_cast(id); + u.name = *name; + u.email = *email; + u.age = static_cast(*age64); + + const auto affected = repo->updateById(u.id, u); + if (affected == 0) + { + json_send(res, 404, J::obj({ + "ok", false, + "error", "not_found" + })); + return; + } + + json_send(res, J::obj({ + "ok", true, + "affected", static_cast(affected), + "data", user_to_kvs(u) + })); }); + + // Delete + app.del("/api/users/{id}", [repo](Request &req, Response &res) + { + const auto id = std::stoll(req.param("id")); + + const auto affected = repo->removeById(static_cast(id)); + if (affected == 0) + { + json_send(res, 404, J::obj({ + "ok", false, + "error", "not_found" + })); + return; + } + + json_send(res, J::obj({ + "ok", true, + "affected", static_cast(affected) + })); }); +} + +int main() +{ + try + { + // DB + auto factory = make_mysql_factory("tcp://127.0.0.1:3306", "root", "", "vixdb"); + + PoolConfig pcfg; + pcfg.min = 1; + pcfg.max = 8; + + ConnectionPool pool{factory, pcfg}; + pool.warmup(); + + // HTTP + App app; + + middleware::app::install(app, "/api/", middleware::app::api_key_dev("dev_key_123")); + + register_routes(app, pool); + + app.run(8080); + return 0; + } + catch (const DBError &e) + { + std::cerr << "[DBError] " << e.what() << "\n"; + return 1; + } + catch (const std::exception &e) + { + std::cerr << "[ERR] " << e.what() << "\n"; + return 1; + } +} +``` + +## Run + +``` bash +vix run users_crud_http.cpp +``` + +## Try with curl + +Set the key once: + +``` bash +K="x-api-key: dev_key_123" +``` + +Create: + +``` bash +curl -i -H "$K" -H "Content-Type: application/json" \ + -d '{"name":"Bob","email":"bob@example.com","age":30}' \ + http://127.0.0.1:8080/api/users +``` + +Read (replace 1): + +``` bash +curl -i -H "$K" http://127.0.0.1:8080/api/users/1 +``` + +Update (replace 1): + +``` bash +curl -i -X PUT -H "$K" -H "Content-Type: application/json" \ + -d '{"name":"Bob Updated","email":"bob@example.com","age":31}' \ + http://127.0.0.1:8080/api/users/1 +``` + +Delete (replace 1): + +``` bash +curl -i -X DELETE -H "$K" http://127.0.0.1:8080/api/users/1 +``` + +Missing key (should fail): + +``` bash +curl -i http://127.0.0.1:8080/api/users/1 +``` + +## What this teaches + +- A clean ORM mapping (`Mapper`) +- A stable data layer (`BaseRepository`) +- A simple pool for scalability (`ConnectionPool`) +- Minimal HTTP CRUD with predictable JSON +- Prefix middleware install for API protection diff --git a/docs/examples/validation.md b/docs/examples/validation.md new file mode 100644 index 0000000..2245c06 --- /dev/null +++ b/docs/examples/validation.md @@ -0,0 +1,254 @@ +# Request Validation (HTTP) + +This example shows how to use `vix::validation` inside HTTP routes. + +Each section: - minimal `main()` - validation inside a route - +structured JSON error response + +------------------------------------------------------------------------ + +## 1) Validate JSON body (POST /register) + +Validates a JSON payload using `BaseModel`. + +``` cpp +#include +#include +#include + +using namespace vix; +namespace J = vix::json; + +struct RegisterForm : vix::validation::BaseModel +{ + std::string email; + std::string password; + + static vix::validation::Schema schema() + { + using namespace vix::validation; + + return schema() + .field("email", &RegisterForm::email, + field().required().email()) + .field("password", &RegisterForm::password, + field().required().length_min(8)); + } +}; + +int main() +{ + App app; + + app.post("/register", [](Request& req, Response& res) + { + RegisterForm form; + + const auto& j = req.json(); + if (j.contains("email")) form.email = j["email"].get(); + if (j.contains("password")) form.password = j["password"].get(); + + auto result = form.validate(); + + if (!result.ok()) + { + J::array errors; + for (const auto& e : result.errors.all()) + { + errors.push_back(J::obj({ + "field", e.field, + "message", e.message + })); + } + + res.status(400).json({ + "ok", false, + "errors", errors + }); + return; + } + + res.json({ + "ok", true, + "message", "User registered" + }); + }); + + app.run(8080); + return 0; +} +``` + +Test: +```bash + curl -X POST http://localhost:8080/register -H "Content-Type: application/json" -d '{"email":"bad","password":"123"}' +``` +------------------------------------------------------------------------ + +## 2) Validate query parameters (GET /search) + +Validate query inputs using `validate_parsed`. + +``` cpp +#include +#include + +using namespace vix; +using namespace vix::validation; + +int main() +{ + App app; + + app.get("/search", [](Request& req, Response& res) + { + auto page = validate_parsed("page", + req.query_value("page", "1")) + .min(1) + .max(100) + .result("page must be a number"); + + if (!page.ok()) + { + res.status(400).json({ + "ok", false, + "error", page.errors.all().front().message + }); + return; + } + + res.json({ + "ok", true, + "page", req.query_value("page", "1") + }); + }); + + app.run(8080); + return 0; +} +``` + +Test: + + curl "http://localhost:8080/search?page=abc" + +------------------------------------------------------------------------ + +## 3) Validate path parameter (GET /users/{id}) + +``` cpp +#include +#include + +using namespace vix; +using namespace vix::validation; + +int main() +{ + App app; + + app.get("/users/{id}", [](Request& req, Response& res) + { + auto id = validate_parsed("id", req.param("id")) + .min(1) + .result("id must be a positive number"); + + if (!id.ok()) + { + res.status(400).json({ + "ok", false, + "error", id.errors.all().front().message + }); + return; + } + + res.json({ + "ok", true, + "id", req.param("id") + }); + }); + + app.run(8080); + return 0; +} +``` + +------------------------------------------------------------------------ + +## 4) Form-style validation (POST /login) + +Use `Form` when binding raw input. + +``` cpp +#include +#include +#include +#include + +using namespace vix; + +struct LoginForm : vix::validation::BaseModel +{ + std::string email; + std::string password; + + static vix::validation::Schema schema() + { + using namespace vix::validation; + + return schema() + .field("email", &LoginForm::email, + field().required().email()) + .field("password", &LoginForm::password, + field().required().length_min(6)); + } +}; + +int main() +{ + App app; + + app.post("/login", [](Request& req, Response& res) + { + using Input = std::vector>; + + Input input = { + {"email", req.query_value("email")}, + {"password", req.query_value("password")} + }; + + auto r = vix::validation::Form::validate(input); + + if (!r) + { + res.status(400).json({ + "ok", false, + "error", r.errors().all().front().message + }); + return; + } + + res.json({ + "ok", true, + "message", "Login valid" + }); + }); + + app.run(8080); + return 0; +} +``` + +------------------------------------------------------------------------ + +## Pattern Summary + +Inside HTTP routes: + +- Bind request data +- Call validation +- If invalid → return 400 with structured errors +- If valid → continue business logic + +This keeps routing clean and validation explicit. + diff --git a/docs/examples/wal-recovery.md b/docs/examples/wal-recovery.md new file mode 100644 index 0000000..7849a62 --- /dev/null +++ b/docs/examples/wal-recovery.md @@ -0,0 +1,284 @@ +# WAL Recovery + +This page shows how to recover state using the Vix.cpp sync WAL (Write-Ahead Log). + +Goal: +- Persist state transitions before any external effect +- Rebuild state deterministically after crash or restart +- Resume safely from an offset (checkpoint) + +This guide is written for both: +- Beginners who want a copy-paste recovery flow +- Experts who want deterministic replay and offset discipline + +--- + +## What is WAL recovery + +A WAL is an append-only log of state transitions. Recovery means: +1) Open the WAL file +2) Replay records in strict append order +3) Rebuild in-memory state (Outbox, indexes, counters, etc.) +4) Continue processing from a known offset + +A key invariant: +- Every transition that matters is appended before side effects. + +If the process crashes, you can replay the WAL and reconstruct the last correct state. + +--- + +## Headers + +```cpp +#include +#include +``` + +Namespace: +```cpp +vix::sync::wal +``` + +--- + +## WAL record types + +WAL records describe durable transitions. Typical record types: + +- PutOperation: operation was created and persisted +- MarkDone: operation completed successfully +- MarkFailed: operation failed (retryable or permanent, depending on stored fields and policy) + +A common recovery strategy: +- Start with empty state +- Apply records in order +- Last record for a given operation id wins + +--- + +## 1) Minimal recovery example (rebuild a map of operations) + +This is the simplest possible recovery: we rebuild a map keyed by operation id. + +What you learn: +- How to replay from offset 0 +- How to apply records deterministically + +```cpp +#include +#include +#include +#include + +#include +#include + +using vix::sync::wal::RecordType; +using vix::sync::wal::Wal; +using vix::sync::wal::WalRecord; + +struct RecoveredOp +{ + std::string id; + RecordType last_type{RecordType::PutOperation}; + std::string last_error; + std::int64_t next_retry_at_ms{0}; +}; + +int main() +{ + std::unordered_map ops; + + Wal wal(Wal::Config{ "./.vix/wal.log", false }); + + wal.replay(0, [&](const WalRecord& rec) + { + auto& o = ops[rec.id]; + o.id = rec.id; + o.last_type = rec.type; + + if (rec.type == RecordType::MarkFailed) + { + o.last_error = rec.error; + o.next_retry_at_ms = rec.next_retry_at_ms; + } + else + { + o.last_error.clear(); + o.next_retry_at_ms = 0; + } + }); + + std::cout << "Recovered ops: " << ops.size() << "\n"; + return 0; +} +``` + +When to use this pattern: +- When you need a minimal reconstruction step before loading higher-level components +- When you want to validate WAL correctness quickly + +--- + +## 2) WAL offsets and checkpoints + +WAL replay can be large. You should store a checkpoint offset. + +Concept: +- last_applied_offset is the WAL byte offset you have fully processed +- On restart you replay from last_applied_offset, not from 0 + +Typical places to store the checkpoint: +- A small file: .vix/wal.offset +- A local database table (if you already use SQLite) +- A config store + +Minimal checkpoint file example: + +```cpp +#include +#include +#include + +static std::int64_t load_checkpoint(const std::string& path) +{ + std::ifstream in(path); + std::int64_t off = 0; + if (in.good()) + in >> off; + return off; +} + +static void save_checkpoint(const std::string& path, std::int64_t off) +{ + std::ofstream out(path, std::ios::trunc); + out << off; +} +``` + +Important rule: +- Only save the checkpoint after you have fully applied all records up to that offset. + +--- + +## 3) Practical recovery: rebuild Outbox from WAL + +In many offline-first designs: +- The WAL is the source of truth +- The Outbox is reconstructed from WAL at startup + +High-level approach: +1) Replay WAL records +2) For PutOperation: insert or update operation data +3) For MarkDone: mark operation done +4) For MarkFailed: mark operation failed and set next retry time + +Pseudo-flow: + +```cpp +wal.replay(from, [&](const WalRecord& rec) +{ + switch (rec.type) + { + case RecordType::PutOperation: + // decode payload into Operation, then store it + break; + + case RecordType::MarkDone: + // mark operation done + break; + + case RecordType::MarkFailed: + // mark failed, store error and next retry time + break; + } +}); +``` + +Note: +- WAL payload is typically a serialized Operation for PutOperation. +- During recovery, decode and rebuild the Outbox state you need. + +--- + +## 4) How to test WAL recovery (beginner-friendly) + +### A) Create a test directory + +```bash +mkdir -p .vix_test +``` + +### B) Run a small program that appends records + +Write a tiny program that: +- Creates Wal at .vix_test/wal.log +- Appends a PutOperation +- Appends MarkFailed +- Exits + +Then run the recovery program and verify that it reports 1 recovered op, last_type MarkFailed, and the error message. + +### C) Re-run recovery multiple times + +Recovery must be deterministic. Running the recovery program twice should give the same result. + +--- + +## 5) Crash simulation test (append then crash) + +A classic test: +1) Append PutOperation +2) Start sending (simulate by printing) +3) Crash before MarkDone +4) Restart and recover +5) You must see the operation as not done, and eligible for retry if policy says so + +Minimal example: + +```cpp +// Process: +// - PutOperation appended +// - program exits before MarkDone +// On restart, recovery sees the operation as pending and can retry. +``` + +This test proves: +- Local accepted work is not lost +- Recovery can safely resume + +--- + +## 6) Common mistakes + +### Saving checkpoint too early +If you store last_applied_offset before applying the record, you can skip it on restart. + +Rule: +- Apply record first, then persist checkpoint. + +### Non-deterministic application +Avoid reading current time during replay decisions. Replay should be pure. +If you need time, store it in the record. + +### Mixing WAL and non-WAL writes +If you mutate external state without WAL append first, recovery becomes ambiguous. + +--- + +## 7) Recommended structure in real systems + +A robust boot sequence: + +1) Load checkpoint offset +2) Replay WAL from checkpoint +3) Rebuild Outbox and indexes +4) Requeue inflight operations older than timeout +5) Start SyncEngine or schedule ticks + +That sequence aligns with offline-first invariants: +- durable local state +- deterministic recovery +- safe convergence after failures + diff --git a/docs/examples/ws-chat.md b/docs/examples/ws-chat.md new file mode 100644 index 0000000..db3930d --- /dev/null +++ b/docs/examples/ws-chat.md @@ -0,0 +1,181 @@ +# WebSocket Chat Example + +This example shows how to build a minimal real-time chat server using +Vix.cpp. + +It is designed for: + +- C++ beginners learning WebSocket +- Backend developers building real-time systems +- Production-ready typed message handling + +## 1. Goal + +We will build: + +- One HTTP route: `/health` +- One WebSocket endpoint +- A simple typed protocol: { "type": "chat.message", "payload": { ... + } } +- Broadcast messages to all connected clients + +## 2. Minimal Chat Server (Single File) + +Create: + +main.cpp + +``` cpp +#include +#include + +using namespace vix; + +int main() +{ + vix::serve_http_and_ws([](App& app, auto& ws) + { + // HTTP route + app.get("/health", [](Request&, Response& res) + { + res.json({ + "ok", true, + "service", "chat" + }); + }); + + // WebSocket events + + ws.on_open([&ws](auto&) + { + ws.broadcast_json("chat.system", { + "text", "A user connected" + }); + }); + + ws.on_close([&ws](auto&) + { + ws.broadcast_json("chat.system", { + "text", "A user disconnected" + }); + }); + + ws.on_typed_message( + [&ws](auto&, const std::string& type, + const vix::json::kvs& payload) + { + if (type == "chat.message") + { + ws.broadcast_json("chat.message", payload); + return; + } + + ws.broadcast_json("chat.unknown", { + "type", type + }); + }); + }); + + return 0; +} +``` + +## 3. How To Run + +From your project directory: + +``` bash +vix run main.cpp +``` + +You should see the server listening on port 8080. + +## 4. Test HTTP Route + +``` bash +curl http://127.0.0.1:8080/health +``` + +Expected output: + +``` json +{ + "ok": true, + "service": "chat" +} +``` + +## 5. Test WebSocket (Beginner Method) + +Install websocat if needed: + +Linux: + +``` bash +sudo apt install websocat +``` + +Then connect: + +``` bash +websocat ws://127.0.0.1:8080/ +``` + +Now send: + +``` json +{"type":"chat.message","payload":{"user":"Alice","text":"Hello!"}} +``` + +Open two terminals running websocat to see broadcast working. + +## 6. Message Protocol + +Every WebSocket message follows this structure: + +``` json +{ + "type": "chat.message", + "payload": { + "user": "Alice", + "text": "Hello" + } +} +``` + +Rules: + +- type defines the event +- payload contains the data +- Server decides how to route based on type + +## 7. Recommended Structure For Real Projects + +Instead of a single file: + +src/ main.cpp chat/ ChatHandler.hpp ChatHandler.cpp + +Keep WebSocket logic separate from HTTP routes. + +## 8. Common Extensions + +You can extend this example with: + +- Authentication before joining chat +- Rooms (ws.join(room)) +- Private messaging +- Presence tracking +- Database persistence +- Rate limiting +- Message validation + +## 9. Key Concepts Learned + +- How to start HTTP + WebSocket in Vix +- How to broadcast messages +- How typed protocols work +- How to test real-time systems quickly +- How to structure C++ WebSocket backends + +This example is intentionally minimal but production-ready in structure. + diff --git a/docs/fundamentals/errors.md b/docs/fundamentals/errors.md new file mode 100644 index 0000000..04ce981 --- /dev/null +++ b/docs/fundamentals/errors.md @@ -0,0 +1,183 @@ +# Errors + +Error handling in Vix is explicit. + +There is no hidden exception translation layer. You decide: + +- The status code +- The error structure +- The message format + +------------------------------------------------------------------------ + +## Basic error response + +``` cpp +#include + +using namespace vix; + +int main() +{ + App app; + + app.get("/error", [](Request&, Response& res) { + res.status(400).json({ + "error", "Bad request" + }); + }); + + app.run(8080); +} +``` + +------------------------------------------------------------------------ + +## 404 example + +``` cpp +#include +#include + +using namespace vix; + +int main() +{ + App app; + + app.get("/users/{id}", [](Request& req, Response& res) { + const std::string id = req.param("id", "0"); + + if (id == "0") + { + res.status(404).json({ + "error", "User not found", + "id", id + }); + return; + } + + res.json({ + "id", id, + "name", "User#" + id + }); + }); + + app.run(8080); +} +``` + +------------------------------------------------------------------------ + +## Validation error + +``` cpp +#include +#include + +using namespace vix; + +int main() +{ + App app; + + app.get("/validate", [](Request& req, Response& res) { + const std::string email = req.query_value("email", ""); + + if (email.find('@') == std::string::npos) + { + res.status(400).json({ + "error", "Invalid email", + "field", "email" + }); + return; + } + + res.json({ + "ok", true, + "email", email + }); + }); + + app.run(8080); +} +``` + +------------------------------------------------------------------------ + +## Internal server error pattern + +``` cpp +#include + +using namespace vix; + +int main() +{ + App app; + + app.get("/internal", [](Request&, Response& res) { + // Simulated failure + bool failure = true; + + if (failure) + { + res.status(500).json({ + "error", "Internal server error" + }); + return; + } + + res.json({"ok", true}); + }); + + app.run(8080); +} +``` + +------------------------------------------------------------------------ + +## Error structure consistency + +For real applications, keep a consistent format: + +``` json +{ + "error": "message", + "code": 400, + "details": {} +} +``` + +Example: + +``` cpp +res.status(400).json({ + "error", "Invalid input", + "code", 400 +}); +``` + +------------------------------------------------------------------------ + +## Important rules + +- Always set an appropriate status code. +- Always return a clear error message. +- Avoid sending partial responses. +- Send the response once. + +------------------------------------------------------------------------ + +## Philosophy + +Vix does not decide what an error looks like. + +You do. + +That makes the system: + +- Predictable +- Transparent +- Production-safe + diff --git a/docs/fundamentals/index.md b/docs/fundamentals/index.md new file mode 100644 index 0000000..946e0f0 --- /dev/null +++ b/docs/fundamentals/index.md @@ -0,0 +1,118 @@ +# Fundamentals + +This section explains the core building blocks of Vix. + +Before building larger systems, you should understand: + +- The App runtime +- The Request object +- The Response object +- The routing model +- The execution flow + +Everything in Vix is explicit and predictable. + +------------------------------------------------------------------------ + +## The Runtime + +Every Vix application starts with: + +``` cpp +App app; +``` + +This creates the HTTP runtime. + +Routes are registered on the `app` instance. + +The server starts with: + +``` cpp +app.run(8080); +``` + +------------------------------------------------------------------------ + +## The Execution Model + +When a request arrives: + +1. The route is matched. +2. The handler lambda is executed. +3. You control the response. +4. The connection is finalized. + +There is no hidden magic layer. + +------------------------------------------------------------------------ + +## The Request Object + +Inside handlers: + +``` cpp +app.get("/", [](Request& req, Response& res) { + // req gives you access to: + // - path params + // - query params + // - headers + // - body +}); +``` + +Request is read-only. It represents incoming data. + +------------------------------------------------------------------------ + +## The Response Object + +Response is fully controlled by you. + +You decide: + +``` cpp +res.status(200); +res.set_header("X-App", "Vix"); +res.send("text"); +res.json({"ok", true}); +``` + +Nothing is sent unless you explicitly send it or return a payload that +auto-sends. + +------------------------------------------------------------------------ + +## Minimal complete example + +``` cpp +#include + +using namespace vix; + +int main() +{ + App app; + + app.get("/", [](Request&, Response& res) { + res.json({"message", "Fundamentals"}); + }); + + app.run(8080); +} +``` + +------------------------------------------------------------------------ + +## Design Principles + +Vix fundamentals are built on: + +- Explicit behavior +- Predictable lifecycle +- No implicit middleware +- No runtime reflection +- No hidden state machines + +The goal is clarity and performance. + diff --git a/docs/fundamentals/lifecycle.md b/docs/fundamentals/lifecycle.md new file mode 100644 index 0000000..704e8cd --- /dev/null +++ b/docs/fundamentals/lifecycle.md @@ -0,0 +1,144 @@ +# Lifecycle + +Understanding the request lifecycle is essential to using Vix correctly. + +Vix keeps the lifecycle simple and explicit. + +------------------------------------------------------------------------ + +## High-level flow + +For every incoming HTTP request: + +1. The runtime accepts the connection. +2. The route is matched. +3. Your handler lambda executes. +4. You build and send the response. +5. The connection is finalized. + +Nothing happens outside of this flow. + +------------------------------------------------------------------------ + +## Minimal example + +``` cpp +#include + +using namespace vix; + +int main() +{ + App app; + + app.get("/", [](Request&, Response& res) { + res.json({"message", "Lifecycle example"}); + }); + + app.run(8080); +} +``` + +When a client calls `/`: + +- The route `/` is matched. +- The lambda runs. +- `res.json(...)` sends the response. +- The request ends. + +------------------------------------------------------------------------ + +## Step-by-step breakdown + +### 1. Route matching + +When a request arrives, Vix checks the HTTP method and path. + +Example: + +``` cpp +app.get("/users/{id}", [](Request& req, Response& res) { + const std::string id = req.param("id", "0"); + res.json({"id", id}); +}); +``` + +If the request is: + + GET /users/42 + +The route matches and `id` becomes `"42"`. + +------------------------------------------------------------------------ + +### 2. Handler execution + +Your lambda is executed synchronously. + +Inside it you can: + +- Read request headers +- Read query parameters +- Read body +- Parse JSON +- Set status +- Set headers +- Send response + +You control everything. + +------------------------------------------------------------------------ + +### 3. Response finalization + +A response is finalized when: + +- You call `res.send()` +- You call `res.json()` +- Or you return a value that auto-sends + +Example: + +``` cpp +app.get("/text", [](const Request&, Response&) { + return "Auto-sent response"; +}); +``` + +If you explicitly send a response: + +``` cpp +app.get("/mix", [](Request&, Response& res) { + res.send("Explicit send"); + return "ignored"; +}); +``` + +The returned value is ignored. + +------------------------------------------------------------------------ + +## Important rules + +- A response should only be sent once. +- After sending, additional modifications are ignored. +- If nothing is sent, behavior depends on runtime defaults. + +Best practice: always explicitly send a response. + +------------------------------------------------------------------------ + +## Lifecycle philosophy + +Vix avoids: + +- Hidden middleware chains +- Implicit async behavior +- Magic auto transformations + +The lifecycle is predictable: + +Request → Handler → Response → End + +That simplicity is intentional. + diff --git a/docs/fundamentals/logging.md b/docs/fundamentals/logging.md new file mode 100644 index 0000000..de641fb --- /dev/null +++ b/docs/fundamentals/logging.md @@ -0,0 +1,187 @@ +# Logging (Core) + +Logging in Vix is **explicit and layered**. + +Vix does not automatically log every request. You decide: + +- What to log +- When to log +- Whether it is **dev output** (`vix::console`) +- Or **production logging** (`vix::utils::Logger`) + +Vix intentionally separates: + +- **Console** → developer-facing runtime output +- **Logger** → structured, production-grade logging + +--- + +## 1. Development logging (vix::console) + +For quick debugging and local feedback, use: + +```cpp +#include + +using namespace vix; + +int main() +{ + App app; + + app.get("/", [](Request&, Response& res) { + console.info("Incoming request on /"); + res.json({"message", "Hello"}); + }); + + app.run(8080); +} +``` + +### Why use console instead of std::cout? + +- Thread-safe atomic lines +- Level filtering (`debug` off by default) +- Proper stdout/stderr routing +- Near-zero cost when filtered +- Environment-controlled verbosity + +Console is designed for **developer ergonomics**, not ingestion pipelines. + +--- + +## 2. Logging request data + +```cpp +app.get("/inspect", [](Request& req, Response& res) { + + console.debug("Path:", "/inspect"); + console.debug("User-Agent:", req.header("User-Agent")); + + res.json({"ok", true}); +}); +``` + +Be careful not to log: + +- Authorization headers +- Tokens +- Passwords +- Personal data + +--- + +## 3. Logging errors (dev mode) + +```cpp +app.get("/error", [](Request&, Response& res) { + + console.error("Simulated error occurred"); + + res.status(500).json({ + "error", "Internal server error" + }); +}); +``` + +Console automatically routes: + +- `warn` / `error` → stderr +- `log` / `info` / `debug` → stdout + +--- + +## 4. Production logging (vix::utils::Logger) + +For real systems, do **not** rely on console. + +Use `vix::utils::Logger` for: + +- Structured logs (JSON / key-value) +- File outputs +- Ingestion pipelines +- Production observability +- Rotating logs +- Persistent storage + +Example (illustrative): + +```cpp +#include + +using namespace vix::utils; + +int main() +{ + Logger logger; + + logger.info({ + {"event", "request_received"}, + {"path", "/inspect"} + }); +} +``` + +Console and Logger serve different purposes by design. + +--- + +## 5. Structured logging pattern + +Instead of: + +```cpp +console.info("User login failed"); +``` + +Prefer structured logging in production: + +```cpp +logger.warn({ + {"event", "login_failed"}, + {"user", "unknown"} +}); +``` + +Structured logs are easier to: + +- Parse +- Filter +- Aggregate +- Send to ELK / Loki / OpenTelemetry + +--- + +## 6. Performance considerations + +Logging is I/O. + +Best practices: + +- Do not log inside hot loops +- Avoid per-request heavy logs under high load +- Keep debug logs disabled in production +- Use sampling if traffic is extreme +- Avoid blocking file I/O in request threads + +Remember: + +- Console is cheap when filtered +- Logger is more powerful but must be configured responsibly + +--- + +## 7. Philosophy + +Logging in Vix is: + +- Explicit +- Layered +- Predictable +- Performance-aware + +Nothing is logged unless you log it. + +Console is for developers. +Logger is for systems. + diff --git a/docs/fundamentals/performance.md b/docs/fundamentals/performance.md new file mode 100644 index 0000000..d53604d --- /dev/null +++ b/docs/fundamentals/performance.md @@ -0,0 +1,149 @@ +# Performance + +Vix is designed for predictable, high-performance networking. + +Performance in Vix comes from: + +- Explicit control +- Minimal abstraction layers +- No garbage collection +- No hidden middleware chains + +------------------------------------------------------------------------ + +## Minimal overhead model + +A typical Vix request flow is: + +Request → Route match → Handler → Response → End + +There are no: + +- Reflection systems +- Dynamic runtime inspection layers +- Hidden serialization pipelines + +Everything you write is what executes. + +------------------------------------------------------------------------ + +## Minimal example + +``` cpp +#include + +using namespace vix; + +int main() +{ + App app; + + app.get("/", [](Request&, Response& res) { + res.json({"message", "Fast and explicit"}); + }); + + app.run(8080); +} +``` + +The handler is a simple lambda. No extra runtime layers are involved. + +------------------------------------------------------------------------ + +## Avoiding performance pitfalls + +### 1. Avoid heavy allocations in hot paths + +Instead of: + +``` cpp +std::string large = some_expensive_operation(); +res.json({"data", large}); +``` + +Prefer precomputed or cached data when possible. + +------------------------------------------------------------------------ + +### 2. Keep handlers small + +Handlers should: + +- Parse input +- Perform minimal logic +- Send response + +Move heavy logic into dedicated components. + +------------------------------------------------------------------------ + +### 3. Avoid unnecessary JSON depth + +Deeply nested objects increase serialization cost. + +Prefer structured but reasonable JSON layouts. + +------------------------------------------------------------------------ + +## Hardware considerations + +For production workloads: + +- Use multi-core CPUs +- Use fast storage (NVMe preferred) +- Avoid memory pressure +- Use proper OS tuning + +Vix itself does not impose artificial limits. Hardware and architecture +choices matter. + +------------------------------------------------------------------------ + +## Throughput mindset + +To maximize throughput: + +- Minimize blocking operations +- Avoid unnecessary copying +- Reuse resources when possible +- Keep I/O efficient + +Example of lightweight route: + +``` cpp +app.get("/ping", [](Request&, Response& res) { + res.json({"message", "pong"}); +}); +``` + +This route has near-zero overhead. + +------------------------------------------------------------------------ + +## Benchmarking rule + +Always benchmark your real workload. + +Synthetic benchmarks may not reflect: + +- Real traffic patterns +- Database latency +- External API delays + +Measure what matters. + +------------------------------------------------------------------------ + +## Philosophy + +Performance in Vix is not magic. + +It comes from: + +- Simplicity +- Explicit behavior +- Predictable execution +- Modern C++ efficiency + +If you write efficient C++, Vix stays efficient. + diff --git a/docs/fundamentals/security.md b/docs/fundamentals/security.md new file mode 100644 index 0000000..c82ed68 --- /dev/null +++ b/docs/fundamentals/security.md @@ -0,0 +1,168 @@ +# Security + +Security in Vix is explicit. + +Vix does not automatically inject security layers. You control: + +- Authentication +- Authorization +- Validation +- Headers +- Error exposure + +------------------------------------------------------------------------ + +## Basic header check + +``` cpp +#include +#include + +using namespace vix; + +int main() +{ + App app; + + app.get("/secure", [](Request& req, Response& res) { + const std::string token = req.header("Authorization"); + + if (token.empty()) + { + res.status(401).json({ + "error", "Missing Authorization header" + }); + return; + } + + res.json({"ok", true}); + }); + + app.run(8080); +} +``` + +------------------------------------------------------------------------ + +## Token validation pattern + +``` cpp +app.get("/admin", [](Request& req, Response& res) { + const std::string token = req.header("Authorization"); + + if (token != "Bearer secret") + { + res.status(403).json({ + "error", "Forbidden" + }); + return; + } + + res.json({"access", "granted"}); +}); +``` + +Always validate tokens properly in production. + +------------------------------------------------------------------------ + +## Input validation + +Never trust user input. + +``` cpp +app.get("/validate", [](Request& req, Response& res) { + const std::string email = req.query_value("email", ""); + + if (email.find('@') == std::string::npos) + { + res.status(400).json({ + "error", "Invalid email" + }); + return; + } + + res.json({"ok", true}); +}); +``` + +------------------------------------------------------------------------ + +## Avoid leaking internal details + +Bad practice: + +``` cpp +res.status(500).json({ + "error", e.what() +}); +``` + +Better practice: + +``` cpp +res.status(500).json({ + "error", "Internal server error" +}); +``` + +Do not expose stack traces or internal logic. + +------------------------------------------------------------------------ + +## Security headers + +You can explicitly set security-related headers: + +``` cpp +app.get("/", [](Request&, Response& res) { + res.set_header("X-Content-Type-Options", "nosniff"); + res.set_header("X-Frame-Options", "DENY"); + res.set_header("Content-Security-Policy", "default-src 'self'"); + + res.json({"ok", true}); +}); +``` + +------------------------------------------------------------------------ + +## HTTPS + +For production: + +- Always run behind HTTPS +- Use a reverse proxy (Nginx, Caddy, etc.) +- Terminate TLS properly +- Keep certificates updated + +Vix focuses on runtime control. Transport security should be properly +configured at deployment level. + +------------------------------------------------------------------------ + +## Rate limiting and protection + +Vix does not enforce rate limiting automatically. + +You can: + +- Implement custom counters +- Use middleware patterns +- Use a reverse proxy for rate limiting + +------------------------------------------------------------------------ + +## Security mindset + +Security is not a framework feature. + +It is an architecture decision. + +In Vix: + +- Nothing is automatic +- Nothing is hidden +- Everything is explicit + +That makes auditing easier and behavior predictable. + diff --git a/docs/guide.md b/docs/guide.md new file mode 100644 index 0000000..b2fe7cf --- /dev/null +++ b/docs/guide.md @@ -0,0 +1,516 @@ +# Routes and Middleware + +This guide explains how to write routes and attach middleware in Vix.cpp, using the style shown in: + +- `vix/examples/vix_routes_showcase.cpp` (routing patterns) +- `vix/examples/http_middleware/mega_middleware_routes.cpp` (middleware patterns) + +Audience: beginners coming from Node.js (Express, Fastify) or Python (Flask, FastAPI). + +--- + +## 1) Quick start + +Run an example file directly: + +```bash +vix run examples/vix_routes_showcase.cpp +``` + +Then test: + +```bash +curl -i http://127.0.0.1:8080/ +curl -i http://127.0.0.1:8080/health +curl -i http://127.0.0.1:8080/users/42 +curl -i "http://127.0.0.1:8080/search?q=vix&page=2&limit=5" +``` + +Middleware mega example: + +```bash +vix run examples/http_middleware/mega_middleware_routes.cpp +``` + +--- + +## 2) Core mental model + +### Routes +A route is `(method, path) -> handler`. + +You write routes on `App`: + +```cpp +App app; + +app.get("/ping", [](Request&, Response& res){ + res.json({"ok", true}); +}); + +app.run(8080); +``` + +### Middleware +A middleware runs before (and sometimes after) a route handler. It can: + +- read request headers/body +- add headers to the response +- store data in `RequestState` (type-based request storage) +- block the request early (return 401/403/400) +- call `next()` to continue to the next middleware/handler + +You can install middleware globally or for a path prefix. + +--- + +## 3) Routes + +### 3.1 Minimal route + +```cpp +app.get("/", [](Request&, Response& res){ + res.json({"message", "Hello, Vix!"}); +}); +``` + +### 3.2 Return values vs using `res` + +Vix supports two styles: + +#### Style A: explicit `res.*` +You fully control the response: + +```cpp +app.get("/status/created", [](Request&, Response& res){ + res.status(201).json({"status", 201, "message", "Created"}); +}); +``` + +#### Style B: return payload (auto-send) +Returning a value sends it automatically: + +```cpp +app.get("/txt", [](const Request&, Response&){ + return "Hello world"; +}); + +app.get("/hello", [](const Request&, Response&){ + return vix::json::o("message", "Hello", "id", 20); +}); +``` + +Important: if you already sent a response using `res`, the returned value is ignored: + +```cpp +app.get("/mix", [](Request&, Response& res){ + res.status(201).send("Created"); + return vix::json::o("ignored", true); // ignored +}); +``` + +### 3.3 Path parameters + +Use `{name}` in the path and read it with `req.param()`. + +```cpp +app.get("/users/{id}", [](Request& req, Response& res){ + const std::string id = req.param("id", "0"); + + if (id == "0") { + res.status(404).json({"error", "User not found", "id", id}); + return; + } + + res.json({"id", id, "name", "User#" + id, "vip", (id == "42")}); +}); +``` + +Multiple parameters: + +```cpp +app.get("/posts/{year}/{slug}", [](Request& req, Response& res){ + res.json({ + "year", req.param("year", "2026"), + "slug", req.param("slug", "hello") + }); +}); +``` + +### 3.4 Query parameters + +Read query values with `req.query_value(key, fallback)`: + +```cpp +app.get("/search", [](Request& req, Response& res){ + const std::string q = req.query_value("q", ""); + const int page = to_int_or(req.query_value("page", "1"), 1); + const int limit = to_int_or(req.query_value("limit", "10"), 10); + + res.json({"q", q, "page", page, "limit", limit}); +}); +``` + +### 3.5 Headers + +Read headers: + +```cpp +app.get("/headers", [](Request& req, Response& res){ + res.json({ + "host", req.header("Host"), + "user_agent", req.header("User-Agent"), + "accept", req.header("Accept") + }); +}); +``` + +Check presence: + +```cpp +app.get("/auth/check", [](Request& req, Response& res){ + res.json({"has_authorization", req.has_header("Authorization")}); +}); +``` + +### 3.6 Request body and JSON + +Raw body: + +```cpp +app.post("/echo/body", [](Request& req, Response& res){ + const std::string body = req.body(); + res.json({"bytes", (long long)body.size(), "body", body}); +}); +``` + +Parsed JSON (when JSON parsing middleware is installed, or when `req.json()` is available): + +```cpp +app.post("/echo/json/fields", [](Request& req, Response& res){ + const auto& j = req.json(); + + std::string name = "unknown"; + bool vip = false; + + if (j.is_object()) { + if (j.contains("name") && j["name"].is_string()) name = j["name"].get(); + if (j.contains("vip") && j["vip"].is_boolean()) vip = j["vip"].get(); + } + + res.json({"name", name, "vip", vip, "raw", j}); +}); +``` + +### 3.7 Status codes + +```cpp +app.get("/status/{code}", [](Request& req, Response& res){ + const int code = to_int_or(req.param("code", "200"), 200); + res.status(code).json({ + "status", code, + "ok", (code >= 200 && code < 300) + }); +}); +``` + +--- + +## 4) Organizing routes (recommended pattern) + +Keep `main()` clean. Put registration in functions: + +```cpp +static void register_api_routes(App& app) { + app.get("/api/ping", [](Request&, Response& res){ + res.json({"ok", true}); + }); +} + +static int run_server() { + App app; + register_api_routes(app); + app.run(8080); + return 0; +} + +int main() { return run_server(); } +``` + +This scales well when your examples become a real service. + +--- + +## 5) Middleware + +Vix supports two middleware styles: + +1) Context-based middleware (recommended): works with `Context` and `Next`. +2) Legacy HTTP middleware: works with `Request`, `Response`, `Next`. + +Adapters exist so you can install both types into the App pipeline. + +### 5.1 Context-based middleware (recommended) + +Signature: + +```cpp +[](vix::middleware::Context& ctx, vix::middleware::Next next) { + // read ctx.req() + // write ctx.res() + next(); +} +``` + +Example: request id middleware storing a value in request state and setting a header: + +```cpp +static vix::middleware::MiddlewareFn mw_request_id() { + return [](vix::middleware::Context& ctx, vix::middleware::Next next) { + RequestId rid; + rid.value = "..." ; // generate id + + ctx.req().emplace_state(std::move(rid)); + ctx.res().header("x-request-id", ctx.req().state().value); + + next(); + }; +} +``` + +Install globally with the adapter: + +```cpp +using namespace vix::middleware::app; + +app.use(adapt_ctx(mw_request_id())); +``` + +### 5.2 Legacy HttpMiddleware + +Signature: + +```cpp +[](vix::Request& req, vix::Response& res, vix::middleware::Next next) { + next(); +} +``` + +Example: require a header: + +```cpp +static vix::middleware::HttpMiddleware mw_require_header(std::string header, std::string expected) { + return [header = std::move(header), expected = std::move(expected)]( + vix::Request& req, vix::Response& res, vix::middleware::Next next + ) mutable { + const std::string got = req.header(header); + if (got != expected) { + res.status(401).json({"ok", false, "error", "unauthorized"}); + return; + } + next(); + }; +} +``` + +Install it on a path using `adapt()`: + +```cpp +using namespace vix::middleware::app; + +install_exact(app, "/api/ping", adapt(mw_require_header("x-demo", "1"))); +``` + +--- + +## 6) Where middleware runs + +You typically install middleware in three ways: + +### 6.1 Global middleware (runs for all routes) + +```cpp +app.use(cors_dev()); +app.use(security_headers_dev(false)); +``` + +Good for: CORS, security headers, logging, request id, metrics. + +### 6.2 Prefix middleware (runs only for routes under a prefix) + +```cpp +using namespace vix::middleware::app; + +install(app, "/api/", rate_limit_dev(120, std::chrono::minutes(1))); +install(app, "/api/secure/", api_key_dev("dev_key_123")); +``` + +Good for: auth on a whole API group, rate limiting, caching. + +### 6.3 Exact middleware (runs only for a single route) + +```cpp +install_exact(app, "/api/echo/json", json_dev(1024, true, true)); +install_exact(app, "/api/echo/form", form_dev(1024, true)); +``` + +Good for: parsers and special checks that should not affect other endpoints. + +--- + +## 7) Chaining middleware + +Most middleware helpers return something you can combine. + +Example: install JSON parsing plus a custom marker middleware: + +```cpp +using namespace vix::middleware::app; + +install_exact( + app, + "/api/echo/json", + chain( + json_dev(1024, true, true), + adapt_ctx(mw_mark_parsed_json()) + ) +); +``` + +--- + +## 8) Using RequestState (share data across middleware and handlers) + +RequestState is type-based storage attached to the request. It is perfect for: + +- auth results (user id, role) +- request id +- parsed body metadata +- timings and diagnostics + +Store a value: + +```cpp +ctx.req().emplace_state(AuthInfo{true, "gaspard", "admin"}); +``` + +Read it later in a handler: + +```cpp +app.get("/api/who", [](Request& req, Response& res){ + if (req.has_state_type()) { + const auto& a = req.state(); + res.json({"authed", a.authed, "subject", a.subject, "role", a.role}); + return; + } + res.json({"authed", false}); +}); +``` + +--- + +## 9) Common presets (what you typically want) + +These are used in the middleware mega example. + +### Security headers +```cpp +app.use(vix::middleware::app::security_headers_dev(false)); +``` + +### CORS (dev) +```cpp +app.use(vix::middleware::app::cors_dev()); +``` + +### Rate limit +```cpp +vix::middleware::app::install(app, "/api/", vix::middleware::app::rate_limit_dev(120, std::chrono::minutes(1))); +``` + +### JSON/Form/Multipart parsing +```cpp +vix::middleware::app::install_exact(app, "/api/echo/json", vix::middleware::app::json_dev(1024, true, true)); +vix::middleware::app::install_exact(app, "/api/echo/form", vix::middleware::app::form_dev(1024, true)); +vix::middleware::app::install_exact(app, "/api/echo/multipart", vix::middleware::app::multipart_save_dev("uploads", 10 * 1024 * 1024)); +``` + +### API key auth (dev) +```cpp +vix::middleware::app::install(app, "/api/secure/", vix::middleware::app::api_key_dev("dev_key_123")); +``` + +### HTTP GET cache middleware +A typical config: + +```cpp +vix::middleware::app::HttpCacheAppConfig cfg; +cfg.prefix = "/api/cache/"; +cfg.only_get = true; +cfg.ttl_ms = 25'000; +cfg.allow_bypass = true; +cfg.bypass_header = "x-vix-cache"; +cfg.bypass_value = "bypass"; +cfg.add_debug_header = true; +cfg.debug_header = "x-vix-cache-status"; +cfg.vary_headers = {"accept-encoding", "accept"}; + +vix::middleware::app::install_http_cache(app, std::move(cfg)); +``` + +Then test: + +```bash +curl -i http://127.0.0.1:8080/api/cache/demo +curl -i http://127.0.0.1:8080/api/cache/demo +curl -i -H "x-vix-cache: bypass" http://127.0.0.1:8080/api/cache/demo +``` + +Look for `x-vix-cache-status`. + +--- + +## 10) A practical layout for real projects + +A simple structure that beginners understand (Node/Python style): + +- `register_public_routes(app)` +- `register_api_routes(app)` +- `register_admin_routes(app)` +- `install_global_middlewares(app)` +- `install_api_middlewares(app)` +- `install_admin_middlewares(app)` + +Then in `main()`: + +```cpp +int main() { + vix::App app; + + install_global_middlewares(app); + install_api_middlewares(app); + + register_public_routes(app); + register_api_routes(app); + + app.run(8080); + return 0; +} +``` + +This mirrors Express/Fastify and stays readable. + +--- + +## 11) Troubleshooting tips + +- Use `curl -i` to see response headers (request id, cache status). +- If a route returns 401/403, check which middleware is installed for its prefix. +- If JSON parsing fails, confirm the JSON middleware is installed on that route and that `Content-Type: application/json` is sent. + +Example JSON request: + +```bash +curl -i -X POST http://127.0.0.1:8080/api/echo/json -H "Content-Type: application/json" -d '{"name":"Ada","vip":true}' +``` + diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..6bff133 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,6 @@ +--- +title: Vix.cpp Documentation +layout: home +--- + + diff --git a/docs/inference/index.md b/docs/inference/index.md new file mode 100644 index 0000000..9919417 --- /dev/null +++ b/docs/inference/index.md @@ -0,0 +1,116 @@ +## inference +# Inference + +The Inference section covers how to build lightweight AI and +model-serving endpoints using Vix. + +Vix does not embed a machine learning framework. + +Instead, it provides: + +- High-performance HTTP handling +- Explicit request control +- Predictable response handling +- Clean integration with C++ inference libraries + +------------------------------------------------------------------------ + +## Philosophy + +Inference in Vix follows one rule: + +Keep the network layer simple. Keep the compute layer isolated. + +Your model code stays separate from your routing code. + +------------------------------------------------------------------------ + +## Minimal inference endpoint + +``` cpp +#include +#include + +using namespace vix; + +int main() +{ + App app; + + app.get("/predict", [](Request& req, Response& res) { + const std::string input = req.query_value("input", ""); + + // Simulated model inference + std::string output = "prediction_for_" + input; + + res.json({ + "input", input, + "prediction", output + }); + }); + + app.run(8080); +} +``` + +------------------------------------------------------------------------ + +## JSON-based inference + +``` cpp +#include + +using namespace vix; + +int main() +{ + App app; + + app.get("/infer", [](Request& req, Response& res) { + const auto& j = req.json(); + + // Simulated inference + res.json({ + "received", j, + "result", "ok" + }); + }); + + app.run(8080); +} +``` + +------------------------------------------------------------------------ + +## Architecture pattern + +Recommended structure for real systems: + +Request → Validation → Model inference → Response formatting + +Keep these concerns separate. + +------------------------------------------------------------------------ + +## Performance mindset + +Inference workloads require: + +- Efficient memory handling +- Minimal copying +- Fast serialization +- Proper hardware utilization + +Vix handles the HTTP layer. Your model library handles computation. + +------------------------------------------------------------------------ + +## Next steps + +In this section you will learn: + +- Integrating ONNX or custom C++ models +- Handling batch inference +- Streaming inference responses +- Performance tuning for model serving + diff --git a/docs/inference/providers.md b/docs/inference/providers.md new file mode 100644 index 0000000..f323599 --- /dev/null +++ b/docs/inference/providers.md @@ -0,0 +1,131 @@ +# Providers + +Vix does not ship an inference engine. + +Instead, you plug in a provider. + +A provider is simply the code that runs inference. + +Examples of providers: + +- ONNX Runtime +- TensorRT +- OpenVINO +- A custom C++ model +- A remote inference service + +Vix only handles HTTP. + +------------------------------------------------------------------------ + +## Minimal provider concept + +A provider takes input and returns output. + +Example: a fake provider inside `main`. + +``` cpp +#include +#include + +using namespace vix; + +int main() +{ + App app; + + app.get("/predict", [](Request& req, Response& res) { + const std::string input = req.query_value("input", ""); + + // Provider logic (minimal example) + std::string prediction = "pred_" + input; + + res.json({ + "input", input, + "prediction", prediction + }); + }); + + app.run(8080); +} +``` + +------------------------------------------------------------------------ + +## Provider with JSON input + +``` cpp +#include +#include + +using namespace vix; + +int main() +{ + App app; + + app.get("/infer", [](Request& req, Response& res) { + const auto& j = req.json(); + + // Minimal provider style + std::string model = "demo-model"; + std::string result = "ok"; + + res.json({ + "model", model, + "input", j, + "result", result + }); + }); + + app.run(8080); +} +``` + +------------------------------------------------------------------------ + +## Provider selection strategy + +Pick based on your constraints: + +- CPU-only inference +- GPU acceleration +- Platform targets +- Model format compatibility +- Deployment simplicity + +Vix stays the same. Only the provider changes. + +------------------------------------------------------------------------ + +## Production notes + +When you use a real provider: + +- Load the model once at startup +- Reuse sessions and buffers +- Avoid reloading per request +- Validate input strictly +- Avoid leaking model internals in errors + +------------------------------------------------------------------------ + +## Common integration style + +In practice you will do: + +- Initialize provider at startup +- Handle requests +- Call provider +- Return JSON output + +Even in production, keep the route handler minimal. + +------------------------------------------------------------------------ + +## Philosophy + +Providers are optional. + +Vix is a runtime. Inference is a module you bring. + diff --git a/docs/inference/reliability.md b/docs/inference/reliability.md new file mode 100644 index 0000000..4d078ba --- /dev/null +++ b/docs/inference/reliability.md @@ -0,0 +1,238 @@ +# Reliability + +This page explains reliability guarantees and failure behavior in Vix +Inference. + +Inference in the real world fails for boring reasons: + +- provider rate limits +- network timeouts +- model cold starts +- overloaded GPUs +- partial responses during streaming +- invalid JSON from clients +- slow clients or disconnects + +The goal is not "never fail". The goal is predictable behavior under +failure. + +------------------------------------------------------------------------ + +## Reliability goals + +Vix Inference is designed around these principles: + +- Fast failure for invalid requests +- Retries for transient provider errors +- Clear error codes and messages +- Safe streaming termination +- Observability (logs + request id) +- No silent partial success + +------------------------------------------------------------------------ + +## Request lifecycle + +A typical request goes through: + +1) validate input (schema and limits) +2) select provider and model +3) execute inference with timeouts +4) stream or buffer output +5) finalize and emit metrics + +If any step fails, Vix returns a structured error. + +------------------------------------------------------------------------ + +## Timeouts + +Always set timeouts. + +Recommended timeouts: + +- connect timeout: 2 to 5 seconds +- first token timeout: 5 to 15 seconds +- total request timeout: 30 to 120 seconds (depends on workload) +- per chunk flush interval: small and constant + +Timeouts must be enforced both: + +- at transport level (HTTP / WS) +- at provider level (SDK / HTTP client) + +------------------------------------------------------------------------ + +## Retries + +Retries should be limited and only for transient failures. + +Good retry candidates: + +- 429 Too Many Requests +- 503 Service Unavailable +- network timeouts +- connection reset + +Bad retry candidates: + +- 400 Bad Request +- 401 Unauthorized +- 403 Forbidden +- 404 Not Found +- 422 Validation errors + +Recommended strategy: + +- max attempts: 2 or 3 +- exponential backoff +- jitter +- retry budget per request + +------------------------------------------------------------------------ + +## Idempotency + +Inference is usually safe to retry because: + +- it does not mutate server state +- it returns generated text + +But streaming can duplicate partial tokens if you retry mid-stream. + +If you support retries on the client: + +- retry only before the first token +- or use an idempotency key and server-side replay + +------------------------------------------------------------------------ + +## Streaming reliability + +Streaming has extra failure modes: + +- client disconnects mid-stream +- provider disconnects mid-stream +- chunk parsing errors +- backpressure and slow readers + +Rules: + +- detect disconnect and cancel inference +- always send an explicit end event when possible +- do not keep streaming forever +- cap total streamed bytes + +Recommended events: + +- `inference.start` +- `inference.token` +- `inference.error` +- `inference.end` + +If the client disconnects, Vix must stop work quickly. + +------------------------------------------------------------------------ + +## Circuit breaker + +If a provider is failing repeatedly, stop sending traffic to it. + +A minimal circuit breaker uses: + +- failure counter in a short window +- open state for a cooldown period +- half-open probe requests + +This protects your system from cascading failure. + +------------------------------------------------------------------------ + +## Fallback providers + +If you configure multiple providers, you can fail over. + +Example behavior: + +- prefer local provider +- if it fails with timeout or 503, try remote provider +- if all fail, return an error with attempts info + +Failover must be visible in logs and metrics. + +------------------------------------------------------------------------ + +## Error format + +A reliable API returns predictable errors. + +Recommended error envelope: + +``` json +{ + "ok": false, + "error": { + "code": "provider_timeout", + "message": "Provider did not respond in time", + "retryable": true + }, + "request_id": "...." +} +``` + +For streaming, send the same envelope inside `inference.error`. + +------------------------------------------------------------------------ + +## Limits + +Reliability includes protecting resources. + +Common limits: + +- max input tokens +- max output tokens +- max request body bytes +- max concurrent streams +- max requests per minute per key +- max streaming duration + +Reject early with a clear 4xx error. + +------------------------------------------------------------------------ + +## Observability + +To debug production failures you need: + +- request id in every response +- structured logs (provider, model, latency) +- metrics (p50/p95/p99, error rates) +- retry counts and final reason + +Minimal log fields: + +- request_id +- provider +- model +- status +- latency_ms +- retry_count +- streaming (true/false) + +------------------------------------------------------------------------ + +## Practical checklist + +Before shipping: + +- timeouts configured +- input validation enforced +- retry policy defined +- streaming end event implemented +- circuit breaker enabled +- rate limit enabled +- logs and metrics enabled + +Reliability is not a feature. It is the default behavior. + diff --git a/docs/inference/routing.md b/docs/inference/routing.md new file mode 100644 index 0000000..e8b1821 --- /dev/null +++ b/docs/inference/routing.md @@ -0,0 +1,176 @@ +# Routing for inference + +Inference endpoints are normal Vix routes. + +The difference is what you do inside the handler: + +- Validate input +- Run model inference (provider call) +- Format output + +Keep the route handler simple. + +------------------------------------------------------------------------ + +## Query-based inference + +``` cpp +#include +#include + +using namespace vix; + +int main() +{ + App app; + + app.get("/predict", [](Request& req, Response& res) { + const std::string input = req.query_value("input", ""); + + if (input.empty()) + { + res.status(400).json({ + "error", "Missing query param", + "param", "input" + }); + return; + } + + // Simulated inference + const std::string output = "pred_" + input; + + res.json({ + "input", input, + "prediction", output + }); + }); + + app.run(8080); +} +``` + +Test: + +``` bash +curl "http://127.0.0.1:8080/predict?input=hello" +``` + +------------------------------------------------------------------------ + +## JSON-based inference + +``` cpp +#include + +using namespace vix; + +int main() +{ + App app; + + app.get("/infer", [](Request& req, Response& res) { + const auto& j = req.json(); + + if (!j.is_object()) + { + res.status(400).json({ + "error", "Expected a JSON object" + }); + return; + } + + // Simulated inference output + res.json({ + "input", j, + "result", "ok" + }); + }); + + app.run(8080); +} +``` + +------------------------------------------------------------------------ + +## Batch inference pattern + +Batch means: one request contains multiple inputs. + +``` cpp +#include + +using namespace vix; +namespace J = vix::json; + +int main() +{ + App app; + + app.get("/batch", [](Request& req, Response& res) { + const auto& j = req.json(); + + if (!j.is_object() || !j.contains("inputs") || !j["inputs"].is_array()) + { + res.status(400).json({ + "error", "Expected JSON with inputs: []" + }); + return; + } + + // Simulated inference over inputs + res.json({ + "inputs", j["inputs"], + "predictions", J::array({"pred_1", "pred_2"}) + }); + }); + + app.run(8080); +} +``` + +------------------------------------------------------------------------ + +## Input size guard + +Inference endpoints should protect the runtime. + +``` cpp +#include +#include + +using namespace vix; + +int main() +{ + App app; + + app.get("/safe", [](Request& req, Response& res) { + const std::string body = req.body(); + + if (body.size() > 1024 * 1024) + { + res.status(413).json({ + "error", "Payload too large" + }); + return; + } + + res.json({"ok", true, "bytes", (long long)body.size()}); + }); + + app.run(8080); +} +``` + +------------------------------------------------------------------------ + +## Rule of thumb + +For inference routing: + +- Validate early +- Reject bad inputs fast +- Keep responses consistent +- Avoid heavy allocations in hot paths +- Load models once, not per request + diff --git a/docs/inference/streaming.md b/docs/inference/streaming.md new file mode 100644 index 0000000..b190d95 --- /dev/null +++ b/docs/inference/streaming.md @@ -0,0 +1,154 @@ +# Streaming + +This page explains streaming in Vix Inference. + +Streaming is used when the model produces tokens progressively. Instead +of waiting for the full response, the server sends chunks as they +arrive. + +------------------------------------------------------------------------ + +## When to use streaming + +Use streaming when you need: + +- low latency first token +- live UI updates (chat typing effect) +- long generations (summaries, code, reasoning) +- server push without buffering full payload + +------------------------------------------------------------------------ + +## Core idea + +A streaming response is a sequence of chunks: + +1) client sends a request +2) server starts inference +3) server emits chunks +4) server ends the stream + +The client processes chunks in order. + +------------------------------------------------------------------------ + +## Minimal HTTP example + +This example shows the intended shape. Exact helper names may differ +based on your inference module version. + +``` cpp +#include + +using namespace vix; + +int main() +{ + App app; + + app.post("/infer/stream", [](Request& req, Response& res) { + (void)req; + + // 1) Set streaming headers (SSE style is common) + res.header("content-type", "text/event-stream"); + res.header("cache-control", "no-cache"); + res.header("connection", "keep-alive"); + + // 2) Start streaming + // send a first chunk + res.write("event: start\n"); + res.write("data: ok\n\n"); + + // 3) Emit chunks (fake loop here) + for (int i = 0; i < 5; ++i) + { + res.write("event: token\n"); + res.write("data: hello\n\n"); + res.flush(); + } + + // 4) End + res.write("event: end\n"); + res.write("data: done\n\n"); + res.flush(); + res.end(); + }); + + app.run(8080); + return 0; +} +``` + +------------------------------------------------------------------------ + +## Recommended chunk format + +Use a typed envelope. This stays consistent with WebSocket typed +messages. + +Example chunk event: + +``` json +{ + "type": "inference.token", + "payload": { + "text": "hello" + } +} +``` + +End event: + +``` json +{ + "type": "inference.end", + "payload": { + "reason": "stop" + } +} +``` + +------------------------------------------------------------------------ + +## Backpressure + +Streaming must handle slow clients. + +Recommended rules: + +- flush only when you have new data +- avoid huge chunks +- cap total bytes per request +- enforce max duration per stream +- close the stream when the client disconnects + +------------------------------------------------------------------------ + +## Error handling + +Errors should be streamed as an event, then the stream must end. + +Example: + +``` json +{ + "type": "inference.error", + "payload": { + "message": "provider timeout" + } +} +``` + +Then send `inference.end`. + +------------------------------------------------------------------------ + +## Notes + +- Streaming is transport specific: HTTP streaming (SSE, chunked) and + WebSocket streaming are both valid. +- Keep chunks small and predictable. +- Always send an explicit end event. + +Next page: providers and routing. + diff --git a/docs/install.md b/docs/install.md new file mode 100644 index 0000000..f2fca61 --- /dev/null +++ b/docs/install.md @@ -0,0 +1,106 @@ +# Install + +This page covers: + +- Installing the Vix CLI +- Platform prerequisites +- Verifying your installation + +------------------------------------------------------------------------ + +## Install Vix CLI + +### Linux / macOS + +``` bash +curl -fsSL https://vixcpp.com/install.sh | bash +``` + +After installation, ensure `~/.local/bin` is in your PATH: + +``` bash +echo $PATH +``` + +Verify: + +``` bash +which vix +vix --version +``` + +------------------------------------------------------------------------ + +### Windows (PowerShell) + +``` powershell +irm https://vixcpp.com/install.ps1 | iex +``` + +Verify: + +``` powershell +Get-Command vix +vix --version +``` + +------------------------------------------------------------------------ + +## Build prerequisites + +Vix projects are modern C++ projects. You need: + +- A C++20 compiler +- CMake 3.20+ +- Ninja (recommended) + +------------------------------------------------------------------------ + +### Ubuntu example + +``` bash +sudo apt update +sudo apt install -y build-essential cmake ninja-build pkg-config libboost-all-dev libssl-dev libsqlite3-dev +``` + +------------------------------------------------------------------------ + +### macOS (Homebrew) + +``` bash +brew install cmake ninja pkg-config boost openssl@3 +``` + +------------------------------------------------------------------------ + +### Windows + +Use: + +- Visual Studio Build Tools (MSVC)\ + or +- clang-cl + +Install dependencies using vcpkg if required by your project. + +------------------------------------------------------------------------ + +## Troubleshooting + +### "vix not found" + +- Restart your terminal +- Ensure `~/.local/bin` is in PATH +- Run `which vix` (Linux/macOS) +- Run `Get-Command vix` (Windows) + +------------------------------------------------------------------------ + +### Download failures + +If installation fails: + +- Check your internet connection +- Verify DNS resolution +- Ensure GitHub releases are accessible from your network + diff --git a/docs/installation.md b/docs/installation.md deleted file mode 100644 index c842496..0000000 --- a/docs/installation.md +++ /dev/null @@ -1,108 +0,0 @@ -## Installation - -### Prerequisites - -Vix.cpp is a **native C++ runtime** and requires a modern toolchain. - -#### All platforms -- **CMake ≥ 3.20** -- **C++20 compiler** - - GCC ≥ 11 - - Clang ≥ 14 - - MSVC ≥ 19.34 (Visual Studio 2022) -- **Git** (with submodules) - -#### Linux -- `pkg-config` -- `ninja` (recommended) -- system development packages: - - Boost - - OpenSSL - - SQLite - - zlib / brotli (optional) - -**Example (Ubuntu):** -```bash -sudo apt update -sudo apt install -y \ - build-essential cmake ninja-build pkg-config \ - libboost-all-dev libssl-dev libsqlite3-dev -``` - -#### macOS -- Xcode Command Line Tools -- Homebrew - -```bash -brew install cmake ninja pkg-config boost openssl@3 -``` - -#### Windows -- **Visual Studio 2022** (Desktop development with C++) -- **Git** -- **PowerShell** -- **vcpkg** (handled automatically by the install script) - ---- - -### Install Vix.cpp (recommended) - -Vix provides platform-specific install scripts that: -- fetch dependencies -- configure the build -- produce the `vix` binary - -#### Linux / macOS -```bash -./install.sh -``` - -You may need: -```bash -chmod +x install.sh -``` - -#### Windows (PowerShell) -```powershell -.\install.ps1 -``` - -> On Windows, dependencies such as **Boost** and **SQLite** are installed automatically via **vcpkg**. - ---- - -### Verify installation - -After installation, verify that `vix` is available: - -```bash -vix --version -``` - -or on Windows: -```powershell -vix.exe --version -``` - -You should see the current release version printed. - ---- - -### Script mode (no project setup) - -Once installed, you can run C++ files directly: - -```bash -vix run main.cpp -vix dev main.cpp -``` - -This compiles, links, and runs your code with the Vix runtime automatically. - ---- - -### Manual build (advanced) - -If you prefer full control, see: -- **Build & Installation** - diff --git a/docs/introduction.md b/docs/introduction.md deleted file mode 100644 index 5d5df9c..0000000 --- a/docs/introduction.md +++ /dev/null @@ -1,153 +0,0 @@ -# Introduction to Vix.cpp - -Vix.cpp is a next‑generation **C++20** web backend framework focused on **speed**, **modularity**, and **developer ergonomics**. -Inspired by ideas from **FastAPI**, **Vue.js**, and **React**, it brings a clean, expressive style to native C++ while retaining zero‑overhead abstractions and low‑level control. - ---- - -## Why Vix.cpp? - -- **Extreme performance** — event‑driven I/O and careful memory locality yield tens of thousands of req/s on commodity hardware. -- **Modern C++20 design** — strong typing, RAII, concepts (where useful), and readable APIs. -- **Modular architecture** — use only what you need: `core`, `utils`, `json`, `orm`, `cli`, `websocket`, `devtools`. -- **Header‑first philosophy** — many pieces are header‑only for easy embedding and fast iteration. -- **Developer experience** — a friendly CLI (`vix new`, `vix build`, `vix run`) and pragmatic defaults. - -> The web doesn’t have to be slow — Vix.cpp aims to prove it with a clear, minimal API and a focus on correctness and speed. - ---- - -## Core Ideas - -1. **Small, sharp core** - The core (`App`, router, request/response, HTTP server) stays tiny and predictable. Everything else is opt‑in. - -2. **Simple routing** - Declarative routes with path parameters: `app.get("/users/{id}", handler);` - -3. **JSON‑first** - Seamless helpers around _nlohmann/json_ via `Vix::json` (builders, small utilities, safe conversions). - -4. **Composability** - Middleware, utilities (Logger, UUID, Time, Env), and an optional ORM layer (MySQL / SQLite) integrate without tight coupling. - -5. **Pragmatism** - Clean, incremental APIs; clear error messages; predictable defaults; portable builds (CMake). - ---- - -## Quick Glance - -```cpp -#include -using namespace Vix; - -int main() { - App app; - app.get("/", [](Request&, Response& res) { - res.send("message", "Hello world"); - }); - app.run(8080); -} -``` - -- One header include (``) to start. -- Minimal boilerplate. -- JSON response helpers out of the box. - ---- - -## Modules Overview - -- **core** — HTTP server, router, request/response, status codes. -- **utils** — Logger (sync/async), UUID, Time, Env, Validation. -- **json** — Light wrappers and builders around _nlohmann/json_. -- **orm** _(optional)_ — Repository/Unit‑of‑Work, QueryBuilder, connection pool, MySQL/SQLite drivers. -- **cli** — `vix new`, `vix build`, `vix run` for fast iteration. -- **websocket** _(WIP)_ — Real‑time channels and messaging. -- **devtools** — Helpers and scripts for local development. - -You can consume modules independently or via the umbrella project. - ---- - -## Performance & Benchmarks (Overview) - -Vix.cpp targets **ultra‑low overhead** per request by combining: - -- event‑driven networking, -- efficient string/JSON handling paths, -- careful thread‑pooling and lock boundaries, -- and predictable memory usage patterns (favoring locality). - -See complete methodology, hardware specs, and raw outputs in **[docs/benchmarks.md](./benchmarks.md)**. - ---- - -## Getting Started - -1. **Clone & submodules** - -```bash -git clone https://github.com/vixcpp/vix.git -cd vix -git submodule update --init --recursive -``` - -2. **Build (Release)** - -```bash -cmake -S . -B build-rel -DCMAKE_BUILD_TYPE=Release -cmake --build build-rel -j -``` - -3. **Run an example** - -```bash -./build-rel/hello_routes -``` - -For platform‑specific setup (Linux/macOS/Windows), see **[Installation](./installation.md)**. -For packaging, sanitizers, and compile_commands.json, see **[Build & Packaging](./build.md)**. - ---- - -## Philosophy & API Design - -- **Clarity over cleverness** — APIs are readable and unsurprising. -- **Minimal global state** — lifetimes are explicit; resources use RAII. -- **Opt‑in features** — only pay for what you use. -- **Stable routes first, meta later** — routing & JSON are first‑class; ORMs, WebSockets, etc. are optional. -- **Tooling matters** — fast feedback loops via CLI and simple CMake presets. - ---- - -## Roadmap (High‑Level) - -- WebSocket engine (channels, rooms, backpressure). -- CLI templates for production scaffolds (Dockerfiles, systemd, Nginx). -- ORM query planner refinements and driver adapters. -- More middlewares (auth, rate‑limit, CORS presets). -- Devtools: profiling hooks and trace exporters. - -For details and status, see **[Architecture](./architecture.md)** and module pages under **[docs/modules](./modules/)**. - ---- - -## Where to Next? - -- **Quick Start** — **[docs/quick-start.md](./quick-start.md)** -- **Installation** — **[docs/installation.md](./installation.md)** -- **Build & Packaging** — **[docs/build.md](./build.md)** -- **CMake Options** — **[docs/options.md](./options.md)** -- **Architecture** — **[docs/architecture.md](./architecture.md)** -- **Examples** — **[docs/examples/overview.md](./examples/overview.md)** -- **ORM Overview** — **[docs/orm/overview.md](./orm/overview.md)** -- **Benchmarks** — **[docs/benchmarks.md](./benchmarks.md)** - ---- - -## Contributing & License - -Contributions are welcome! Please read **CONTRIBUTING.md**. -Licensed under **MIT** — see **LICENSE**. diff --git a/docs/modules/async/api.md b/docs/modules/async/api.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/modules/async/asio.md b/docs/modules/async/asio.md new file mode 100644 index 0000000..0830934 --- /dev/null +++ b/docs/modules/async/asio.md @@ -0,0 +1,195 @@ +# asio_net_service + +`asio_net_service` is an internal networking service used by the Vix async runtime. It owns an independent `asio::io_context` that runs on a dedicated network thread, and it is created lazily by `vix::async::core::io_context::net()`. + +This file lives in the `detail` namespace on purpose. End users typically interact with higher-level networking APIs (`tcp_stream`, `udp_socket`, `dns_resolver`, etc.), not with `asio_net_service` directly. + +--- + +## What problem it solves + +Networking backends like Asio need an event loop (`asio::io_context::run()`) to drive async I/O completions. + +Vix already has its own scheduler (`vix::async::core::scheduler`) for coroutine continuations and posted tasks. Rather than mixing scheduler internals with socket readiness, `asio_net_service` isolates networking into: + +- its own `asio::io_context` +- its own dedicated thread that runs `ioc_.run()` +- a work guard to keep `run()` alive + +This separation keeps the core scheduler minimal and makes the networking backend an optional service. + +--- + +## Where it sits in the architecture + +- `vix::async::core::io_context` + - owns a `scheduler` (core coroutine resumption and task queue) + - lazily owns services: + - `thread_pool` (CPU jobs) + - `timer` (deadlines) + - `signal_set` (signals) + - `net()` -> `asio_net_service` (network backend) + +So the model is: + +- scheduler thread: your main event loop calling `io_context.run()` +- network thread: Asio loop used by socket primitives +- worker threads: optional thread pool (CPU compute) + +--- + +## Key design points + +### 1) Dedicated thread for networking + +`asio_net_service` runs: + +- `net_thread_` calls `ioc_.run()` + +This means networking completions happen on the net thread. Concrete networking primitives should then "bridge back" to the Vix scheduler thread to resume user coroutines in a predictable place. + +### 2) Work guard keeps Asio alive + +Without a guard, `asio::io_context::run()` returns as soon as there is no pending work. + +The service keeps a `executor_work_guard` alive so the Asio loop stays running even when no sockets are active yet. + +### 3) stop() is explicit + +`stop()` is responsible for shutting down the Asio loop and letting the net thread exit. Destruction calls stop and joins the thread, guaranteeing a clean shutdown when `io_context` is destroyed. + +--- + +## Public surface + +### Constructor + +```cpp +explicit asio_net_service(vix::async::core::io_context& ctx); +``` + +- binds to the core `io_context` +- creates a work guard +- starts the network thread + +### Accessor + +```cpp +asio::io_context& asio_ctx() noexcept; +``` + +Used only by internal implementations to bind sockets, resolvers, timers, etc. + +### stop() + +```cpp +void stop() noexcept; +``` + +- releases the guard (so Asio is allowed to finish) +- calls `ioc_.stop()` +- requests thread shutdown + +--- + +## Typical lifecycle + +### Lazy creation + +`io_context` should create it on first `ctx.net()` call. This keeps startup cheap if the app never uses networking. + +### Shutdown behavior + +A typical shutdown sequence is: + +1. app calls `ctx.stop()` (stops Vix scheduler loop) +2. `io_context` destructor triggers service cleanup +3. `asio_net_service::~asio_net_service()` calls `stop()` and joins `net_thread_` + +Because `net_thread_` runs independently, joining is important to avoid a dangling thread during program exit. + +--- + +## How networking primitives should integrate with it + +Concrete networking types (TCP/UDP/DNS implementations) will usually: + +1. create Asio objects bound to `asio_net_service::asio_ctx()` +2. start async operations on the net thread +3. when Asio completes, post the continuation to the Vix scheduler thread + +Example pattern (conceptual): + +```cpp +// PSEUDO CODE for a TCP read awaitable +void on_asio_complete(std::error_code ec, std::size_t n) { + // store ec/n + // then resume coroutine on Vix scheduler thread + ctx.get_scheduler().post(h); +} +``` + +This keeps the rule simple for users: + +- your coroutines resume on the Vix scheduler thread +- networking backends run on their own threads + +--- + +## Threading model and safety notes + +- `asio::io_context` is safe to use from multiple threads, but Vix uses one dedicated net thread for predictability. +- `asio_ctx()` must outlive any sockets/resolvers created from it. +- `stop()` must be safe to call more than once (idempotent behavior is recommended). +- Avoid calling `join()` from the net thread itself. Destruction should happen from the owning context thread. + +--- + +## Common pitfalls + +### 1) Forgetting the guard + +If you do not keep the work guard alive, `ioc_.run()` may immediately return and networking operations will never complete. + +### 2) Resuming coroutines on the wrong thread + +If you resume user continuations directly from the net thread, you can create subtle race conditions because the rest of Vix assumes resumption happens on the scheduler thread. + +The recommended rule is: + +- Asio callbacks run on net thread +- coroutine resumption posts back to `ctx.get_scheduler()` + +### 3) stop order + +If `io_context` is destroyed while networking primitives still hold references to Asio objects, you can get crashes. + +Practical rule: + +- destroy all network objects (streams, listeners, sockets) before `io_context` destruction +- or ensure those objects internally handle `asio_ctx().stopped()` and early exit + +--- + +## Suggested testing checklist + +When implementing the `.cpp` for `asio_net_service`, verify: + +- creating the service starts the thread and `ioc_.run()` stays alive +- `stop()` stops `ioc_` and the thread exits +- `stop()` is safe to call multiple times +- destructor always joins the thread +- lazy creation from `io_context::net()` works and does not create services unless needed + +--- + +## Next docs that depend on this + +Once `asio_net_service` exists, it becomes the backend for: + +- `tcp_stream` and `tcp_listener` implementations +- `udp_socket` implementation +- `dns_resolver` implementation + +Those guides should reference the same threading rule: Asio completes on net thread, coroutine resumes on scheduler thread. + diff --git a/docs/modules/async/cancel.md b/docs/modules/async/cancel.md new file mode 100644 index 0000000..de6a38e --- /dev/null +++ b/docs/modules/async/cancel.md @@ -0,0 +1,160 @@ +# async/core cancel + +Cooperative cancellation primitives for Vix.cpp async code. + +This header defines a tiny cancellation model: + +- `cancel_source`: the owner that can request cancellation +- `cancel_token`: a cheap observer you pass into async work +- `cancel_state`: shared state storing the atomic cancellation flag +- `cancelled_ec()`: a standard `std::error_code` for “operation canceled” + +The design is intentionally minimal and deterministic: no callbacks, no allocations on cancel, no surprises. It is safe to use across threads. + +--- + +## Why this exists + +In async systems, you often need a way to stop work early: + +- user closes a request +- shutdown begins +- a timeout expires +- one branch in a fan-out fails and you want to cancel the rest + +`cancel_token` gives every task a fast “should I stop?” check. `cancel_source` lets a controller signal that stop. + +--- + +## Key types + +### cancel_state + +Shared internal object that holds the cancellation flag. + +- `request_cancel()` sets an atomic boolean to true +- `is_cancelled()` reads it + +Thread safety: +- writes use `memory_order_release` +- reads use `memory_order_acquire` + +That is enough for a simple “flag” protocol across threads. + +### cancel_token + +Read-only view of a cancellation state. + +- Default constructed tokens are “empty” (non-cancelable). +- Tokens are cheap to copy. +- Tokens can be passed across threads safely. + +Methods: + +- `can_cancel()` returns true if it is linked to a source +- `is_cancelled()` returns true if cancellation has been requested + +### cancel_source + +Owner of the shared state. + +- Constructing a source creates a new shared state. +- `token()` returns a `cancel_token` observing the same state. +- `request_cancel()` flips the flag (idempotent). +- `is_cancelled()` reads the flag. + +--- + +## Error model + +`cancelled_ec()` returns a standard error code representing a cancelled operation: + +```cpp +std::error_code ec = vix::async::core::cancelled_ec(); +``` + +It uses `make_error_code(errc::canceled)` from `vix/async/core/error.hpp`. + +Use this when you want to return an explicit “canceled” status from functions that use error codes. + +--- + +## Usage + +### 1) Basic pattern (cooperative loop) + +```cpp +#include + +using vix::async::core::cancel_source; +using vix::async::core::cancel_token; + +void do_work(cancel_token ct) +{ + while (!ct.is_cancelled()) + { + // ... do a chunk of work ... + // keep chunks small enough that cancellation is responsive + } +} +``` + +### 2) Cancel from another thread + +```cpp +#include +#include + +using vix::async::core::cancel_source; + +void run() +{ + cancel_source src; + auto tok = src.token(); + + std::thread worker([tok] { + while (!tok.is_cancelled()) + { + // work + } + }); + + // later + src.request_cancel(); + + worker.join(); +} +``` + +### 3) Return cancellation as an error code + +```cpp +#include + +using vix::async::core::cancel_token; + +std::error_code read_something(cancel_token ct) +{ + if (ct.is_cancelled()) + return vix::async::core::cancelled_ec(); + + // do work... + return {}; +} +``` + +--- + +## Notes and best practices + +- Cancellation is cooperative: nothing is forcibly stopped. Your code must check the token. +- Checkpoints matter: put checks at boundaries (loop iterations, before expensive steps, before blocking operations). +- Empty tokens are valid: treat them as “never canceled” and proceed normally. +- `request_cancel()` is idempotent: calling it multiple times is fine. + +--- + +## Related + +- `vix/async/core/error.hpp` provides the error category and `errc::canceled`. + diff --git a/docs/modules/async/dns.md b/docs/modules/async/dns.md new file mode 100644 index 0000000..45cfe54 --- /dev/null +++ b/docs/modules/async/dns.md @@ -0,0 +1,135 @@ +# dns + +The `dns` module defines the asynchronous hostname resolution interface used by Vix async networking. + +It provides an abstract, coroutine-friendly resolver that integrates with `io_context` and the async runtime. + +--- + +## Overview + +Header: `vix/async/net/dns.hpp` +Namespace: `vix::async::net` + +Main components: + +- `resolved_address` +- `dns_resolver` (abstract interface) +- `make_dns_resolver(io_context&)` + +This layer is intentionally abstract so that different backends (Asio, system resolver, custom cache, etc.) can be plugged in without changing user code. + +--- + +## resolved_address + +```cpp +struct resolved_address { + std::string ip; + std::uint16_t port{0}; +}; +``` + +Represents a single resolved endpoint: + +- `ip`: textual IPv4 or IPv6 address +- `port`: port in host byte order + +Example: + +```cpp +resolved_address a; +a.ip = "93.184.216.34"; +a.port = 80; +``` + +--- + +## dns_resolver + +```cpp +class dns_resolver { +public: + virtual ~dns_resolver() = default; + + virtual core::task> async_resolve( + std::string host, + std::uint16_t port, + core::cancel_token ct = {}) = 0; +}; +``` + +### Contract + +- Resolves a hostname (e.g. `"example.com"`) +- Returns one or more resolved IP addresses +- Supports cooperative cancellation +- Uses `task<>` for coroutine-based integration + +### Error Model + +`async_resolve` may: + +- throw `std::system_error` +- throw runtime-specific resolution errors +- throw cancellation error if `cancel_token` is triggered + +Exceptions are rethrown when the awaiting coroutine resumes. + +--- + +## Factory + +```cpp +std::unique_ptr +make_dns_resolver(core::io_context& ctx); +``` + +Creates the default resolver implementation associated with the given `io_context`. + +The actual backend is runtime-defined (typically Asio-backed). + +--- + +## Example + +```cpp +#include +#include + +using namespace vix::async; + +core::task resolve_example(core::io_context& ctx) +{ + auto resolver = net::make_dns_resolver(ctx); + + auto results = co_await resolver->async_resolve("example.com", 80); + + for (auto& r : results) + { + // r.ip and r.port available + } + + co_return; +} +``` + +--- + +## Design Notes + +- DNS is modeled as a pure async operation returning `task>`. +- No blocking APIs are exposed. +- No dependency on concrete socket types. +- Fully compatible with cancellation and scheduler semantics. + +--- + +## Related + +- `tcp_stream` +- `udp_socket` +- `io_context` +- `task` +- `cancel_token` + diff --git a/docs/modules/async/error.md b/docs/modules/async/error.md new file mode 100644 index 0000000..73d110e --- /dev/null +++ b/docs/modules/async/error.md @@ -0,0 +1,146 @@ +# async/core/error + +Error handling system for the Vix async runtime. + +## Overview + +The `error.hpp` file defines the error model used across: + +- Scheduler +- Thread pool +- Runtime +- Timers +- Cancellation system + +It is based on: + +- `std::error_code` +- A custom `async` error category +- A compact `enum class errc` + +The design is: + +- Lightweight +- Allocation-free +- Stable +- Compatible with standard C++ error handling + +## Error Enumeration + +``` cpp +enum class errc : std::uint8_t +``` + +### Generic errors + + Code Meaning + ------------------ --------------------- + ok No error + invalid_argument Invalid input + not_ready Operation not ready + timeout Operation timed out + canceled Operation canceled + closed Resource closed + overflow Capacity overflow + +### Runtime / Scheduler + + Code Meaning + ------------ ----------------- + stopped Runtime stopped + queue_full Task queue full + +### Thread Pool + + Code Meaning + ---------- -------------------------- + rejected Task submission rejected + +### Platform + + Code Meaning + --------------- ----------------------- + not_supported Feature not supported + +## Error Category + +The async subsystem defines its own `std::error_category`. + +``` cpp +class error_category : public std::error_category +``` + +Category name: + +``` cpp +"async" +``` + +This integrates seamlessly with `std::error_code`. + +## Creating Error Codes + +``` cpp +using namespace vix::async::core; + +std::error_code ec = make_error_code(errc::timeout); + +if (ec) +{ + std::cout << ec.message(); // "timeout" +} +``` + +Because `is_error_code_enum` is specialized, you can write: + +``` cpp +std::error_code ec = errc::canceled; +``` + +## Cancellation Integration + +The cancellation system uses: + +``` cpp +errc::canceled +``` + +Helper: + +``` cpp +std::error_code cancelled_ec(); +``` + +This ensures consistent error propagation across the async runtime. + +## Design Goals + +- No exceptions required +- Thread-safe propagation +- Zero dynamic allocation +- Deterministic behavior +- Stable ABI-friendly enum + +## Typical Usage Pattern + +``` cpp +std::error_code ec; + +if (queue_full) + ec = errc::queue_full; + +if (ec) +{ + return ec; +} +``` + +This style makes async code: + +- Explicit +- Predictable +- Composable + +The async error model keeps the runtime deterministic and fully +compatible with modern C++ error handling. + diff --git a/docs/modules/async/example.md b/docs/modules/async/example.md new file mode 100644 index 0000000..9ed22fc --- /dev/null +++ b/docs/modules/async/example.md @@ -0,0 +1,227 @@ +--- +title: Minimal HTTP Server (Vix async) +--- + +# Minimal HTTP Server (Vix async) + +This example shows how to build a tiny HTTP server using **Vix async**: + +- single `io_context` event loop +- `tcp_listener` accept loop +- per-connection coroutine handler +- graceful stop on `SIGINT` / `SIGTERM` +- log output via `vix::console` + +It is intentionally minimal so you can copy/paste it into a demo or evolve it into a real HTTP module. + +--- + +## What you get + +- Listens on `0.0.0.0:8080` +- Accepts multiple clients +- Reads the HTTP headers until `\r\n\r\n` +- Responds with a fixed `200 OK` and the body: + +``` +Hello from Vix async +``` + +- Closes the connection after sending the response + +--- + +## Full code + +```cpp +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +using vix::async::core::io_context; +using vix::async::core::task; + +namespace +{ + static std::string ok(std::string_view body) + { + std::string r; + r += "HTTP/1.1 200 OK\r\n"; + r += "Content-Length: " + std::to_string(body.size()) + "\r\n"; + r += "Connection: close\r\n\r\n"; + r.append(body.data(), body.size()); + return r; + } + + static task client(std::unique_ptr c) + { + if (!c || !c->is_open()) + co_return; + + std::vector buf(2048); + std::string req; + + while (c->is_open() && req.find("\r\n\r\n") == std::string::npos) + { + std::size_t n = co_await c->async_read(std::span(buf.data(), buf.size())); + if (n == 0) + break; + + req.append(reinterpret_cast(buf.data()), n); + + // Safety cap to avoid unbounded memory growth for this demo + if (req.size() > 64 * 1024) + break; + } + + const std::string resp = ok("Hello from Vix async\n"); + co_await c->async_write(std::span( + reinterpret_cast(resp.data()), resp.size())); + + c->close(); + co_return; + } + + static task server(io_context &ctx) + { + // Stop cleanly on Ctrl+C or a SIGTERM from a process manager + auto &sig = ctx.signals(); + sig.add(SIGINT); + sig.add(SIGTERM); + sig.on_signal([&](int) + { vix::console.warn("[http] stop"); ctx.stop(); }); + + auto l = vix::async::net::make_tcp_listener(ctx); + + // Note: the endpoint expects a uint16_t port, hence the cast + co_await l->async_listen({"0.0.0.0", static_cast(8080)}, 128); + + vix::console.log("[http] ready http://0.0.0.0:8080"); + + while (ctx.is_running()) + { + auto c = co_await l->async_accept(); + vix::async::core::spawn_detached(ctx, client(std::move(c))); + } + + l->close(); + co_return; + } + +} // namespace + +int main() +{ + io_context ctx; + + // Start the server coroutine (task starts suspended in Vix async) + auto t = server(ctx); + ctx.post(t.handle()); + + // Run the event loop until ctx.stop() + ctx.run(); + return 0; +} +``` + +--- + +## Run it + +### 1) Compile and run with Vix + +```bash +vix run server.cpp +``` + +If you want colored console output (when supported): + +```bash +vix run server.cpp --log-color=always +``` + +You should see something like: + +``` +[http] ready http://0.0.0.0:8080 +``` + +### 2) Test with curl + +```bash +curl -i http://127.0.0.1:8080 +``` + +### 3) Test with HTTPie + +```bash +http :8080 +``` + +--- + +## How it works + +### `io_context` + +`io_context` is the runtime event loop. We post the coroutine handle and then run the loop: + +- `ctx.post(t.handle())` schedules the coroutine to start +- `ctx.run()` drives I/O, timers, and coroutine resumes +- `ctx.stop()` stops the loop (triggered by signals in this example) + +### `server()` coroutine + +The server coroutine is responsible for: + +1) Installing signal handlers (`SIGINT`, `SIGTERM`) +2) Listening on an address and port (`async_listen`) +3) Accepting connections (`async_accept`) +4) Spawning one detached coroutine per connection (`spawn_detached`) + +### `client()` coroutine + +The client handler does: + +1) Read data until it sees `\r\n\r\n` (end of headers) +2) Write a minimal HTTP response +3) Close the socket + +--- + +## Notes and limitations + +This is a minimal demo, so it does **not**: + +- parse request method/path +- handle request bodies (`Content-Length`) +- support keep-alive +- implement timeouts +- implement proper HTTP header handling + +It is meant to be a clear starting point. + +--- + +## Next steps + +If you want to evolve this into a real HTTP server, the next upgrades are: + +1) Parse the request line (`GET /path HTTP/1.1`) +2) Add routing (`/`, `/health`, `/metrics`) +3) Support `Content-Length` for POST bodies +4) Add read/write timeouts +5) Keep-alive and request pipelining (optional) + +If you plan to publish this as a reusable example, you can also package it for the Vix Registry and ship it with `vix publish`. + diff --git a/docs/modules/async/index.md b/docs/modules/async/index.md new file mode 100644 index 0000000..2bfa495 --- /dev/null +++ b/docs/modules/async/index.md @@ -0,0 +1,216 @@ +# Async + +The `vix::async` module provides a coroutine-based asynchronous runtime. + +This guide explains the core building blocks and how they fit together. +It focuses on patterns, not full showcase applications. + +--- + +# Core Concepts + +Main components: + +- `io_context` → event loop +- `task` → coroutine task +- `timers()` → async sleep +- `cpu_pool()` → offload CPU work +- `signals()` → signal handling +- `spawn_detached()` → fire-and-forget coroutine + +Async in Vix is: + +- Explicit +- Coroutine-based +- No hidden threads +- Fully controlled by your event loop + +--- + +# 1. Event loop and task + +Every async program starts with an `io_context`. + +Minimal structure: + +```cpp +#include +#include + +using vix::async::core::io_context; +using vix::async::core::task; + +task app(io_context& ctx) +{ + // async logic here + ctx.stop(); + co_return; +} + +int main() +{ + io_context ctx; + + auto t = app(ctx); + ctx.post(t.handle()); + + ctx.run(); +} +``` + +Flow: + +1. Create `io_context` +2. Create a `task` +3. Post the coroutine handle +4. Call `run()` + +Nothing runs automatically. You control the loop. + +--- + +# 2. Timers + +Use `ctx.timers().sleep_for()` for async delays. + +```cpp +co_await ctx.timers().sleep_for( + std::chrono::milliseconds(50) +); +``` + +This does not block the thread. +The coroutine suspends and resumes later. + +--- + +# 3. CPU pool + +Heavy CPU work should not block the event loop. + +Use: + +```cpp +int result = co_await ctx.cpu_pool().submit([]{ + int sum = 0; + for (int i = 0; i < 100000; ++i) + sum += (i % 7); + return sum; +}); +``` + +This runs the lambda on a worker thread pool. +The coroutine resumes when the result is ready. + +Rule: + +- I/O and coordination stay in the event loop. +- CPU-heavy work goes to `cpu_pool()`. + +--- + +# 4. Signals + +To handle SIGINT or SIGTERM: + +```cpp +auto& sig = ctx.signals(); + +sig.add(SIGINT); +sig.add(SIGTERM); + +sig.on_signal([&](int s){ + ctx.stop(); +}); + +co_await sig.async_wait(); +``` + +This allows clean shutdown logic. + +--- + +# 5. TCP server pattern + +Async servers follow this structure: + +1. Create listener +2. `async_listen` +3. Loop with `async_accept` +4. Spawn per-connection task + +Pattern: + +```cpp +auto listener = make_tcp_listener(ctx); +co_await listener->async_listen({"0.0.0.0", 9090}, 128); + +while (ctx.is_running()) +{ + auto client = co_await listener->async_accept(); + + spawn_detached( + ctx, + handle_client(std::move(client)) + ); +} +``` + +Each client is handled in its own coroutine. +No hidden threads are created. + +--- + +# 6. when_all and when_any + +Run multiple tasks concurrently. + +```cpp +auto tup = co_await when_all(sched, a(), b()); +``` + +- Waits for all tasks +- Returns a tuple of results + +```cpp +auto [index, values] = + co_await when_any(sched, a(), b()); +``` + +- Returns when the first task completes +- `index` tells which one finished + +Useful for parallel I/O or timeouts. + +--- + +# Execution Model + +Vix async is: + +- Single explicit event loop +- Cooperative scheduling +- Coroutine suspension via `co_await` +- Deterministic control over lifecycle + +You decide: + +- When to stop +- Where CPU work runs +- How tasks are composed + +--- + +# Recommended Usage + +- Use `io_context` as your runtime root +- Offload heavy work to `cpu_pool()` +- Always stop the context explicitly +- Prefer structured concurrency (`when_all`, `when_any`) +- Use `spawn_detached` carefully (fire-and-forget) + +Async in Vix follows the same philosophy as the rest of the runtime: + +Explicit control. No hidden magic. + + diff --git a/docs/modules/async/io.md b/docs/modules/async/io.md new file mode 100644 index 0000000..be8b831 --- /dev/null +++ b/docs/modules/async/io.md @@ -0,0 +1,164 @@ +# io_context + +`vix::async::core::io_context` is the **core runtime context** for Vix async. +It owns a `scheduler` and exposes **lazy services** used by higher-level features: CPU thread pool, timers, signal handling, and networking. + +This type is designed to be: +- Explicit: you drive it via `run()` and stop it via `stop()` +- Deterministic: no hidden background loops +- Lightweight: services are created only when you ask for them + +--- + +## Header + +```cpp +#include +``` + +--- + +## What it provides + +### 1) Scheduler ownership +`io_context` owns a `scheduler` instance and exposes it via: + +```cpp +scheduler& get_scheduler() noexcept; +const scheduler& get_scheduler() const noexcept; +``` + +You can also post work directly: + +```cpp +template +void post(Fn&& fn); + +void post(std::coroutine_handle<> h); +``` + +### 2) Event loop control + +```cpp +void run(); // processes queued tasks, typically blocks +void stop() noexcept; // requests the scheduler to stop +bool is_running() const noexcept; +``` + +### 3) Lazy services +These are created on first access and owned by the `io_context`: + +```cpp +thread_pool& cpu_pool(); +timer& timers(); +signal_set& signals(); +vix::async::net::detail::asio_net_service& net(); +``` + +Notes: +- `net()` returns an internal service type in a `detail` namespace. The goal is to keep the public API light and avoid pulling networking details into headers. + +--- + +## Minimal example + +```cpp +#include +#include + +static void demo_basic(vix::async::core::io_context& ctx) +{ + ctx.post([&]{ + std::cout << "hello from scheduler\n"; + ctx.stop(); + }); + + ctx.run(); +} + +int main() +{ + vix::async::core::io_context ctx; + demo_basic(ctx); + return 0; +} +``` + +--- + +## Using lazy services + +### CPU pool (compute-bound work) +You typically keep IO on the scheduler and push heavy CPU work to the pool. + +```cpp +static void demo_cpu_pool(vix::async::core::io_context& ctx) +{ + auto& pool = ctx.cpu_pool(); // created here if needed + (void)pool; + + // Example usage depends on thread_pool API (submit, schedule, etc.) +} +``` + +### Timers +```cpp +static void demo_timers(vix::async::core::io_context& ctx) +{ + auto& t = ctx.timers(); // created here if needed + (void)t; + + // Example usage depends on timer API (sleep_for, at, etc.) +} +``` + +### Signals +```cpp +static void demo_signals(vix::async::core::io_context& ctx) +{ + auto& sig = ctx.signals(); // created here if needed + (void)sig; + + // Example usage depends on signal_set API. +} +``` + +### Networking +```cpp +static void demo_net(vix::async::core::io_context& ctx) +{ + auto& n = ctx.net(); // internal Asio-backed service + (void)n; + + // Networking APIs are intentionally built on top of this service. +} +``` + +--- + +## Lifecycle rules and best practices + +- Create one `io_context` per subsystem that needs a runtime loop. +- Call `run()` from the thread you want to own the loop. +- Use `post()` to inject work safely from other parts of the program. +- Stop the loop using `stop()` (or a higher-level shutdown mechanism). +- Avoid constructing lazy services unless you need them. They are owned by the context and destroyed with it. + +--- + +## Common pitfalls + +- Forgetting to call `run()`: posting tasks does nothing until the scheduler is driven. +- Calling `run()` twice concurrently: only one thread should drive a given context loop unless the scheduler explicitly supports multi-threaded driving. +- Storing references to lazy services after context destruction: they are owned by `io_context`. + +--- + +## Related + +- `scheduler` (task queue + coroutine resumption) +- `thread_pool` (compute scheduling) +- `timer` (time-based scheduling) +- `signal_set` (signal handling) +- Networking module built on top of `net()` service + diff --git a/docs/modules/async/scheduler.md b/docs/modules/async/scheduler.md new file mode 100644 index 0000000..a31e460 --- /dev/null +++ b/docs/modules/async/scheduler.md @@ -0,0 +1,222 @@ +# scheduler + +Minimal single-thread scheduler for async tasks and coroutine resumption. + +--- + +## Overview + +`scheduler` is the core execution engine of the Vix async runtime. + +It provides: + +- A thread-safe FIFO job queue +- An event loop (`run()`) +- Posting of: + - Generic callables + - Coroutine continuations +- An awaitable (`schedule()`) to hop onto the scheduler thread + +It is intentionally: + +- Small +- Deterministic +- Single-threaded (run() executes on the calling thread) +- Designed to be embedded inside `io_context` + +--- + +## Basic Usage + +```cpp +#include + +using namespace vix::async::core; + +int main() +{ + scheduler sched; + + sched.post([] { + std::cout << "Hello from scheduler\n"; + }); + + sched.run(); // blocks +} +``` + +--- + +## Posting Work + +### 1. Posting a Callable + +```cpp +sched.post([] { + do_something(); +}); +``` + +The callable is: + +- Stored in a type-erased job +- Enqueued +- Executed when `run()` processes it + +--- + +### 2. Posting a Coroutine Handle + +```cpp +std::coroutine_handle<> h = ...; +sched.post(h); +``` + +The scheduler wraps the handle into a job that resumes the coroutine. + +--- + +## Awaitable Scheduling + +The scheduler provides: + +```cpp +co_await sched.schedule(); +``` + +### What it does + +- Always suspends +- Posts the continuation to the scheduler queue +- Resumes execution on the scheduler thread + +Example: + +```cpp +task my_task(scheduler& sched) +{ + co_await sched.schedule(); + std::cout << "Now running inside scheduler thread\n"; +} +``` + +--- + +## Event Loop + +### run() + +```cpp +sched.run(); +``` + +Behavior: + +- Blocks the calling thread +- Waits on condition_variable +- Processes jobs in FIFO order +- Exits when: + - `stop()` is requested + - Queue is empty and stop flag observed + +--- + +### stop() + +```cpp +sched.stop(); +``` + +- Sets stop flag +- Wakes all waiters +- Allows `run()` to exit cleanly + +--- + +## Introspection + +```cpp +bool running = sched.is_running(); +std::size_t pending = sched.pending(); +``` + +- `is_running()` → whether run() loop is active +- `pending()` → number of queued jobs + +--- + +## Internal Design + +### Job Queue + +Jobs are stored as: + +```cpp +std::deque +``` + +Each job contains: + +- A small type-erased callable holder +- Move-only semantics +- Virtual dispatch through a lightweight polymorphic base + +This avoids std::function overhead while remaining generic. + +--- + +## Thread Safety + +Protected by: + +- `std::mutex` +- `std::condition_variable` + +Safe for: + +- Multiple producers calling `post()` +- Single consumer running `run()` + +--- + +## Design Principles + +scheduler is intentionally: + +- Minimal +- Predictable +- Not a thread pool +- Not a work-stealing system +- Not multi-threaded + +Higher-level concurrency (CPU pools, networking, timers) is built on top of this primitive. + +--- + +## Typical Role in Vix + +`scheduler` is embedded inside: + +```cpp +io_context +``` + +The io_context: + +- Owns a scheduler +- Exposes higher-level async services +- Coordinates timers, signals, networking + +--- + +## Summary + +scheduler is the lowest-level execution primitive of the Vix async core: + +- FIFO task execution +- Coroutine resumption +- Deterministic single-thread loop +- Foundation for the entire runtime + +It is small by design — everything else builds on top of it. + diff --git a/docs/modules/async/signal.md b/docs/modules/async/signal.md new file mode 100644 index 0000000..e6fefc2 --- /dev/null +++ b/docs/modules/async/signal.md @@ -0,0 +1,306 @@ +# signal + +`signal_set` is a small asynchronous signal watcher designed to integrate POSIX signals into the Vix async runtime (`io_context` + `scheduler`). + +It lets you: + +- register signals you care about (`add`, `remove`) +- `co_await` the next signal via `async_wait()` +- optionally run a callback on each received signal (`on_signal`) +- stop the watcher (`stop()`) + +This guide focuses on how to use the API, how it behaves, and what the important constraints are. + +--- + +## What problem does this solve? + +Signals (like `SIGINT` when you press Ctrl+C) arrive outside your normal program flow. + +If you want a clean shutdown in an async program, you typically want: + +- a single place to observe signals +- a way to `co_await` a signal in coroutines +- delivery of completions on your scheduler thread (not on some random signal context) + +`signal_set` provides that integration point. + +--- + +## Design model + +`signal_set` is: + +- bound to an `io_context` +- potentially backed by a dedicated worker thread (lazy start) +- able to deliver signal events to: + - a single awaiting coroutine (single waiter model) + - an optional callback + +Important behaviors implied by the header: + +1. **Single waiter model** + - `signal_set` stores one `std::coroutine_handle<> waiter_` and `bool waiter_active_`. + - That strongly suggests only one `async_wait()` is intended at a time. + - If you need multiple consumers, build a fan-out layer (e.g. channel/queue) on top. + +2. **Queue for pending signals** + - Captured signals are buffered in `pending_`. + - If a signal arrives before you call `async_wait()`, it can be consumed later. + +3. **Callbacks run on the scheduler thread** + - `on_signal(fn)` says the callback is posted via `io_context` posting mechanism. + - That means your callback runs on the same thread that executes `ctx.run()`. + +4. **Cancellation support** + - `async_wait(cancel_token)` integrates with Vix cancellation. + - If cancellation is requested, the task should complete with a cancellation error or equivalent behavior (see your `task<>` contract). + +5. **Stop is explicit** + - `stop()` requests shutdown and should wake any active waiter. + +--- + +## Typical usage + +### 1) Basic Ctrl+C handling + +In an async app, you often want Ctrl+C to request cancellation and let your tasks unwind. + +```cpp +#include +#include +#include +#include + +using namespace vix::async::core; + +task app_main(io_context& ctx, signal_set& sigs, cancel_source& cs) +{ + // Wait for SIGINT (Ctrl+C) + int sig = co_await sigs.async_wait(cs.token()); + (void)sig; + + // Request cancellation for the rest of the system + cs.request_cancel(); + + co_return; +} + +int main() +{ + io_context ctx; + + // signal_set is a lazy watcher bound to ctx + signal_set sigs(ctx); + + // Register SIGINT + sigs.add(SIGINT); + + cancel_source cs; + + // Start your app tasks (depends on your task runner API) + // Example shape: + // vix::async::core::spawn(ctx, app_main(ctx, sigs, cs)); + + ctx.run(); + return 0; +} +``` + +Notes: + +- Register signals early (before `run`) so you do not miss early signals. +- Ensure `ctx.run()` is running so posted completions can execute. + +### 2) Using a callback instead of awaiting + +Sometimes you just want a lightweight handler that flips a flag and triggers shutdown. + +```cpp +signal_set sigs(ctx); +sigs.add(SIGINT); +sigs.add(SIGTERM); + +sigs.on_signal([&](int sig){ + // Runs on the scheduler thread + // Keep it short and safe. + cs.request_cancel(); +}); +``` + +This is convenient when you do not want a dedicated coroutine waiting on signals. + +### 3) Supporting both callback and coroutine wait + +You can do both: + +- callback for immediate side effects +- coroutine wait for structured shutdown sequencing + +Be careful to avoid duplicate actions (e.g. requesting cancel twice is usually fine). + +--- + +## API reference + +### `signal_set(io_context& ctx)` + +Binds the signal watcher to a runtime context. + +The watcher can post completions and callbacks back onto `ctx` so they execute on the scheduler thread. + +### `void add(int sig)` + +Register a signal number (e.g. `SIGINT`, `SIGTERM`) to observe. + +The implementation may start the internal worker thread lazily on first registration or first wait. + +### `void remove(int sig)` + +Stop observing a given signal number. + +If the signal is already pending in the queue, removal does not necessarily remove it from the pending queue. + +### `task async_wait(cancel_token ct = {})` + +Asynchronously wait for the next received signal. + +Key behaviors: + +- If `pending_` already contains a signal, the task can complete quickly by consuming it. +- If no pending signal exists, the coroutine suspends and `waiter_` is stored. +- Cancellation token may cancel the wait. + +Because the header stores a single waiter handle, assume: + +- only one active `async_wait()` at a time +- calling `async_wait()` again concurrently is either rejected, undefined, or causes overwrites + +If you need multiple waits, serialize them: + +```cpp +for (;;) +{ + int sig = co_await sigs.async_wait(ct); + // handle sig +} +``` + +### `void on_signal(std::function fn)` + +Register a callback invoked for each received signal. + +The callback is posted onto the scheduler thread. + +Rules of thumb: + +- Keep it fast (no blocking I/O, no heavy work). +- Delegate heavy work to other tasks using `ctx.post(...)` or your coroutine orchestration. + +### `void stop() noexcept` + +Requests shutdown of the internal worker (if any) and wakes the active waiter (if any). + +Use this if your program is exiting and you want to ensure: + +- worker thread terminates +- pending tasks can unblock + +--- + +## Threading and safety notes + +- Internal state is protected by a mutex (`m_`). +- The worker thread captures signals and pushes them to `pending_`. +- Delivery to coroutines/callbacks is done by posting onto `io_context` (scheduler thread). + +Practical implications: + +- You should treat `signal_set` methods as thread-safe unless your implementation says otherwise. +- Your callback runs on the scheduler thread, so it can safely touch scheduler-owned state if that state is also scheduler-thread-confined. +- Do not assume callbacks run immediately after the signal arrives: they are queued via `ctx_post`. + +--- + +## Cancellation behavior + +`async_wait(cancel_token)` indicates waiters can be cancelled. + +Make sure your shutdown logic accounts for: + +- cancellation requested before a signal arrives +- stop() called while waiting +- multiple signals arriving quickly + +A robust pattern is: + +- request cancellation on signal +- let other tasks watch the cancel token +- stop the io_context once tasks have drained + +--- + +## Common patterns + +### Graceful shutdown: signal cancels, main loop exits + +- signal triggers cancellation +- your main coroutine or control loop stops the context + +Pseudo: + +```cpp +int sig = co_await sigs.async_wait(ct); +cs.request_cancel(); +// wait for tasks to finish if you have a join mechanism +ctx.stop(); +``` + +### Coalesce repeated signals + +If you press Ctrl+C multiple times, you may get multiple pending signals. + +You can ignore subsequent ones: + +```cpp +bool first = true; +for (;;) +{ + int sig = co_await sigs.async_wait(ct); + if (first) + { + first = false; + cs.request_cancel(); + } +} +``` + +--- + +## Practical limitations + +1. POSIX-only by intent + - The header references POSIX signals (``). + - On non-POSIX platforms, you may provide a stub or use `errc::not_supported`. + +2. Signal semantics are platform-dependent + - Delivery rules depend on process/thread masks. + - If you do signal masking in other threads, document your expectation. + +3. Single waiter model + - If you need multiple consumers, build a small channel around it. + +--- + +## Summary + +`signal_set` is the async bridge from OS signals to Vix coroutines: + +- register signals (`add`) +- await the next signal (`async_wait`) +- optional callback (`on_signal`) posted on the scheduler thread +- stop cleanly (`stop`) + +It is small, explicit, and integrates into `io_context` without hiding complex behavior. + diff --git a/docs/modules/async/spawn.md b/docs/modules/async/spawn.md new file mode 100644 index 0000000..3ac39ac --- /dev/null +++ b/docs/modules/async/spawn.md @@ -0,0 +1,101 @@ +# spawn + +`spawn` is the tiny "fire and forget" helper for Vix async tasks. + +It takes a `task`, schedules it onto an `io_context` scheduler, and lets it run without returning anything to the caller. + +This is useful for background work where you do not want to propagate a result, for example telemetry, cache warmups, periodic refresh, or "best effort" cleanup. + +## What it provides + +- `vix::async::core::spawn_detached(io_context&, task)` +- An internal helper coroutine type: `detail::detached_task` + +`spawn_detached` is intentionally small and deterministic: + +- The coroutine is posted to `io_context` via `ctx.post(handle)`. +- The coroutine frame destroys itself at the end (final suspend). +- Exceptions are swallowed by design (detached tasks have no observer). + +## API + +```cpp +namespace vix::async::core { + inline void spawn_detached(io_context& ctx, task t); +} +``` + +### Behavior + +1. Wrap the provided `task` into an internal coroutine (`make_detached`). +2. Post the wrapper coroutine handle to the `io_context` scheduler. +3. The wrapper `co_await`s the original task. +4. At completion, `final_suspend` destroys the wrapper frame. + +## How it works internally + +### `detail::detached_task` + +`detached_task` is a dedicated coroutine type with a promise that: + +- starts suspended (`initial_suspend = suspend_always`) +- self-destroys at final suspend +- swallows exceptions in `unhandled_exception()` + +That makes it safe as a scheduler posted coroutine handle: + +- it will not leak (self-destruction) +- it will not crash the runtime due to an unobserved exception + +### `detail::make_detached(task)` + +```cpp +inline detached_task make_detached(task t) { + co_await t; + co_return; +} +``` + +This wrapper exists so `spawn_detached` can post a coroutine handle that has a self-destroying final suspend, even if the original task implementation is updated later. + +## Example + +A simple "background job" that runs on the scheduler thread: + +```cpp +#include +#include + +using namespace vix::async::core; + +task background() +{ + // do something best-effort + // if you throw here, it will be swallowed (detached) + co_return; +} + +int main() +{ + io_context ctx; + + spawn_detached(ctx, background()); + + ctx.run(); + return 0; +} +``` + +## Notes and best practices + +- `spawn_detached` is for `task` only. If you need a value, return a `task` and `co_await` it somewhere. +- Because exceptions are swallowed, treat detached tasks as "best effort". + - If you need observability, add logging inside the task body or later connect `unhandled_exception()` to the logger. +- The work runs on the scheduler thread unless the task uses other services (cpu pool, timers, net) and awaits them. + +## Related + +- `task`: coroutine result type and ownership model +- `scheduler`: single-thread scheduler queue and event loop +- `io_context`: runtime context that owns the scheduler and lazy services + diff --git a/docs/modules/async/task.md b/docs/modules/async/task.md new file mode 100644 index 0000000..4b60304 --- /dev/null +++ b/docs/modules/async/task.md @@ -0,0 +1,243 @@ +# task + +Coroutine task type for the async core runtime. + +`task` is Vix.cpp’s minimal coroutine return type: it represents an asynchronous computation that eventually produces a value of type `T` (or throws). Tasks are lazy by default: they start suspended and only run when awaited or explicitly scheduled. + +This page documents the public behavior of `vix::async::core::task` as implemented in `task.hpp`. + +--- + +## Header + +```cpp +#include +``` + +Namespace: + +```cpp +vix::async::core +``` + +--- + +## What `task` is + +`task` is a move-only coroutine handle owner with these properties: + +- **Lazy**: created suspended (`initial_suspend()` is `suspend_always`). +- **Single-consumer result**: the produced value is moved out once in `await_resume()`. +- **Exception-aware**: exceptions are captured and rethrown from `await_resume()`. +- **Continuation-based**: `co_await task` resumes the awaiting coroutine at final suspend. +- **Detachable**: `std::move(t).start(sched)` schedules the coroutine and releases ownership. + +There is also a specialization: + +- `task`: same behavior, but no value. + +--- + +## Lifecycle and suspension model + +### Lazy start + +A task does not start running when you create it. It starts when: + +- You `co_await` it, or +- You call `std::move(task).start(scheduler)`. + +This comes from: + +- `promise_common::initial_suspend()` returns `std::suspend_always`. + +### Continuation wiring + +When you `co_await` a task: + +1. The awaiter stores the awaiting coroutine handle into `promise.continuation`. +2. The runtime resumes the task coroutine. +3. At `final_suspend`, the task returns the continuation handle so the runtime resumes it. + +If there is no continuation and the task is detached, the coroutine frame destroys itself at final suspend. + +--- + +## Basic usage + +### Await a task that returns a value + +```cpp +#include + +using vix::async::core::task; + +task compute() +{ + co_return 42; +} + +task demo() +{ + int v = co_await compute(); + (void)v; + co_return; +} +``` + +### Await a task that returns void + +```cpp +using vix::async::core::task; + +task do_work() +{ + co_return; +} + +task demo() +{ + co_await do_work(); + co_return; +} +``` + +--- + +## Error propagation + +If a task throws, the exception is captured in the promise via `unhandled_exception()` and rethrown in `await_resume()`. + +Example: + +```cpp +#include +#include + +using vix::async::core::task; + +task fails() +{ + throw std::runtime_error("boom"); + co_return 0; +} + +task demo() +{ + try + { + int x = co_await fails(); + (void)x; + } + catch (const std::exception &) + { + // handle error + } + co_return; +} +``` + +## Detaching and scheduling + +### `start(scheduler&)` + +`start()` detaches the task and posts it onto a scheduler: + +- Marks `promise.detached = true` +- Posts the coroutine handle to `scheduler::post(handle)` +- Releases ownership (`task` becomes empty) + +Signature (both `task` and `task`): + +```cpp +void start(scheduler &sched) && noexcept; +``` + +Important implications: + +- You must call it on an rvalue: `std::move(t).start(sched)`. +- After `start()`, the task object no longer owns the coroutine frame. +- If nobody awaits it (no continuation), the frame self-destroys at final suspend. + +Example: + +```cpp +#include +#include + +using vix::async::core::scheduler; +using vix::async::core::task; + +task background_job() +{ + // do something + co_return; +} + +void run_detached() +{ + scheduler sched; + + // fire-and-forget + std::move(background_job()).start(sched); + + // drive the scheduler on this thread + sched.run(); +} +``` + +--- + +## Awaiting from coroutines + +### `operator co_await` + +Both lvalue and rvalue `co_await` are supported: + +```cpp +auto operator co_await() & noexcept; +auto operator co_await() && noexcept; +``` + +Behavior: + +- `await_ready()` is true if the handle is empty or already completed. +- `await_suspend(awaiting)` stores the continuation and resumes the task. +- `await_resume()` rethrows captured exceptions and returns/moves the result. + +--- + +## Ownership rules + +- `task` is **move-only** (copy is deleted). +- Destroying a non-detached `task` destroys its coroutine frame (`h_.destroy()`). +- After `start()`, the task releases its handle, so its destructor does nothing. + +You can inspect: + +```cpp +bool valid() const noexcept; +explicit operator bool() const noexcept; +handle_type handle() const noexcept; +``` + +--- + +## Notes and constraints + +- `task` rejects reference result types at compile time: + + `static_assert(!std::is_reference_v)` + + If you need to return references, use `task>`. + +- The implementation is intentionally minimal: it does not provide cancellation, timeouts, or executors directly. Those are built around it (e.g. `cancel_token`, `io_context`, timers). + +--- + +## Related + +- `scheduler` for driving posted tasks and coroutine resumptions +- `io_context` as the higher-level runtime that owns a scheduler and services +- `cancel_source` / `cancel_token` for cooperative cancellation + diff --git a/docs/modules/async/tcp.md b/docs/modules/async/tcp.md new file mode 100644 index 0000000..f5b6b10 --- /dev/null +++ b/docs/modules/async/tcp.md @@ -0,0 +1,308 @@ +# TCP (async/net) + +This page documents the public TCP interfaces in **`vix::async::net`**. + +These types are intentionally small and abstract: +- `tcp_stream` represents a connected TCP socket. +- `tcp_listener` represents a listening socket that accepts connections. +- `make_tcp_stream()` and `make_tcp_listener()` create runtime-backed implementations (typically Asio-backed) that integrate with `vix::async::core::io_context`. + +The design goal is to keep the public API stable while allowing the internal networking backend to evolve without leaking implementation details into user code. + +--- + +## Files + +Typical include: + +```cpp +#include +``` + +Related core pieces used by the TCP layer: + +```cpp +#include +#include +#include +#include +``` + +--- + +## Types + +### tcp_endpoint + +A simple endpoint description used by both connect and listen operations. + +```cpp +struct tcp_endpoint { + std::string host; + std::uint16_t port{0}; +}; +``` + +Notes: +- `host` can be a DNS name (`"example.com"`) or an IP string (`"127.0.0.1"`, `"::1"`). +- `port` is in host byte order. + +--- + +## tcp_stream + +`tcp_stream` is an abstract interface for a connected TCP socket. + +```cpp +class tcp_stream { +public: + virtual ~tcp_stream() = default; + + virtual core::task async_connect( + const tcp_endpoint& ep, + core::cancel_token ct = {}) = 0; + + virtual core::task async_read( + std::span buf, + core::cancel_token ct = {}) = 0; + + virtual core::task async_write( + std::span buf, + core::cancel_token ct = {}) = 0; + + virtual void close() noexcept = 0; + virtual bool is_open() const noexcept = 0; +}; +``` + +### Behavior rules + +- **Coroutine-based:** all async operations return `core::task<...>`. +- **Cancellation:** operations accept a `core::cancel_token`. + - If cancellation is observed, implementations should fail with `std::system_error(cancelled_ec())`. +- **Errors:** networking failures should be reported via `std::system_error` (e.g. connect refused, host not found, broken pipe). +- **Partial IO:** `async_read` and `async_write` return the number of bytes actually transferred. + - A return value of `0` from `async_read()` typically indicates "peer closed" (EOF), depending on backend semantics. +- **Idempotent close:** `close()` may be called multiple times safely. +- **State:** `is_open()` reports whether the underlying socket is usable. + +### Buffer types + +The API uses `std::span`: +- Read: `std::span` (mutable destination buffer) +- Write: `std::span` (immutable source buffer) + +This allows callers to manage buffers explicitly and avoids hidden allocations. + +--- + +## tcp_listener + +`tcp_listener` is an abstract interface for a listening socket that accepts connections. + +```cpp +class tcp_listener { +public: + virtual ~tcp_listener() = default; + + virtual core::task async_listen( + const tcp_endpoint& bind_ep, + int backlog = 128) = 0; + + virtual core::task> async_accept( + core::cancel_token ct = {}) = 0; + + virtual void close() noexcept = 0; + virtual bool is_open() const noexcept = 0; +}; +``` + +### Behavior rules + +- **async_listen():** + - Binds to `bind_ep.host:bind_ep.port` + - Starts listening with the provided `backlog` + - Fails with `std::system_error` on bind/listen errors + +- **async_accept():** + - Waits for one incoming connection and returns a `std::unique_ptr` + - Accept can be canceled via `cancel_token` + - Accept can fail with `std::system_error` (closed listener, system error, etc.) + +- **close():** + - Stops accepting new connections + - Wakes an ongoing `async_accept()` depending on backend behavior + +--- + +## Factories + +These functions create concrete runtime-backed implementations that are associated with an `io_context`. + +```cpp +std::unique_ptr make_tcp_stream(core::io_context& ctx); +std::unique_ptr make_tcp_listener(core::io_context& ctx); +``` + +### Why factories + +- Keeps the public API minimal and stable +- Allows different backends (Asio today, potentially others later) +- Centralizes integration with the scheduler in `io_context` + +--- + +## Usage examples + +### 1) Connect + write + read (client) + +```cpp +#include +#include +#include +#include +#include +#include + +using namespace vix::async; + +static core::task client(core::io_context& ctx) +{ + auto s = net::make_tcp_stream(ctx); + + net::tcp_endpoint ep; + ep.host = "127.0.0.1"; + ep.port = 8080; + + co_await s->async_connect(ep); + + const std::string msg = "ping\n"; + std::span out{ + reinterpret_cast(msg.data()), + msg.size() + }; + + (void)co_await s->async_write(out); + + std::array buf{}; + const std::size_t n = co_await s->async_read(std::span{buf.data(), buf.size()}); + + // You decide how to interpret bytes (text, binary, framing, etc). + (void)n; + + s->close(); + co_return; +} + +static void run_client() +{ + core::io_context ctx; + core::spawn_detached(ctx, client(ctx)); + ctx.run(); +} + +int main() +{ + run_client(); +} +``` + +Notes: +- `spawn_detached()` schedules a `task` without returning a join handle. +- IO is buffer-explicit; conversions from `std::string` to bytes are done explicitly. + +--- + +### 2) Listen + accept loop (server) + +```cpp +#include +#include +#include +#include +#include + +using namespace vix::async; + +static core::task handle_client(std::unique_ptr s) +{ + std::array buf{}; + + while (s->is_open()) + { + const std::size_t n = co_await s->async_read(std::span{buf.data(), buf.size()}); + if (n == 0) + break; + + // echo back + (void)co_await s->async_write(std::span{buf.data(), n}); + } + + s->close(); + co_return; +} + +static core::task server(core::io_context& ctx) +{ + auto l = net::make_tcp_listener(ctx); + + net::tcp_endpoint bind; + bind.host = "0.0.0.0"; + bind.port = 8080; + + co_await l->async_listen(bind, 128); + + while (l->is_open()) + { + auto s = co_await l->async_accept(); + core::spawn_detached(ctx, handle_client(std::move(s))); + } + + co_return; +} + +static void run_server() +{ + core::io_context ctx; + core::spawn_detached(ctx, server(ctx)); + ctx.run(); +} + +int main() +{ + run_server(); +} +``` + +Notes: +- This is a minimal echo server. Real servers add framing, protocol parsing, backpressure, and timeouts. +- Spawning one detached coroutine per accepted connection is a simple model. Higher-level servers may use connection limits or pools. + +--- + +## Cancellation pattern + +Cancellation is cooperative: you pass a `cancel_token` into async calls. + +```cpp +#include + +vix::async::core::cancel_source cs; +auto ct = cs.token(); + +// Later: request cancellation from any thread +cs.request_cancel(); + +// Then pass ct into async_connect/read/write/accept +``` + +The exact point where cancellation is observed depends on the backend implementation. + +--- + +## Design notes + +- The TCP API is intentionally low-level: it does not impose framing, buffering strategy, or protocol rules. +- `std::span` makes IO explicit and avoids implicit conversions. +- Abstract interfaces + factories keep user code stable while allowing backend improvements. + diff --git a/docs/modules/async/threadpool.md b/docs/modules/async/threadpool.md new file mode 100644 index 0000000..930a919 --- /dev/null +++ b/docs/modules/async/threadpool.md @@ -0,0 +1,226 @@ +# thread_pool (async/core) + +`vix::async::core::thread_pool` is a small CPU thread pool designed to work with `io_context`. +It runs submitted jobs on worker threads and resumes awaiting coroutines back onto the `io_context` scheduler thread. + +This gives you a clean split: + +- Worker threads: do compute work (hashing, parsing, compression, DB client CPU work, etc.) +- Scheduler thread: resumes coroutines and continues async flows + +## Header + +```cpp +#include +``` + +## Concepts + +### Two submission styles + +1) Fire-and-forget + +- `submit(std::function)` +- No result +- Useful for background work where you do not need to await completion + +2) Coroutine-friendly submission + +- `submit(Fn, cancel_token) -> task` +- Lets you `co_await` the result +- Captures exceptions from the worker and rethrows them when resuming + +### Cancellation model + +The coroutine-friendly overload accepts a `cancel_token`. + +- If cancellation is already requested when the worker starts executing the job, the job fails with a cancellation error. +- That cancellation error is `std::system_error(cancelled_ec())` where `cancelled_ec()` is the async cancellation error code. + +Important: cancellation here is cooperative and checked only at job start. +If your job is long-running, you should also check `ct.is_cancelled()` inside the job body and return early. + +### Exception model + +- Any exception thrown by the job on a worker thread is captured. +- When the awaiting coroutine resumes, `await_resume()` rethrows that exception. + +That means your coroutine can use normal `try/catch` around `co_await pool.submit(...)`. + +## API + +### Construction + +```cpp +explicit thread_pool(io_context& ctx, + std::size_t threads = std::thread::hardware_concurrency()); +``` + +- `ctx` is used to post coroutine continuations back to the scheduler. +- `threads` controls the number of worker threads. + +### Destruction + +```cpp +~thread_pool(); +``` + +- Requests stop and joins all worker threads. + +### Fire-and-forget + +```cpp +void submit(std::function fn); +``` + +### Awaitable submission + +```cpp +template +auto submit(Fn&& fn, cancel_token ct = {}) + -> task>; +``` + +- `Fn` runs on a worker thread. +- The awaiting coroutine resumes on the `io_context` scheduler thread. + +### Stop + +```cpp +void stop() noexcept; +``` + +Requests workers to exit. + +### Size + +```cpp +std::size_t size() const noexcept; +``` + +Returns number of worker threads. + +## Usage + +### Example 1: Await a compute result + +```cpp +#include +#include +#include +#include +#include + +using namespace vix::async::core; + +task app(io_context& ctx) +{ + // Lazily creates the pool in io_context in your codebase, + // but you can also create it directly. + auto& pool = ctx.cpu_pool(); + + std::string input = "hello"; + + // Run on worker threads + auto out = co_await pool.submit([s = input]() { + // Heavy compute goes here + std::string r = s; + for (auto& c : r) c = static_cast(c - 32); + return r; + }); + + // Resumed on scheduler thread here + std::cout << out << "\n"; + co_return; +} + +int main() +{ + io_context ctx; + + // Run the coroutine on the scheduler thread + auto t = app(ctx); + std::move(t).start(ctx.get_scheduler()); + + ctx.run(); +} +``` + +### Example 2: Cancellation + +```cpp +#include +#include +#include + +using namespace vix::async::core; + +task work(io_context& ctx) +{ + auto& pool = ctx.cpu_pool(); + + cancel_source cs; + auto ct = cs.token(); + + // Request cancellation before scheduling + cs.request_cancel(); + + try + { + auto v = co_await pool.submit([=]() -> int { + // This will not run if cancellation is observed at start + return 42; + }, ct); + + (void)v; + } + catch (const std::system_error& e) + { + // e.code() should equal cancelled_ec() + } + + co_return; +} +``` + +If you want deeper cooperative cancellation for long jobs, check the token inside the job: + +```cpp +auto v = co_await pool.submit([ct]() -> int { + for (int i = 0; i < 10'000'000; ++i) + { + if (ct.is_cancelled()) + throw std::system_error(cancelled_ec()); + // do work... + } + return 1; +}, ct); +``` + +### Example 3: Fire-and-forget + +```cpp +auto& pool = ctx.cpu_pool(); + +pool.submit([] { + // runs on a worker thread + // no awaiting, no result +}); +``` + +## Notes + +- `thread_pool` is intended for CPU-bound work. + For I/O-bound work, prefer your async networking and timers services. +- Awaited jobs always resume on the `io_context` scheduler thread. + This keeps coroutine code deterministic and reduces locking in user code. +- The pool uses a single mutex and a FIFO job queue. + This keeps behavior explicit and simple. + +## Related + +- `cancel_source`, `cancel_token` in `vix::async::core::cancel` +- `scheduler` in `vix::async::core::scheduler` +- `task` in `vix::async::core::task` +- `io_context` in `vix::async::core::io_context` + diff --git a/docs/modules/async/timer.md b/docs/modules/async/timer.md new file mode 100644 index 0000000..f9908b5 --- /dev/null +++ b/docs/modules/async/timer.md @@ -0,0 +1,243 @@ +# Timer + +`vix::async::core::timer` is a small timer service integrated with `io_context`. + +It provides two primary features: + +- `after()`: schedule a callback to run after a delay +- `sleep_for()`: coroutine-friendly delay that resumes on the `io_context` scheduler thread + +The timer is designed to be explicit and deterministic: +- it uses a dedicated worker thread to wait for deadlines +- it posts completions back onto the `io_context` scheduler thread +- it supports per-entry cancellation via `cancel_token` + +--- + +## Key types + +- `timer::clock`: `std::chrono::steady_clock` (monotonic, safe for timeouts) +- `timer::time_point`: deadline timestamp +- `timer::duration`: delay duration + +Because the clock is steady, timers are not affected by wall clock changes. + +--- + +## Lifecycle and threading model + +When you construct a `timer`, it starts a worker thread that: + +1. Waits until the earliest deadline in the ordered queue. +2. When a deadline fires, it checks cancellation. +3. If not cancelled, it posts the job back to the `io_context` scheduler thread. + +Important: +- Scheduled callbacks do NOT run on the timer worker thread. +- They run on the scheduler thread via `io_context::post()` (through `ctx_post()`). +- This keeps all your async callbacks serialized on the runtime thread, matching the rest of the async core. + +Destruction: +- `~timer()` stops the worker and releases queued jobs. +- You can also call `stop()` explicitly. + +--- + +## Schedule a callback with `after()` + +### Signature + +```cpp +template +void after(duration d, Fn&& fn, cancel_token ct = {}); +``` + +### Behavior + +- Computes `deadline = clock::now() + d` +- Wraps your callable into a type-erased job +- Inserts it into the ordered queue +- Wakes the worker so it can recalculate the next deadline +- If `ct.is_cancelled()` is true when the deadline fires, the job is skipped + +### Example + +```cpp +#include +#include + +using namespace vix::async::core; + +static void run_example() +{ + io_context ctx; + auto& t = ctx.timers(); + + t.after(std::chrono::seconds(1), [&]() + { + // Runs on the io_context scheduler thread + // not on the timer worker thread + // Do short work here + }); + + ctx.run(); +} + +int main() +{ + run_example(); +} +``` + +Tip: +- Keep `after()` callbacks short. If you need CPU-heavy work, submit it to `ctx.cpu_pool()` and continue from there. + +--- + +## Coroutine sleep with `sleep_for()` + +### Signature + +```cpp +task sleep_for(duration d, cancel_token ct = {}); +``` + +### Behavior + +- Suspends the awaiting coroutine +- Schedules a timer entry that posts the coroutine handle back onto the scheduler thread +- When resumed, it continues on the `io_context` scheduler thread + +This gives you an ergonomic delay inside coroutines without blocking any thread. + +### Example + +```cpp +#include +#include +#include +#include + +using namespace vix::async::core; + +static task job(io_context& ctx) +{ + auto& t = ctx.timers(); + + // Hop onto scheduler thread (optional but common in this design) + co_await ctx.get_scheduler().schedule(); + + co_await t.sleep_for(std::chrono::milliseconds(250)); + co_await t.sleep_for(std::chrono::milliseconds(250)); + + co_return; +} + +static void run_example() +{ + io_context ctx; + + // Run coroutine in detached mode + spawn_detached(ctx, job(ctx)); + + ctx.run(); +} + +int main() +{ + run_example(); +} +``` + +--- + +## Cancellation + +Both `after()` and `sleep_for()` accept a `cancel_token`. + +- If the token is cancelled before the deadline, the entry is skipped. +- If you want cancellation errors (instead of "skip"), implement that at the call site: + - request cancel + - decide whether you treat "not fired" as success or as cancellation + +### Example with cancel_source + +```cpp +#include +#include +#include +#include +#include + +using namespace vix::async::core; + +static task cancellable_sleep(io_context& ctx) +{ + cancel_source src; + cancel_token ct = src.token(); + + // Cancel after 100ms + ctx.timers().after(std::chrono::milliseconds(100), [&]() + { + src.request_cancel(); + }); + + // This sleep will be skipped if cancellation is observed before firing + co_await ctx.timers().sleep_for(std::chrono::seconds(1), ct); + + co_return; +} + +static void run_example() +{ + io_context ctx; + spawn_detached(ctx, cancellable_sleep(ctx)); + ctx.run(); +} + +int main() +{ + run_example(); +} +``` + +Note: +- The current timer contract is "skip on cancel". +- If you want `sleep_for()` to throw `std::system_error(cancelled_ec())`, you can implement that in `sleep_for()` by checking the token and capturing an exception for the awaiting coroutine. + +--- + +## Ordering and tie breaking + +Timer entries are stored in a `std::multiset` ordered by: + +1. `when` (deadline time) +2. `id` (monotonic sequence number) + +This makes ordering stable even when multiple entries share the same deadline. + +--- + +## Common pitfalls + +- Using long work inside `after()` callback + - Fix: offload to `cpu_pool().submit(...)` and resume later. +- Forgetting to run the scheduler + - `timer` posts completions onto the scheduler thread, so you must call `ctx.run()`. +- Expecting exact timing + - Like any timer, wakeups depend on OS scheduling and load. Use it for delays and timeouts, not for precise real-time scheduling. + +--- + +## Recommended pattern + +Use the timer for: +- timeouts around I/O +- retries with backoff +- periodic tasks (reschedule inside the callback) +- coroutine delays + +Keep business logic: +- on the scheduler thread for short operations +- on the CPU pool for heavy compute + diff --git a/docs/modules/async/udp.md b/docs/modules/async/udp.md new file mode 100644 index 0000000..d316506 --- /dev/null +++ b/docs/modules/async/udp.md @@ -0,0 +1,250 @@ +# UDP (async/net) + +`udp.hpp` defines Vix.cpp's coroutine-first UDP abstraction: endpoints, datagram metadata, and an `udp_socket` interface that can be implemented by a runtime backend (typically Asio) and integrated with `vix::async::core::io_context`. + +UDP is message oriented: you send and receive datagrams. There is no connection and no delivery guarantee. This API models that directly. + +## Header + +```cpp +#include +``` + +Namespace: `vix::async::net` + +Related core types: +- `vix::async::core::task` +- `vix::async::core::cancel_token` +- `vix::async::core::io_context` + +--- + +## Types + +### udp_endpoint + +Represents a UDP endpoint by host and port. + +```cpp +struct udp_endpoint { + std::string host; + std::uint16_t port{0}; +}; +``` + +Notes: +- `host` can be a hostname or an IP string (IPv4 or IPv6). +- `port` is in host byte order. + +### udp_datagram + +Metadata returned by `async_recv_from()`. + +```cpp +struct udp_datagram { + udp_endpoint from; + std::size_t bytes{0}; +}; +``` + +Meaning: +- `from`: the sender endpoint. +- `bytes`: number of bytes written into the receive buffer. + +--- + +## udp_socket + +`udp_socket` is the abstract interface for coroutine-based UDP operations. + +```cpp +class udp_socket { +public: + virtual ~udp_socket() = default; + + virtual core::task async_bind(const udp_endpoint& bind_ep) = 0; + + virtual core::task async_send_to( + std::span buf, + const udp_endpoint& to, + core::cancel_token ct = {} + ) = 0; + + virtual core::task async_recv_from( + std::span buf, + core::cancel_token ct = {} + ) = 0; + + virtual void close() noexcept = 0; + virtual bool is_open() const noexcept = 0; +}; +``` + +### async_bind(bind_ep) + +Binds the socket to a local endpoint. + +- Completes when binding succeeds. +- Throws `std::system_error` on failure. + +### async_send_to(buf, to, ct) + +Sends one datagram. + +- Returns the number of bytes sent. +- Throws `std::system_error` on send failure or cancellation. + +### async_recv_from(buf, ct) + +Receives one datagram into `buf`. + +- Returns `udp_datagram` with sender + byte count. +- Throws `std::system_error` on receive failure or cancellation. + +### close() and is_open() + +- `close()` is idempotent. +- `is_open()` reports whether the underlying socket is open. + +--- + +## Factory + +### make_udp_socket(ctx) + +Creates a UDP socket associated with a core `io_context`. + +```cpp +std::unique_ptr make_udp_socket(core::io_context& ctx); +``` + +The concrete type is runtime dependent and typically backed by Asio. + +--- + +## Cancellation + +Most operations accept `core::cancel_token`: +- If cancellation is observed, the implementation should fail the operation by throwing `std::system_error(core::cancelled_ec())`. +- If you pass a default token `{}`, the operation is not cancel aware. + +Example token setup: + +```cpp +vix::async::core::cancel_source src; +auto ct = src.token(); + +// later, from another context or thread: +src.request_cancel(); +``` + +--- + +## Example: UDP echo (single socket) + +This example shows: +- bind +- recv_from +- send_to back to sender + +```cpp +#include +#include +#include + +#include +#include + +using namespace vix::async; + +static core::task udp_echo_server(core::io_context& ctx) +{ + auto sock = net::make_udp_socket(ctx); + + co_await sock->async_bind(net::udp_endpoint{"0.0.0.0", 9000}); + + std::array buf{}; + + while (sock->is_open()) + { + auto dg = co_await sock->async_recv_from(std::span(buf.data(), buf.size())); + + // echo back exactly what we received + std::span out(buf.data(), dg.bytes); + co_await sock->async_send_to(out, dg.from); + } + + co_return; +} + +int main() +{ + core::io_context ctx; + + // Spawn the server and run the scheduler + core::spawn_detached(ctx, udp_echo_server(ctx)); + ctx.run(); + + return 0; +} +``` + +--- + +## Example: UDP discovery ping + +A typical use case in Vix style is discovery and heartbeats. This sketch sends a ping datagram to a known endpoint. + +```cpp +#include +#include +#include + +#include +#include + +using namespace vix::async; + +static core::task send_ping(core::io_context& ctx) +{ + auto sock = net::make_udp_socket(ctx); + + // Optional: bind to an ephemeral port + co_await sock->async_bind(net::udp_endpoint{"0.0.0.0", 0}); + + std::array msg{}; + std::memcpy(msg.data(), "PING", 4); + + std::span out(msg.data(), msg.size()); + co_await sock->async_send_to(out, net::udp_endpoint{"127.0.0.1", 37020}); + + sock->close(); + co_return; +} + +int main() +{ + core::io_context ctx; + core::spawn_detached(ctx, send_ping(ctx)); + ctx.run(); + return 0; +} +``` + +--- + +## Behavior and design notes + +- UDP is datagram based: each `async_send_to()` corresponds to one datagram, and `async_recv_from()` returns one datagram. +- Your receive buffer size matters: if the datagram is larger than the buffer, the backend may truncate it (backend specific). +- `udp_endpoint.host` is a string in the public API. Backend implementations usually resolve it to an address internally. +- `close()` should wake any pending operations according to backend rules. If a pending coroutine resumes, it should see an error (for example `errc::closed`) or a cancellation style error depending on implementation. + +--- + +## Common patterns + +- Bind once, then loop on `async_recv_from()` for servers. +- For clients: optionally bind to port 0, then send and receive. +- Combine UDP with `timer` for retry windows, and with `cancel_token` for shutdown. + diff --git a/docs/modules/async/when.md b/docs/modules/async/when.md new file mode 100644 index 0000000..af7e6fe --- /dev/null +++ b/docs/modules/async/when.md @@ -0,0 +1,225 @@ +# when + +This page documents `when_all()` and `when_any()` from `vix::async::core`. + +They let you run multiple `task` concurrently and wait for: +- all tasks to finish (`when_all`) +- the first task to finish (`when_any`) + +Both utilities are scheduler-driven and integrate with Vix's `task` and `scheduler` types. + +## Header + +```cpp +#include +``` + +## Mental model + +### What these functions do + +- `when_all(sched, t1, t2, ...)` + - starts all tasks concurrently + - resumes when every task finishes + - returns a tuple of results in the same order as inputs + +- `when_any(sched, t1, t2, ...)` + - starts all tasks concurrently + - resumes when the first task finishes + - returns: + - the winner index + - a tuple where the winner slot contains its result + +### Why the scheduler is required + +Both functions: +- post work onto the scheduler +- resume the awaiting coroutine by posting the continuation back onto the scheduler thread + +So the scheduler is the "home thread" for resumption and coordination. + +## Return types + +### void tasks are mapped to std::monostate + +To keep a consistent tuple type, `task` results become `std::monostate` in output tuples. + +Example: +```cpp +task a(); +task b(); + +auto tup = co_await when_all(sched, a(), b()); +// type: std::tuple +``` + +### when_all return type + +```cpp +template +task, std::monostate, Ts>...>> +when_all(scheduler& sched, task... ts); +``` + +### when_any return type + +```cpp +template +task, std::monostate, Ts>...>>> +when_any(scheduler& sched, task... ts); +``` + +- `.first` is the winner index in `[0..N-1]` +- `.second` is the tuple of results (only the winner is guaranteed to be populated meaningfully) + +## Exception behavior + +### when_all + +- each runner captures exceptions +- the first captured exception is rethrown when `when_all` resumes +- result tuple is returned only if no exception was captured + +### when_any + +- an exception captured by any runner is stored in `st->ex` +- the awaiter rethrows if `st->ex` is set when resuming + +Important: +- This design is simple and deterministic. +- It means a losing task that throws could still cause `when_any` to throw, depending on timing. +- If you want "only winner decides", evolve the state to store the winner's exception only. + +## Concurrency and lifetime notes + +- Internally, both functions start runner coroutines in detached mode using `task::start(scheduler&)`. +- Shared state (`std::shared_ptr`) owns: + - remaining counters / done flag + - continuation handle + - stored results + - first exception (if any) + +This keeps coordination safe even if tasks finish on different timings. + +## Examples + +### Example 1: when_all with values + +```cpp +#include +#include +#include + +using vix::async::core::task; + +task fetch_a() +{ + co_return 10; +} + +task fetch_b() +{ + co_return std::string("ok"); +} + +task demo(vix::async::core::io_context& ctx) +{ + auto& sched = ctx.get_scheduler(); + + auto [a, b] = co_await when_all(sched, fetch_a(), fetch_b()); + // a == 10 + // b == "ok" + + co_return; +} +``` + +### Example 2: when_all with void tasks + +```cpp +task step1() +{ + co_return; +} + +task step2() +{ + co_return 42; +} + +task demo(vix::async::core::io_context& ctx) +{ + auto& sched = ctx.get_scheduler(); + + auto tup = co_await when_all(sched, step1(), step2()); + // type: std::tuple + + auto x = std::get<1>(tup); // 42 + (void)x; + + co_return; +} +``` + +### Example 3: when_any (race) + +```cpp +task fast() +{ + co_return 1; +} + +task slow() +{ + co_return 2; +} + +task demo(vix::async::core::io_context& ctx) +{ + auto& sched = ctx.get_scheduler(); + + auto [idx, tup] = co_await when_any(sched, slow(), fast()); + + // idx is either 0 or 1 + // winner result is in std::get(tup) + + int winner = (idx == 0) ? std::get<0>(tup) : std::get<1>(tup); + (void)winner; + + co_return; +} +``` + +### Example 4: running end-to-end + +Minimal pattern: +- build your top-level coroutine +- schedule it on the scheduler +- run the context loop + +```cpp +#include +#include + +vix::async::core::task main_task(vix::async::core::io_context& ctx); + +int main() +{ + vix::async::core::io_context ctx; + + vix::async::core::spawn_detached(ctx, main_task(ctx)); + ctx.run(); + + return 0; +} +``` + +## Design notes + +- `when_all` uses a `remaining` counter and resumes the continuation when it reaches zero. +- `when_any` uses an atomic `done` flag and only the first finisher posts the continuation. +- Resumption is posted back to the scheduler so you get deterministic thread affinity. + +If you later want cancellation of losers for `when_any`, pair it with `cancel_source` and pass `cancel_token` into the tasks you start, then cancel the losers when the winner completes. + diff --git a/docs/modules/cache/guide.md b/docs/modules/cache/guide.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/modules/cache/index.md b/docs/modules/cache/index.md new file mode 100644 index 0000000..8909518 --- /dev/null +++ b/docs/modules/cache/index.md @@ -0,0 +1,275 @@ +# Cache + +The cache module is a small, explicit, policy driven cache layer designed for offline first systems. + +It is built around three ideas: + +- A `CacheStore` persists entries (memory, file, etc). +- A `CachePolicy` decides freshness and stale reuse rules. +- A `CacheContext` carries runtime semantics (offline, network error) that influence decisions. + +This module is intentionally deterministic: the cache does not perform network IO, does not hide state transitions, and makes reuse decisions based on explicit inputs. + +--- + +## Concepts + +### CacheEntry + +A cached response entry. + +Fields (typical HTTP use): + +- `status` (int) +- `body` (string) +- `headers` (map) +- `created_at_ms` (int64) + +This is the minimal metadata required for policy decisions. + +### CacheContext + +A small runtime context used during `get()`: + +- `offline`: true when the system is considered offline +- `network_error`: true when the last request failed due to network issues + +Helpers: + +- `CacheContext::Online()` +- `CacheContext::Offline()` +- `CacheContext::NetworkError()` + +There is also `CacheContextMapper` to derive context from `vix::net::NetworkProbe` and a request outcome. + +### CachePolicy + +Defines freshness and stale reuse rules, using only entry age: + +- `ttl_ms`: entry is fresh when `age_ms <= ttl_ms` +- `stale_if_error_ms`: maximum age allowed when `network_error` is true +- `stale_if_offline_ms`: maximum age allowed when `offline` is true +- flags to enable or disable stale reuse on error or offline + +Helpers: + +- `is_fresh(age_ms)` +- `allow_stale_error(age_ms)` +- `allow_stale_offline(age_ms)` + +### CacheStore + +Storage backend interface: + +- `put(key, entry)` +- `get(key) -> optional` +- `erase(key)` +- `clear()` + +Stores are synchronous by design to keep behavior explicit and testable. + +### Cache facade + +`Cache` composes a policy and a store: + +- `get(key, now_ms, ctx) -> optional` +- `put(key, entry)` +- `prune(now_ms) -> size_t` + +Policy is consulted on `get()` to decide whether the stored entry is usable for the given context. + +--- + +## Cache keys + +### CacheKey + +A deterministic key builder for request based caching. + +It normalizes: + +- method uppercased +- query parameters sorted +- selected header names lowercased +- header values trimmed + +Key format: + +`METHOD path?sorted_query |h:header=value;header=value;` + +Use it for HTTP GET caching where equivalent requests must map to the same cache entry. + +Example: + +```cpp +#include + +std::unordered_map headers = { + {"Accept", "application/json"}, + {"Authorization", "Bearer X"} // typically excluded +}; + +std::string key = vix::cache::CacheKey::fromRequest( + "GET", + "/api/products", + "page=2&limit=20", + headers, + {"accept"} // include only what should vary the cache +); +``` + +Tip: include as few headers as possible. Prefer stable headers like `accept` or a locale header. Avoid volatile ones like authorization unless you really need per user caching. + +--- + +## Stores + +### MemoryStore + +Simple hash map store. + +- Thread safe via a single mutex +- No eviction +- No persistence + +Good for tests, small caches, and short lived processes. + +### LruMemoryStore + +An in memory LRU store. + +- O(1) average put get erase +- Fixed max number of entries +- Thread safe via a single mutex + +Good for hot path caching where you want bounded memory. + +### FileStore + +A file backed store that persists all entries into a single JSON file. + +- Loads lazily on first access +- Keeps an in memory map protected by a mutex +- Any mutation triggers a flush to disk + +Config: + +- `file_path` (default `./.vix/cache_http.json`) +- `pretty_json` (debug friendly, larger file) + +This is best for durable local caching (offline first HTTP cache) when you want the cache to survive restarts. + +--- + +## Typical usage + +This example shows the intended workflow for HTTP GET caching: + +1) Build a deterministic key. +2) Try `cache.get()` with the current context. +3) If cache hit, serve it. +4) If miss, fetch from network, then `cache.put()`. + +```cpp +#include +#include +#include +#include +#include + +using vix::cache::Cache; +using vix::cache::CacheContext; +using vix::cache::CacheEntry; +using vix::cache::CachePolicy; +using vix::cache::LruMemoryStore; + +static std::int64_t now_ms(); // your clock helper + +int main() +{ + CachePolicy policy; + policy.ttl_ms = 30'000; + policy.stale_if_error_ms = 5 * 60'000; + policy.stale_if_offline_ms = 30 * 60'000; + + auto store = std::make_shared(LruMemoryStore::Config{.max_entries = 2048}); + Cache cache(policy, store); + + std::unordered_map headers = {{"Accept", "application/json"}}; + + std::string key = vix::cache::CacheKey::fromRequest( + "GET", + "/api/products", + "page=1&limit=20", + headers, + {"accept"} + ); + + CacheContext ctx = CacheContext::Online(); + if (auto hit = cache.get(key, now_ms(), ctx)) + { + // Serve from cache + // status = hit->status + // body = hit->body + // headers = hit->headers + return 0; + } + + // Network fetch happens outside of cache. + // After success: + CacheEntry e; + e.status = 200; + e.body = "{\"ok\":true}"; + e.created_at_ms = now_ms(); + + cache.put(key, e); + return 0; +} +``` + +--- + +## Offline and network error behavior + +The cache does not detect offline state by itself. You pass it in via `CacheContext`. + +- When `ctx.offline == true`, policy may allow stale reuse up to `stale_if_offline_ms`. +- When `ctx.network_error == true`, policy may allow stale reuse up to `stale_if_error_ms`. + +If neither applies and the entry is older than `ttl_ms`, the cache returns `nullopt`. + +To derive context from `vix::net::NetworkProbe`, use `CacheContextMapper`: + +```cpp +#include + +auto ctx = vix::cache::contextFromProbeAndOutcome( + probe, + now_ms, + vix::cache::RequestOutcome::NetworkError +); +``` + +--- + +## Pruning + +`Cache::prune(now_ms)` removes expired entries according to the policy. + +Call it: + +- at startup +- periodically (timer) +- before persisting big batches +- after long offline sessions + +Exact pruning behavior depends on the store implementation and the Cache facade implementation. + +--- + +## Design notes + +- This cache layer is optimized for explicitness, determinism, and offline first correctness. +- It is ideal for HTTP GET caching and store and forward style systems. +- If you need advanced validation (ETag, If-Modified-Since), keep it explicit at the HTTP layer and store any validation metadata in `CacheEntry::headers`. + diff --git a/docs/modules/cli.md b/docs/modules/cli.md deleted file mode 100644 index 8448469..0000000 --- a/docs/modules/cli.md +++ /dev/null @@ -1,304 +0,0 @@ -# 🧩 Vix.cpp — CLI Module - -### Modern C++ Runtime Tooling • Zero-Friction Development • Fast Web Apps - -![C++](https://img.shields.io/badge/C%2B%2B-20-blue.svg) -![License](https://img.shields.io/badge/License-MIT-green) -![Status](https://img.shields.io/badge/Status-Stable-success) -![Platform](https://img.shields.io/badge/Platform-Linux%20|%20macOS%20|%20Windows-lightgrey) -![Runtime](https://img.shields.io/badge/Runtime-Vix.cpp%201.x-orange) - -> **Vix CLI** is the official command-line interface for -> **Vix.cpp** — the modern C++ backend runtime. -> -> It provides a **professional, modern, runtime-like developer experience** -> for C++, comparable to **Python**, **Node.js**, **Deno**, or **Bun**. - ---- - -# 🚀 Overview - -The Vix CLI (`vix`) brings modern runtime ergonomics to C++: - -- Instant project creation -- Smart CMake-based builds -- Friendly compiler diagnostics -- Sanitizer-first validation -- Script-like execution of `.cpp` files -- Packaging & artifact verification -- Built-in interactive REPL (**default**) - -Running `vix` with no arguments launches the **interactive REPL**. - ---- - -# ⚙️ Features - -## 🧠 Built-in REPL (default) - -```bash -vix -``` - -- Variables & expressions -- JSON literals -- Math evaluation -- Runtime APIs (`Vix.cwd()`, `Vix.env()`, etc.) -- Script-like exploration - -Explicit mode: - -```bash -vix repl -``` - ---- - -## 🏗️ Project scaffolding - -```bash -vix new blog -``` - -Creates: - -- CMake-based project -- Modern C++20 structure -- Ready-to-run Vix app - ---- - -## ⚡ Smart build system - -```bash -vix build -``` - -- Uses CMake presets automatically -- Parallel builds -- Colored logs & spinners -- Clean Ctrl+C handling - ---- - -## 🚀 Run applications - -```bash -vix run -``` - -- Auto-build if required -- Real-time logs -- Runtime log-level injection - -Script mode: - -```bash -vix run demo.cpp -``` - ---- - -## 🧪 Check & Tests (Sanitizers ready) - -Compile-only validation: - -```bash -vix check -vix check demo.cpp -``` - -With sanitizers: - -```bash -vix check demo.cpp --san -vix check demo.cpp --asan -vix check demo.cpp --ubsan -vix check demo.cpp --tsan -``` - -Run tests: - -```bash -vix tests -vix tests --san -``` - ---- - -## 📦 Packaging & Verification - -Create a distribution artifact: - -```bash -vix pack --name blog --version 1.0.0 -``` - -Verify artifacts: - -```bash -vix verify dist/blog@1.0.0 -vix verify dist/blog@1.0.0 --require-signature -``` - ---- - -## 🧠 ErrorHandler — your C++ teacher - -- Explains template & overload errors -- Detects missing includes -- Highlights the _first real error_ -- Provides actionable hints - ---- - -# 🧰 Commands - -```bash -vix [options] -``` - -| Command | Description | -| ------------------------- | ---------------------------- | -| `vix` | Start REPL (default) | -| `vix repl` | Start REPL explicitly | -| `vix new ` | Create a new project | -| `vix build [name]` | Configure + build | -| `vix run [name] [--args]` | Build and run | -| `vix dev [name]` | Dev mode (watch & reload) | -| `vix check [path]` | Compile-only validation | -| `vix tests [path]` | Run tests | -| `vix pack [options]` | Create distribution artifact | -| `vix verify [options]` | Verify artifact | -| `vix orm ` | ORM tooling | -| `vix help [command]` | Show help | -| `vix version` | Show version | - ---- - -# 🧪 Usage Examples - -```bash -vix -vix new api -cd api -vix dev -vix check --san -vix tests -vix pack --name api --version 1.0.0 -vix verify dist/api@1.0.0 -``` - ---- - -# 🧩 Architecture - -The CLI is built around a command dispatcher: - -```cpp -std::unordered_map commands; -``` - -### Main components - -| Path | Responsibility | -| -------------------------------- | -------------------- | -| `include/vix/cli/CLI.hpp` | CLI entry & parsing | -| `src/CLI.cpp` | Command routing | -| `src/ErrorHandler.cpp` | Compiler diagnostics | -| `src/commands/ReplCommand.cpp` | Interactive REPL | -| `src/commands/CheckCommand.cpp` | Validation | -| `src/commands/PackCommand.cpp` | Packaging | -| `src/commands/VerifyCommand.cpp` | Verification | - ---- - -# 🔧 Build & Installation - -### Standalone CLI build - -```bash -git clone https://github.com/vixcpp/vix.git -cd vix/modules/cli -cmake -B build -S . -cmake --build build -j$(nproc) -``` - -Binary: - -```bash -./build/vix -``` - ---- - -### Full Vix build - -```bash -cd vix -cmake -B build -S . -cmake --build build -``` - ---- - -# ⚙️ Configuration - -### Environment variables - -| Variable | Description | -| --------------------- | ------------------------- | -| `VIX_LOG_LEVEL` | Runtime log level | -| `VIX_STDOUT_MODE` | `line` for real-time logs | -| `VIX_MINISIGN_SECKEY` | Secret key for `pack` | -| `VIX_MINISIGN_PUBKEY` | Public key for `verify` | - ---- - -# 📦 CLI Help Output - -```sql -Vix.cpp — Modern C++ backend runtime -Version: v1.x.x - -Usage: - vix [GLOBAL OPTIONS] [ARGS...] - vix help - -Quick start: - vix new api - cd api && vix dev - vix pack --name api --version 1.0.0 && vix verify - -Commands: - Project: - new Create a new Vix project - build [name] Configure + build - run [name] Build and run - dev [name] Dev mode - check [path] Compile-only validation - tests [path] Run tests - - Packaging & security: - pack Create distribution artifact - verify Verify artifact or package - - REPL: - repl Start interactive REPL - (default) Run `vix` to start the REPL - -Global options: - --verbose - -q, --quiet - --log-level - -h, --help - -v, --version - -``` - ---- - -# 🧾 License - -**MIT License** © [Gaspard Kirira](https://github.com/gkirira) -See [`LICENSE`](../../LICENSE) for details. diff --git a/docs/modules/cli/add.md b/docs/modules/cli/add.md new file mode 100644 index 0000000..407850f --- /dev/null +++ b/docs/modules/cli/add.md @@ -0,0 +1,81 @@ +# vix add + +Add a dependency from the Vix registry. + +--- + +## Usage + +```bash +vix add /@ +``` + +--- + +## Description + +`vix add`: + +- Adds a dependency to your project +- Requires an exact version (V1 registry model) +- Pins the resolved commit SHA in `vix.lock` +- Works with the local registry index + +Before adding a dependency, you should sync the registry. + +--- + +## Sync Registry + +```bash +vix registry sync +``` + +--- + +## Example + +```bash +vix add gaspardkirira/tree@0.1.0 +``` + +--- + +## Versioning Rules + +- Exact version required +- No version ranges in V1 +- Version is resolved to a commit SHA +- The commit is stored in `vix.lock` + +This ensures: + +- Reproducible builds +- Deterministic dependency resolution +- No implicit upgrades + +--- + +## What Happens Internally + +1. Registry index is searched locally +2. Version is resolved to a commit +3. Entry is written to `vix.lock` +4. Dependencies are installed via: + +```bash +vix deps +``` + +--- + +## Notes + +- Requires prior `vix registry sync` +- Lockfile must be committed to version control +- Exact version required in current registry model + +--- + +`vix add` is part of the deterministic dependency workflow in Vix. + diff --git a/docs/modules/cli/build.md b/docs/modules/cli/build.md new file mode 100644 index 0000000..06ebf87 --- /dev/null +++ b/docs/modules/cli/build.md @@ -0,0 +1,181 @@ +# vix build + +Configure and build a Vix project using embedded CMake presets. + +--- + +## Usage + +```bash +vix build [options] -- [cmake args...] +``` + +--- + +## Description + +`vix build` wraps CMake + Ninja with: + +- Embedded presets +- Strong signature cache (tool versions + CMake file hashes) +- Optional fast no-op exit +- Auto sccache / ccache detection +- Auto mold / lld detection +- Clean log separation + +Designed for ultra-fast iteration loops. + +--- + +# Presets + +``` +dev -> Ninja + Debug (build-dev) +dev-ninja -> Ninja + Debug (build-dev-ninja) +release -> Ninja + Release (build-release) +``` + +Default preset: `dev` + +--- + +# Options + +``` +--preset dev | dev-ninja | release +--target Cross-compilation target triple +--sysroot Sysroot for cross toolchain +--static Enable VIX_LINK_STATIC=ON +-j, --jobs Parallel jobs (default: CPU count) +--clean Force reconfigure (ignore signature cache) +--no-cache Disable signature cache +--fast Exit immediately if up-to-date (Ninja dry-run) +--linker auto | default | mold | lld +--launcher auto | none | sccache | ccache +--no-status Disable NINJA_STATUS progress formatting +--no-up-to-date Disable Ninja dry-run check +-d, --dir Project directory +-q, --quiet Minimal output +--targets List detected cross toolchains +--cmake-verbose Show full CMake output +--build-target Build specific CMake target +-h, --help Show help +``` + +--- + +# Environment Variables + +``` +VIX_BUILD_HEARTBEAT=1 +``` + +Enables heartbeat output when build is silent for several seconds. + +--- + +# Examples + +Basic build: + +```bash +vix build +``` + +Fast loop: + +```bash +vix build --fast +``` + +Release: + +```bash +vix build --preset release +``` + +Static release: + +```bash +vix build --preset release --static +``` + +With launcher + linker: + +```bash +vix build --launcher sccache --linker mold +``` + +Cross-compile: + +```bash +vix build --target aarch64-linux-gnu +``` + +Pass raw CMake arguments: + +```bash +vix build -- -DVIX_SYNC_BUILD_TESTS=ON +``` + +Parallel jobs: + +```bash +vix build -j 8 +``` + +--- + +# Logs + +Each preset has its own directory: + +``` +build-dev*/configure.log +build-dev*/build.log +``` + +Logs are always written even if console output is minimal. + +--- + +# Signature Cache + +Vix calculates a strong signature based on: + +- Compiler version +- CMake version +- Linker version +- Relevant CMake file hashes + +If nothing changed, configure is skipped. + +`--clean` bypasses this cache. + +--- + +# Fast Mode + +``` +vix build --fast +``` + +If Ninja reports the project is already up-to-date, Vix exits immediately without full build processing. + +Optimized for tight dev loops. + +--- + +# Design Goals + +- Deterministic builds +- Fast iteration +- Clean output +- Explicit configuration +- Cross-compilation ready +- Secure and reproducible artifacts + +--- + +`vix build` is the foundation of the Vix development workflow. + diff --git a/docs/modules/cli/deps.md b/docs/modules/cli/deps.md new file mode 100644 index 0000000..b90cd66 --- /dev/null +++ b/docs/modules/cli/deps.md @@ -0,0 +1,126 @@ +# Dependencies + +The `vix deps` command installs project dependencies defined in `vix.lock` +into a local directory and generates a CMake include file. + +This system is: + +- Deterministic (lockfile-based) +- Local (no global state) +- CMake-native +- Explicit + +--- + +## Command + +```bash +vix deps +``` + +--- + +## What it does + +When executed, Vix: + +1. Reads `vix.lock` +2. Downloads required packages +3. Installs them into: + +```text +./.vix/deps +``` + +4. Generates: + +```text +./.vix/vix_deps.cmake +``` + +You must include this file in your root `CMakeLists.txt`. + +--- + +## Typical workflow + +Add dependencies: + +```bash +vix add gaspardkirira/tree@0.4.0 +vix add gaspardkirira/binary_search@0.1.1 +``` + +Install them: + +```bash +vix deps +``` + +--- + +## CMake integration + +In your root `CMakeLists.txt`: + +```cmake +include(${CMAKE_SOURCE_DIR}/.vix/vix_deps.cmake) +``` + +This makes dependency targets available to your project. + +Example usage: + +```cmake +target_link_libraries(my_app + PRIVATE + tree + binary_search +) +``` + +--- + +## vix.lock + +The `vix.lock` file ensures: + +- Exact version resolution +- Reproducible builds +- No surprise upgrades +- Same dependency graph across machines + +Never edit this file manually. + +--- + +## Design principles + +- No global install directory +- No implicit include paths +- No hidden transitive linkage +- Fully explicit linking in CMake + +This keeps builds predictable and safe. + +--- + +## Summary + +`vix deps` is the install step of the Vix dependency system. + +It transforms: + +```text +vix.lock +``` + +into: + +```text +./.vix/deps +./.vix/vix_deps.cmake +``` + +Simple. Deterministic. CMake-native. + diff --git a/docs/modules/cli/dev.md b/docs/modules/cli/dev.md new file mode 100644 index 0000000..89210eb --- /dev/null +++ b/docs/modules/cli/dev.md @@ -0,0 +1,131 @@ +# vix dev + +Developer-friendly entrypoint for running Vix applications with auto-reload. + +Internally equivalent to: + +``` +vix run --watch +``` + +But optimized for development workflows. + +--- + +## Usage + +```bash +vix dev [name] [options] [-- app-args...] +``` + +--- + +## Description + +`vix dev`: + +- Configures the project (if needed) +- Builds it +- Runs it +- Watches for file changes +- Automatically rebuilds and restarts + +Works for: + +- CMake projects +- Single .cpp scripts +- .vix manifests + +--- + +## Options + +``` +--force-server Force classification as long-lived server +--force-script Force classification as short-lived script +--watch, --reload Enable hot reload (default in dev) +-j, --jobs Parallel compile jobs +--log-level trace | debug | info | warn | error | critical +--verbose Shortcut for debug logs +-q, --quiet Show warnings and errors only +``` + +--- + +## Examples + +Run current project: + +```bash +vix dev +``` + +Run named app: + +```bash +vix dev api +``` + +Script mode with reload: + +```bash +vix dev server.cpp +``` + +Pass runtime args (after `--`): + +```bash +vix dev server.cpp -- --port 8080 +``` + +Force server mode: + +```bash +vix dev server.cpp --force-server +``` + +Force script mode: + +```bash +vix dev tool.cpp --force-script +``` + +--- + +## Mode Classification + +Vix automatically detects whether your app is: + +- Server-like (long-running) +- Script-like (short-lived) + +You can override this behavior with: + +``` +--force-server +--force-script +``` + +--- + +## When To Use vix dev + +- Backend API development +- Rapid prototyping +- Iterating on async servers +- Testing script logic +- Live development with auto-rebuild + +--- + +## Design Goals + +- Zero friction development loop +- Automatic rebuild on change +- Clean output +- Same behavior across project and script modes + +--- + +`vix dev` is the recommended command during development. + diff --git a/docs/modules/cli/index.md b/docs/modules/cli/index.md new file mode 100644 index 0000000..98224f2 --- /dev/null +++ b/docs/modules/cli/index.md @@ -0,0 +1,188 @@ +# CLI Overview + +The Vix CLI is the central interface for building, running, packaging, and managing Vix projects. + +It is designed to be: + +- Explicit +- Predictable +- Modular +- Production-oriented + +--- + +## Version Example + +```bash +vix -h +``` + +Example output: + +``` +Vix.cpp — Modern C++ backend runtime +Version: v1.x.x +``` + +--- + +## Core Philosophy + +The CLI is structured around: + +- Project lifecycle +- Dependency management +- Packaging and security +- Network runtime +- Database migrations +- Developer tooling + +Everything is explicit. +No hidden background behavior. + +--- + +# Command Categories + +## 1. Project + +``` +vix new +vix build +vix run +vix dev +vix check +vix tests +vix repl +``` + +Used to create and manage applications. + +--- + +## 2. Project Structure (Modules) + +``` +vix modules +``` + +Opt-in modular system for adding and validating Vix modules. + +--- + +## 3. Network + +``` +vix p2p +``` + +Run a P2P node with TCP transport and discovery. + +--- + +## 4. Registry + +``` +vix registry +vix add @ +vix search +vix remove +vix list +vix store +vix publish +vix deps +``` + +Manages dependencies via a git-based registry model. + +Fully offline search supported. + +--- + +## 5. Packaging & Security + +``` +vix pack +vix verify +vix install +``` + +Secure artifact generation and verification: + +- SHA256 +- Minisign support +- Versioned distributions + +--- + +## 6. Database (ORM) + +``` +vix orm +``` + +Manage migrations, status, rollback, and schema operations. + +--- + +## 7. Info + +``` +vix help +vix version +``` + +Show help and version information. + +--- + +# Global Options + +``` +--verbose +--quiet +--log-level +-h / --help +-v / --version +``` + +Supported log levels: + +``` +trace | debug | info | warn | error | critical +``` + +--- + +# Quick Start Example + +```bash +vix new api +cd api +vix dev +``` + +Package and verify: + +```bash +vix pack --version 1.0.0 +vix verify +``` + +--- + +# Design Goals + +- Zero hidden dependency resolution +- Reproducible builds +- Explicit version pinning +- Secure artifact validation +- Clean project structure +- Developer-first experience + +--- + +The CLI is the backbone of Vix workflows. + +It connects development, packaging, registry, networking, and runtime in one consistent interface. + diff --git a/docs/modules/cli/install.md b/docs/modules/cli/install.md new file mode 100644 index 0000000..aa38034 --- /dev/null +++ b/docs/modules/cli/install.md @@ -0,0 +1,109 @@ +# vix install + +Install a Vix package into the local store. + +--- + +## Usage + +```bash +vix install --path [options] +``` + +--- + +## Description + +`vix install`: + +- Installs a packaged project into the local Vix store +- Verifies payload digest and checksums +- Verifies signature (if present) +- Copies package into deterministic store layout + +By default, verification is enabled. + +--- + +## Options + +```bash +-p, --path Package folder or .vixpkg artifact (required) +--store Override store root +--force Overwrite if already installed +--no-verify Skip verification (NOT recommended) +--verbose Print detailed checks and copied files +--require-signature Fail if signature missing or invalid +--pubkey minisign public key +-h, --help Show help +``` + +--- + +## Store Location + +Default resolution order: + +1. `VIX_STORE` +2. `XDG_DATA_HOME/vix` +3. `~/.local/share/vix` + +Final layout: + +``` +/packs///-/ +``` + +--- + +## Examples + +Install archive: + +```bash +vix install --path ./dist/blog@1.0.0.vixpkg +``` + +Install folder package: + +```bash +vix install --path ./dist/blog@1.0.0 +``` + +Force overwrite: + +```bash +vix install --path ./dist/blog@1.0.0 --force +``` + +Require signature verification: + +```bash +vix install --path ./blog@1.0.0.vixpkg --require-signature --pubkey ./keys/vix-pack.pub +``` + +--- + +## Security Notes + +- Verification runs by default +- `--no-verify` disables integrity checks (not recommended) +- `--require-signature` enforces cryptographic verification +- Public key resolution: + - `--pubkey` + - `VIX_MINISIGN_PUBKEY` + +--- + +## Typical Workflow + +```bash +vix pack --version 1.0.0 +vix verify --require-signature +vix install --path ./dist/blog@1.0.0.vixpkg +``` + +--- + +`vix install` moves verified packages into the reproducible Vix store. + diff --git a/docs/modules/cli/list.md b/docs/modules/cli/list.md new file mode 100644 index 0000000..5d869f6 --- /dev/null +++ b/docs/modules/cli/list.md @@ -0,0 +1,64 @@ +# vix list + +List project dependencies from `vix.lock`. + +--- + +## Usage + +```bash +vix list +``` + +--- + +## Description + +`vix list`: + +- Reads the local `vix.lock` +- Displays all pinned dependencies +- Shows exact versions +- Shows resolved commit references + +This command does not access the network. + +--- + +## Example + +```bash +vix list +``` + +Example output: + +``` +gaspardkirira/tree@0.1.0 +adastra/json@0.3.2 +``` + +--- + +## Behavior Notes + +- Works offline +- Requires a `vix.lock` file +- Reflects the exact state of the project +- Useful for auditing dependency graph + +--- + +## Typical Workflow + +After adding or removing: + +```bash +vix add gaspardkirira/tree@0.1.0 +vix list +``` + +--- + +`vix list` provides a deterministic view of your dependency state. + diff --git a/docs/modules/cli/modules.md b/docs/modules/cli/modules.md new file mode 100644 index 0000000..dd27359 --- /dev/null +++ b/docs/modules/cli/modules.md @@ -0,0 +1,193 @@ +# Vix Modules + +The `vix modules` command enables a strict, app-first module system for any CMake project. + +The design is inspired by Go modules philosophy: +- Explicit dependencies +- Strict public/private boundaries +- No accidental header leakage +- Enforced architectural discipline + +--- + +## Command Overview + +```bash +vix modules [options] +``` + +### Subcommands + +```text +init Initialize modules mode +add Create a module skeleton +check Validate module safety rules +``` + +### Global Options + +```text +-d, --dir Project root (default: current) +--project Override project name +--no-patch Do not patch root CMakeLists.txt +--patch Patch root CMakeLists.txt (default) +--no-link Do not auto-link module into main target +--link Auto-link module into main target (default) +-h, --help Show help +``` + +--- + +# 1. Initialize Modules Mode + +```bash +vix modules init +``` + +This creates: + +```text +modules/ +cmake/vix_modules.cmake +``` + +It can optionally patch your root `CMakeLists.txt`. + +Example root patch: + +```cmake +include(cmake/vix_modules.cmake) +vix_modules_enable() +``` + +--- + +# 2. Add a Module + +```bash +vix modules add auth +``` + +Creates: + +```text +modules/auth/ + include/auth/ + src/ + CMakeLists.txt +``` + +Public header example: + +```cpp +// modules/auth/include/auth/api.hpp +#pragma once + +namespace auth { + void login(); +} +``` + +Private implementation: + +```cpp +// modules/auth/src/api.cpp +#include + +namespace auth { + void login() {} +} +``` + +Generated CMake target: + +```cmake +add_library(_auth) +add_library(::auth ALIAS _auth) + +target_include_directories(_auth + PUBLIC modules/auth/include + PRIVATE modules/auth/src +) +``` + +--- + +# 3. Explicit Cross-Module Dependency + +If `products` depends on `auth`, you must declare it explicitly: + +```cmake +target_link_libraries(_products + PUBLIC ::auth +) +``` + +No implicit dependency resolution is allowed. + +--- + +# 4. Module Structure Contract + +Each module must follow: + +```text +modules//include//... public headers +modules//src/... private implementation +``` + +Public include style: + +```cpp +#include +``` + +Never: + +```cpp +#include "modules/auth/src/internal.hpp" +``` + +--- + +# 5. Validate Module Safety + +```bash +vix modules check +``` + +This validates: + +- Public headers do not include private headers +- Cross-module usage is explicitly linked +- Include structure follows contract +- No accidental boundary violations + +--- + +# Design Philosophy + +Vix modules enforce: + +- Architectural clarity +- Dependency correctness +- Long-term maintainability +- Build-time safety + +Everything must be explicit. +Nothing is automatic. +No hidden coupling. + +--- + +# Minimal Workflow Example + +```bash +vix modules init +vix modules add auth +vix modules add products +vix modules check +``` + +This results in a clean, explicit, Go-like module architecture for C++. + diff --git a/docs/modules/cli/new.md b/docs/modules/cli/new.md new file mode 100644 index 0000000..3bb25d7 --- /dev/null +++ b/docs/modules/cli/new.md @@ -0,0 +1,129 @@ +# vix new + +Create a new Vix project. + +--- + +## Usage + +```bash +vix new [options] +``` + +--- + +## Options + +``` +-d, --dir Base directory where the project folder will be created +--app Generate an application template +--lib Generate a library template (header-only) +--force Overwrite if directory exists (no prompt) +``` + +--- + +## Environment Variables + +``` +VIX_NONINTERACTIVE=1 Disable interactive prompts +CI=1 Disable interactive prompts +``` + +--- + +# Interactive Mode + +When no template flag is provided, Vix will prompt: + +## Template + +- Application +- Library (header-only) + +## Features (Application) + +Optional: + +- ORM (database layer) +- Sanitizers (debug only) +- Static C++ runtime + +## Advanced + +- Full static binary (maps to VIX_LINK_FULL_STATIC=ON) + +--- + +# Examples + +Create an application: + +```bash +vix new api +``` + +Create a library: + +```bash +vix new tree --lib +``` + +Create inside a directory: + +```bash +vix new blog -d ./projects +``` + +Force overwrite: + +```bash +vix new api --force +``` + +--- + +# Output Example (Application) + +``` +✔ Project created. +Location: /home/user/api + +Next steps: + cd api/ + vix build + vix run +``` + +Tip: + +``` +vix dev +``` + +--- + +# Output Example (Library) + +``` +✔ Project created. +Location: /home/user/test_lib + +Next steps: + cd test_lib/ + vix tests +``` + +--- + +# Behavior Notes + +- If directory exists, you will be prompted unless `--force` is used. +- Library template is header-only by default. +- Application template includes CMake configuration. +- Interactive mode is disabled in CI environments. + +--- + +This command bootstraps the full Vix project structure. + diff --git a/docs/modules/cli/orm.md b/docs/modules/cli/orm.md new file mode 100644 index 0000000..09a3c09 --- /dev/null +++ b/docs/modules/cli/orm.md @@ -0,0 +1,150 @@ +# vix orm + +Database migrations and schema management. + +--- + +## Usage + +```bash +vix orm migrate [options] +vix orm rollback --steps [options] +vix orm status [options] +vix orm makemigrations --new [options] +``` + +--- + +## Description + +`vix orm` manages: + +- Database migrations +- Schema evolution +- Migration history +- Rollback operations + +Supports MySQL and SQLite dialects. + +--- + +## Commands + +### Migrate + +```bash +vix orm migrate +``` + +Apply pending migrations. + +--- + +### Rollback + +```bash +vix orm rollback --steps +``` + +Rollback last N applied migrations. + +Required: + +```bash +--steps +``` + +--- + +### Status + +```bash +vix orm status +``` + +Show applied and pending migrations. + +--- + +### Makemigrations + +```bash +vix orm makemigrations --new +``` + +Generate migration from schema diff. + +Options: + +```bash +--new New schema (required) +--snapshot Previous schema snapshot (default: schema.json) +--name