diff --git a/.github/workflows/sdk-release.yml b/.github/workflows/sdk-release.yml index 78cdd11..522f2ec 100644 --- a/.github/workflows/sdk-release.yml +++ b/.github/workflows/sdk-release.yml @@ -7,7 +7,7 @@ on: workflow_dispatch: inputs: tag: - description: "Optional tag to build/release (e.g. v2.1.10). If empty, uses the current ref." + description: "Optional tag to build/release (e.g. v2.5.3). If empty, uses the current ref." required: false default: "" @@ -20,22 +20,35 @@ concurrency: jobs: build-sdk: - name: build-sdk (${{ matrix.sdkos }} / ${{ matrix.arch }}) + name: build-sdk (${{ matrix.variant }} / linux / x86_64) + strategy: fail-fast: false matrix: include: - - os: ubuntu-24.04 + - variant: core + os: ubuntu-24.04 sdkos: linux arch: x86_64 + asset: vix-sdk-linux-x86_64.tar.gz + install_dir: install-core + build_dir: build-core - - os: macos-14 - sdkos: macos - arch: aarch64 + - variant: db + os: ubuntu-24.04 + sdkos: linux + arch: x86_64 + asset: vix-sdk-db-linux-x86_64.tar.gz + install_dir: install-db + build_dir: build-db - - os: windows-2022 - sdkos: windows + - variant: full + os: ubuntu-24.04 + sdkos: linux arch: x86_64 + asset: vix-sdk-full-linux-x86_64.tar.gz + install_dir: install-full + build_dir: build-full runs-on: ${{ matrix.os }} @@ -58,10 +71,10 @@ jobs: uses: lukka/get-cmake@latest - name: Install deps (Linux) - if: runner.os == 'Linux' shell: bash run: | set -euxo pipefail + sudo apt-get update sudo apt-get install -y --no-install-recommends \ build-essential \ @@ -72,151 +85,173 @@ jobs: curl \ unzip \ zip \ + tar \ nlohmann-json3-dev \ libssl-dev \ zlib1g-dev \ libsqlite3-dev \ libbrotli-dev \ libspdlog-dev \ - libfmt-dev - - - name: Install deps (macOS) - if: runner.os == 'macOS' - shell: bash - run: | - set -euxo pipefail - brew update - brew install cmake ninja pkg-config openssl@3 spdlog fmt nlohmann-json sqlite brotli - - - name: Setup vcpkg (Windows) - if: runner.os == 'Windows' - shell: pwsh - run: | - git clone https://github.com/microsoft/vcpkg $env:GITHUB_WORKSPACE\vcpkg - "VCPKG_ROOT=$env:GITHUB_WORKSPACE\vcpkg" | Out-File -FilePath $env:GITHUB_ENV -Append - cd $env:GITHUB_WORKSPACE\vcpkg - .\bootstrap-vcpkg.bat - - - name: Vcpkg install (manifest, Windows) - if: runner.os == 'Windows' - shell: pwsh - run: | - Write-Host "VCPKG_ROOT=$env:VCPKG_ROOT" - cd $env:GITHUB_WORKSPACE - - & "$env:VCPKG_ROOT\vcpkg.exe" install --triplet x64-windows --x-manifest-root "$env:GITHUB_WORKSPACE" - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + libfmt-dev \ + libmysqlcppconn-dev - - name: Configure (Unix) - if: runner.os != 'Windows' + - name: Configure SDK shell: bash run: | set -euxo pipefail - CMAKE_ARGS=( - -S . -B build -G Ninja + COMMON_ARGS=( + -S . + -B "${{ matrix.build_dir }}" + -G Ninja -DCMAKE_BUILD_TYPE=Release -DVIX_ENABLE_INSTALL=ON - -DVIX_DB_USE_MYSQL=OFF - -DVIX_CORE_WITH_MYSQL=OFF - -DVIX_ENABLE_HTTP_COMPRESSION=OFF - -DCMAKE_INSTALL_PREFIX="${GITHUB_WORKSPACE}/install" + -DVIX_BUILD_EXAMPLES=OFF + -DVIX_BUILD_TESTS=OFF + -DVIX_TIME_BUILD_TESTS=OFF + -DVIX_TIME_BUILD_BENCH=OFF + -DVIX_TEMPLATE_BUILD_TESTS=OFF + -DVIX_TEMPLATE_BUILD_EXAMPLES=OFF + -DVIX_TEMPLATE_BUILD_BENCH=OFF + -DCMAKE_INSTALL_PREFIX="${GITHUB_WORKSPACE}/${{ matrix.install_dir }}" ) - if [ "${{ runner.os }}" = "macOS" ]; then - if command -v brew >/dev/null 2>&1; then - BREW_SSL="$(brew --prefix openssl@3 2>/dev/null || true)" - if [ -n "$BREW_SSL" ] && [ -d "$BREW_SSL" ]; then - CMAKE_ARGS+=(-DOPENSSL_ROOT_DIR="$BREW_SSL") - CMAKE_ARGS+=(-DCMAKE_PREFIX_PATH="$(brew --prefix)") - fi - fi + if [ "${{ matrix.variant }}" = "core" ]; then + cmake "${COMMON_ARGS[@]}" \ + -DVIX_ENABLE_DB=OFF \ + -DVIX_ENABLE_ORM=OFF \ + -DVIX_ENABLE_WEBSOCKET=OFF \ + -DVIX_DB_USE_SQLITE=OFF \ + -DVIX_DB_USE_MYSQL=OFF \ + -DVIX_DB_USE_POSTGRES=OFF \ + -DVIX_DB_USE_REDIS=OFF \ + -DVIX_CORE_WITH_MYSQL=OFF \ + -DVIX_ENABLE_HTTP_COMPRESSION=OFF + fi + + if [ "${{ matrix.variant }}" = "db" ]; then + cmake "${COMMON_ARGS[@]}" \ + -DVIX_ENABLE_DB=ON \ + -DVIX_ENABLE_ORM=OFF \ + -DVIX_DB_USE_SQLITE=ON \ + -DVIX_DB_USE_MYSQL=ON \ + -DVIX_DB_USE_POSTGRES=OFF \ + -DVIX_DB_USE_REDIS=OFF \ + -DVIX_CORE_WITH_MYSQL=OFF \ + -DVIX_ENABLE_HTTP_COMPRESSION=OFF fi - cmake "${CMAKE_ARGS[@]}" + if [ "${{ matrix.variant }}" = "full" ]; then + cmake "${COMMON_ARGS[@]}" \ + -DVIX_ENABLE_DB=ON \ + -DVIX_ENABLE_ORM=ON \ + -DVIX_DB_USE_SQLITE=ON \ + -DVIX_DB_USE_MYSQL=ON \ + -DVIX_DB_USE_POSTGRES=OFF \ + -DVIX_DB_USE_REDIS=OFF \ + -DVIX_CORE_WITH_MYSQL=OFF \ + -DVIX_ENABLE_HTTP_COMPRESSION=ON \ + -DVIX_ENABLE_WEBSOCKET=ON \ + -DVIX_ENABLE_CLI=ON \ + -DVIX_ENABLE_MIDDLEWARE=ON \ + -DVIX_ENABLE_P2P=ON \ + -DVIX_ENABLE_P2P_HTTP=ON \ + -DVIX_ENABLE_CACHE=ON \ + -DVIX_ENABLE_ASYNC=ON \ + -DVIX_ENABLE_VALIDATION=ON \ + -DVIX_ENABLE_CRYPTO=ON \ + -DVIX_ENABLE_WEBRPC=ON \ + -DVIX_ENABLE_TIME=ON \ + -DVIX_ENABLE_TESTS_MODULE=ON \ + -DVIX_ENABLE_TEMPLATE=ON \ + -DVIX_ENABLE_PROCESS=ON + fi - - name: Build (Unix) - if: runner.os != 'Windows' + - name: Build SDK shell: bash run: | set -euxo pipefail - cmake --build build -j2 + cmake --build "${{ matrix.build_dir }}" -j2 - - name: Install (Unix) - if: runner.os != 'Windows' + - name: Install SDK shell: bash run: | set -euxo pipefail - cmake --install build + cmake --install "${{ matrix.build_dir }}" - - name: Configure (Windows) - if: runner.os == 'Windows' - shell: pwsh - run: | - cmake -S . -B build -A x64 ` - -DVIX_ENABLE_INSTALL=ON ` - -DVIX_DB_USE_MYSQL=OFF ` - -DVIX_CORE_WITH_MYSQL=OFF ` - -DVIX_ENABLE_HTTP_COMPRESSION=OFF ` - -DCMAKE_INSTALL_PREFIX="$env:GITHUB_WORKSPACE\install" ` - -DVCPKG_TARGET_TRIPLET=x64-windows ` - -DCMAKE_TOOLCHAIN_FILE="$env:VCPKG_ROOT\scripts\buildsystems\vcpkg.cmake" - - - name: Build (Windows) - if: runner.os == 'Windows' - shell: pwsh + - name: Package SDK + shell: bash run: | - cmake --build build --config Release - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + set -euxo pipefail - - name: Install (Windows) - if: runner.os == 'Windows' - shell: pwsh - run: | - cmake --install build --config Release - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + mkdir -p dist + + test -d "${{ matrix.install_dir }}" + tar -C "${{ matrix.install_dir }}" -czf "dist/${{ matrix.asset }}" . - - name: Package SDK (Unix) - if: runner.os != 'Windows' + ls -lah dist + + - name: Validate packaged SDK binary shell: bash run: | set -euxo pipefail - mkdir -p dist - ASSET="vix-sdk-${{ matrix.sdkos }}-${{ matrix.arch }}.tar.gz" - tar -C install -czf "dist/$ASSET" . - - name: Validate packaged SDK (Linux) - if: runner.os == 'Linux' + rm -rf smoke-bin + mkdir -p smoke-bin + + tar -xzf "dist/${{ matrix.asset }}" -C smoke-bin + + test -f smoke-bin/bin/vix + file smoke-bin/bin/vix + ldd smoke-bin/bin/vix || true + smoke-bin/bin/vix --version + + - name: Validate packaged SDK with consumer project shell: bash run: | set -euxo pipefail - mkdir -p smoke - tar -xzf "dist/vix-sdk-${{ matrix.sdkos }}-${{ matrix.arch }}.tar.gz" -C smoke + rm -rf smoke-sdk consumer + mkdir -p smoke-sdk consumer - test -f smoke/bin/vix - file smoke/bin/vix - ldd smoke/bin/vix || true - smoke/bin/vix --version + tar -xzf "dist/${{ matrix.asset }}" -C smoke-sdk - - name: Package SDK (Windows) - if: runner.os == 'Windows' - shell: pwsh - run: | - New-Item -ItemType Directory -Force -Path dist | Out-Null - $asset = "vix-sdk-windows-${{ matrix.arch }}.zip" + cat > consumer/CMakeLists.txt <<'EOF' + cmake_minimum_required(VERSION 3.20) + project(vix_sdk_consumer LANGUAGES CXX) + + set(CMAKE_CXX_STANDARD 20) + set(CMAKE_CXX_STANDARD_REQUIRED ON) + + find_package(Vix CONFIG REQUIRED) + + add_executable(app main.cpp) + target_link_libraries(app PRIVATE vix::vix) + EOF + + cat > consumer/main.cpp <<'EOF' + #include + + #include - if (!(Test-Path "install")) { - Write-Host "install directory not found" - exit 1 + int main() + { + std::cout << "Vix SDK consumer build works\n"; + return 0; } + EOF - Compress-Archive -Path "install\*" -DestinationPath "dist\$asset" -Force + cmake -S consumer -B consumer/build \ + -DCMAKE_PREFIX_PATH="${GITHUB_WORKSPACE}/smoke-sdk" + + cmake --build consumer/build -j2 + + ./consumer/build/app - name: Upload dist uses: actions/upload-artifact@v4 with: - name: sdk-dist-${{ matrix.sdkos }}-${{ matrix.arch }} + name: sdk-dist-${{ matrix.variant }}-${{ matrix.sdkos }}-${{ matrix.arch }} path: dist/* if-no-files-found: error @@ -235,26 +270,37 @@ jobs: shell: bash run: | set -euxo pipefail + mkdir -p dist - find dist-all -type f -name "vix-sdk-*" -exec cp -f {} dist/ \; - ls -la dist + + find dist-all -type f -name "vix-sdk-*.tar.gz" -exec cp -f {} dist/ \; + + test -f dist/vix-sdk-linux-x86_64.tar.gz + test -f dist/vix-sdk-db-linux-x86_64.tar.gz + test -f dist/vix-sdk-full-linux-x86_64.tar.gz + + ls -lah dist - name: Generate sha256 files shell: bash run: | set -euxo pipefail + cd dist - for f in vix-sdk-*; do + + for f in vix-sdk-*.tar.gz; do [ -f "$f" ] || continue sha256sum "$f" > "$f.sha256" done - ls -la + + ls -lah - name: Determine tag id: tag shell: bash run: | set -euxo pipefail + if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ -n "${{ github.event.inputs.tag }}" ]; then echo "tag=${{ github.event.inputs.tag }}" >> "$GITHUB_OUTPUT" else diff --git a/.gitmodules b/.gitmodules index ce0b8c6..7f1550d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -131,3 +131,11 @@ path = modules/process url = https://github.com/vixcpp/process.git branch = dev +[submodule "modules/threadpool"] + path = modules/threadpool + url = https://github.com/vixcpp/threadpool.git + branch = dev +[submodule "modules/kv"] + path = modules/kv + url = https://github.com/vixcpp/kv.git + branch = dev diff --git a/CHANGELOG.md b/CHANGELOG.md index a4209c0..0869cfc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,9 +5,154 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ---- +## v2.5.3 + +### Added +- Added `vix replay` to record, inspect, list, clean, and replay previous Vix executions. +- Added replay support for project runs and single-file script runs. +- Added local replay storage under `.vix/runs/` with `run.json`, `stdout.log`, `stderr.log`, and `combined.log`. +- Added `vix replay last`, `vix replay failed`, `vix replay show`, `vix replay list`, and `vix replay clean`. +- Added `--replay` for `vix run` to explicitly record executions under `.vix/runs/`. +- Added opt-in OpenAPI/docs mode for `vix run` through `--docs`. +- Added `--tsan` support for ThreadSanitizer-based script runs. +- Added a new incremental build graph foundation for `vix build`, including build nodes, build tasks, dependency file parsing, object cache metadata, and a parallel build scheduler. +- Added local build graph state storage under the build directory to prepare faster no-op and incremental build decisions. +- Added shared runtime error location helpers for extracting `file:line:column` locations and printing runtime code frames. +- Added source-based fallback location hints for runtime errors when logs do not provide an exact user frame. +- Added `threadpool` as a dedicated Vix module. +- Added `kv` as a dedicated Vix module for durable local-first key-value storage. +- Added compile database import for `vix build` so the build graph can use real CMake/Ninja compile commands from `compile_commands.json`. +- Added Ninja build edge import for `vix build` so the build graph can understand archive, link, copy, install, and utility edges from `build.ninja`. +- Added a guarded target-aware graph executor for experimental graph-based target builds. +- Added a reusable `BuildStyle` renderer for consistent build output, progress, and diagnostics. +- Added Vix-native test output styling for `vix tests`, aligned with the `vix build` progress style. +- Added `vix tests --raw` to show raw internal test runner output when needed. +- Added `vix tests --test ` and `vix tests -R ` to run one test or a filtered group of tests. +- Added Vix-native output styling for `vix check`, aligned with `vix build` and `vix tests`. +- Added clean script-mode check output for `vix check file.cpp`. +- Added structured progress lines for `vix check` configure, build, tests, and runtime steps. +- Added a dedicated `vix dev` session engine with target-aware rebuild orchestration. +- Added a `DevFileIndex` for faster dev-mode file watching using filtered file indexing, `mtime`, and file size comparisons. + +### Changed +- Changed `vix run` replay recording to be opt-in through `--replay`, so normal runs no longer create `.vix/runs/`. +- Changed `vix run` OpenAPI/docs behavior to be opt-in through `--docs`, so docs are disabled by default. +- Improved `vix replay` to use the same process execution behavior as `vix run`. +- Improved `Ctrl+C` handling consistency between `vix run` and `vix replay`. +- Improved `vix new` internals and output rendering with a dedicated `NewOutput` renderer. +- Improved CLI help output to stay aligned with registered commands. +- Improved direct script cache fingerprinting and single-file run output handling. +- Improved runtime diagnostics for sanitizer and non-sanitizer crashes. +- Improved `vix run` live output filtering to avoid printing raw allocator crash noise before friendly diagnostics. +- Improved compile-time error rules with shorter messages, one focused hint, code frames, and consistent `at:` output. +- Improved template error rules with cleaner diagnostics for concepts, coroutine awaiters, invalid overrides, object slicing, downcasts, and template substitution failures. +- Improved CMake/build error diagnostics with shorter messages and focused hints. +- Improved `vix build` internals with a build state cache that can skip unnecessary work when the project inputs have not changed. +- Improved `vix build` preparation for deeper parallel and incremental compilation by wiring the new build graph into the build command flow. +- Improved `vix build` to build the main project target by default instead of the full `all` target, making no-op and focused rebuilds much faster. +- Improved `vix build -v` output by hiding internal graph, cache, project path, and CMake variable details unless debug logging is enabled. +- Improved `vix build` diagnostics with a cleaner unified build error style, including location, error, code frame, and focused hints. +- Improved `vix build --build-target all` to preserve the full build behavior explicitly when examples, tests, and auxiliary targets need to be rebuilt. +- Improved `vix tests` output with a cleaner Vix-native header, progress line, success summary, and failure diagnostics. +- Improved `vix tests` failure reporting by extracting failed test names and useful error messages instead of dumping raw runner output by default. +- Improved `vix tests -v` to show detailed Vix-formatted failure details while keeping raw runner output behind `--raw`. +- Improved `vix tests` to run supported backends in parallel by default and report the active job count. +- Improved `vix check` output with a cleaner Vix-native header, progress lines, timing summary, and reduced default noise. +- Improved `vix check --tests` to reuse the `vix tests` experience instead of exposing raw CTest output directly. +- Improved `vix check` project mode so sanitizer checks validate the sanitizer build by default without forcing runtime execution. +- Improved `vix check --san --run` behavior so runtime validation remains explicit for project checks. +- Improved `vix check file.cpp` script mode with cleaner build, configure, runtime, and sanitizer progress output. +- Improved `vix check --verbose` to keep detailed project, script, build directory, and cache information available without showing it by default. +- Improved `vix check` build execution to hide internal CMake and Ninja output during normal successful checks. +- Improved `vix dev` output to align with the cleaner `vix build` style. +- Improved `vix dev` rebuild flow to use real Ninja progress instead of a fake spinner animation. +- Improved `vix dev` reload behavior by clearing the terminal before restarting the application. +- Improved `vix dev` file watching to ignore build folders, `.git`, `.vix`, docs, generated output, and unrelated files. +- Improved `vix dev` startup output to reduce noise while keeping detailed information available through verbose/debug mode. +- Improved raw log detection for linker errors, sanitizer reports, CMake failures, uncaught exceptions, and common runtime crashes. +- Aligned core HTTP JSON handling around the stable `vix::json::Json` API. +- Updated request parsing, response serialization, request handlers, and configuration storage to use the Vix JSON API consistently. +- Moved the thread pool implementation out of `core` into the dedicated `threadpool` module. + +### Fixed +- Fixed opt-in replay recording for `vix run main.cpp --replay`. +- Fixed opt-in replay recording for project mode `vix run --replay`. +- Fixed opt-in replay recording for interrupted runs. +- Fixed opt-in replay recording for CMake fallback script runs. +- Fixed `vix run` so normal runs no longer create replay data unless `--replay` is passed. +- Fixed `vix run` so OpenAPI/docs are disabled by default unless `--docs` is passed. +- Fixed `vix replay` missing-run errors to show a clearer message. +- Fixed `vix replay --help` and `vix help replay` routing. +- Fixed script `SIGINT` handling so user interruptions are treated as normal shutdowns. +- Fixed script sanitizer runtime diagnostics so `vix run file.cpp --san` and CMake fallback scripts show precise code frames for crashes such as double free. +- Fixed ThreadSanitizer diagnostics for script runs with `--tsan`. +- Fixed double-free diagnostics without `--san` by pointing to suspicious `delete`, `delete[]`, or `free()` source lines when possible. +- Fixed iterator invalidation diagnostics by reporting ASan use-after-free from invalidated iterators as iterator invalidation when detected. +- Fixed STL debug iterator diagnostics to show a code frame when the log only reports a singular iterator without a source location. +- Fixed segmentation fault, abort, out-of-range, mutex, condition variable, thread, span, and `std::string_view` runtime rules to use consistent code frame output. +- Fixed uncaught exception diagnostics to show one clear hint and a guessed throw location when possible. +- Fixed raw runtime logs being printed before friendly Vix diagnostics. +- Fixed build metadata storage location in the Vix home cache. +- Fixed compiler warnings and removed dead CLI code. +- Fixed `vix dev` reload detection after file saves by keeping a stable file index instead of comparing fresh snapshots only. +- Fixed `vix tests` output so internal runner details no longer leak into the default interface. +- Fixed `vix tests` failure progress styling so failed test progress is shown in red. +- Fixed `vix tests` summaries to report counts such as `Passed 23 tests` or `Failed 1 of 23 tests`. +- Fixed `vix check` default output so successful CMake and Ninja logs no longer leak into normal output. +- Fixed `vix check --tests` so test execution uses the Vix test runner output style. +- Fixed `vix check --san` for library projects so sanitizer checks no longer assume a runnable executable exists. +- Fixed `vix check file.cpp` script builds so they no longer force an invalid target name such as `main`. +- Fixed `vix check file.cpp --san` output so runtime validation is shown as a clean progress step. +- Fixed duplicate `Compiling (dev)` output during dev reloads. +- Fixed dev-mode progress output so rebuilds no longer show misleading animated progress. +- Fixed dev-mode restart rendering to avoid stale application output mixing with rebuild output. + +### Internal +- Added the internal replay subsystem: + - `ReplayTypes` + - `ReplayRecord` + - `ReplayPaths` + - `ReplayId` + - `ReplayClock` + - `ReplayJson` + - `ReplayStore` + - `ReplayPrinter` + - `ReplayRecorder` + - `ReplayCapture` + - `ReplayProcess` + - `ReplayRunner` + - `ReplayList` + - `ReplayCommand` +- Refactored `NewCommand` and `ModulesCommand` internals into dedicated command modules. +- Updated `RunProcess`, `RunCommand`, `RunScript`, and `DirectScriptRunner` to support replay capture. +- Refactored runtime error rules to share `RuntimeLocation`, `find_best_runtime_location(...)`, `find_best_runtime_location_or_source_hint(...)`, and `make_at_text(...)`. +- Refactored raw log detection and CMake build detection for cleaner dispatch order and more consistent output. +- Normalized compile-time, runtime, template, CMake, linker, and sanitizer diagnostics around the same output style. +- Expanded the internal incremental build foundation: + - `BuildNode` + - `BuildTask` + - `DependencyFile` + - `CompileCommands` + - `BuildNinja` + - `ObjectCache` + - `BuildGraph` + - `BuildGraphExecutor` + - `BuildScheduler` + - `BuildStyle` +- Wired `BuildCommand` to initialize, import, and persist build graph state during `vix build`. +- Wired `BuildCommand` to import `compile_commands.json` and `build.ninja` as the source of truth for future target-aware graph execution. +- Guarded the experimental graph executor behind `VIX_GRAPH_EXECUTOR` while keeping CMake/Ninja as the default stable execution path. +- Moved detailed build graph, artifact cache, build state, and CMake variable output behind debug logging. +- Registered `modules/threadpool` as a standalone submodule. +- Registered `modules/kv` as a standalone submodule. +- Integrated `kv` into the umbrella CMake build and Vix package export flow. +- Removed the old core-owned thread pool sources and experimental executor wiring. +- Refactored dev-mode rebuild handling into `DevSession`, `DevRebuilder`, and `DevFileIndex`. +- Reworked dev-mode change classification so source/header changes trigger rebuilds while CMake and Vix config changes trigger reconfigure plus rebuild. + +### Compatibility +- No breaking changes. -## [Unreleased] ## v2.5.2 ### Added @@ -142,6 +287,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Compatibility - No breaking changes. +- Preserved compatibility for existing `nlohmann::json` and `Simple.hpp` response helpers while routing core JSON handling through `vix::json::Json`. # v2.5.1 diff --git a/CMakeLists.txt b/CMakeLists.txt index da668af..4f00cc4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -214,6 +214,8 @@ option(VIX_TEMPLATE_BUILD_TESTS "Build vix::template tests" option(VIX_TEMPLATE_BUILD_EXAMPLES "Build vix::template examples" OFF) option(VIX_TEMPLATE_BUILD_BENCH "Build vix::template benchmarks" OFF) option(VIX_ENABLE_PROCESS "Build Vix Process module" ON) +option(VIX_ENABLE_THREADPOOL "Build Vix ThreadPool module" ON) +option(VIX_ENABLE_KV "Build Vix KV module" ON) # ---------------------------------------------------- # Tooling / Static analysis @@ -351,13 +353,13 @@ else() include(FetchContent) - FetchContent_Declare( + fetchcontent_declare( asio GIT_REPOSITORY https://github.com/chriskohlhoff/asio.git GIT_TAG asio-1-30-2 ) - FetchContent_MakeAvailable(asio) + fetchcontent_makeavailable(asio) target_include_directories(vix_thirdparty_asio SYSTEM INTERFACE $ @@ -488,12 +490,12 @@ if (EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/modules/json/CMakeLists.txt") set(_VIX_JSON_BACKEND "submodule") elseif (VIX_FORCE_FETCH_JSON) message(WARNING "modules/json missing — fetching nlohmann/json fallback") - FetchContent_Declare( + fetchcontent_declare( nlohmann_json GIT_REPOSITORY https://github.com/nlohmann/json.git GIT_TAG v3.11.3 ) - FetchContent_MakeAvailable(nlohmann_json) + fetchcontent_makeavailable(nlohmann_json) add_library(vix_json INTERFACE) target_link_libraries(vix_json INTERFACE nlohmann_json::nlohmann_json) @@ -690,6 +692,56 @@ else() message(STATUS "Process: disabled or not present.") endif() +# --- ThreadPool (optional) --- +set(VIX_HAS_THREADPOOL OFF) + +if (VIX_ENABLE_THREADPOOL AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/modules/threadpool/CMakeLists.txt") + message(STATUS "Adding 'modules/threadpool'...") + + set(VIX_THREADPOOL_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) + set(VIX_THREADPOOL_BUILD_TESTS OFF CACHE BOOL "" FORCE) + set(VIX_THREADPOOL_BUILD_BENCHMARKS OFF CACHE BOOL "" FORCE) + + add_subdirectory(modules/threadpool threadpool_build) + + if (TARGET vix::threadpool OR TARGET vix_threadpool) + set(VIX_HAS_THREADPOOL ON) + + if (TARGET vix_threadpool AND NOT TARGET vix::threadpool) + add_library(vix::threadpool ALIAS vix_threadpool) + endif() + else() + message(WARNING "ThreadPool module added but no vix::threadpool target was exported.") + endif() +else() + message(STATUS "ThreadPool: disabled or not present.") +endif() + +# --- KV (optional) --- +set(VIX_HAS_KV OFF) + +if (VIX_ENABLE_KV AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/modules/kv/CMakeLists.txt") + message(STATUS "Adding 'modules/kv'...") + + set(VIX_KV_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) + set(VIX_KV_BUILD_TESTS OFF CACHE BOOL "" FORCE) + set(VIX_KV_BUILD_BENCHMARKS OFF CACHE BOOL "" FORCE) + + add_subdirectory(modules/kv kv_build) + + if (TARGET vix::kv OR TARGET vix_kv) + set(VIX_HAS_KV ON) + + if (TARGET vix_kv AND NOT TARGET vix::kv) + add_library(vix::kv ALIAS vix_kv) + endif() + else() + message(WARNING "KV module added but no vix::kv target was exported.") + endif() +else() + message(STATUS "KV: disabled or not present.") +endif() + # --- Core --- if (EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/modules/core/CMakeLists.txt") message(STATUS "Adding 'modules/core'...") @@ -1035,6 +1087,14 @@ if (TARGET vix::process) target_link_libraries(vix INTERFACE vix::process) endif() +if (TARGET vix::threadpool) + target_link_libraries(vix INTERFACE vix::threadpool) +endif() + +if (TARGET vix::kv) + target_link_libraries(vix INTERFACE vix::kv) +endif() + if (TARGET vix::p2p) target_link_libraries(vix INTERFACE vix::p2p) endif() @@ -1190,13 +1250,13 @@ if (VIX_BUILD_TESTS) find_package(GTest QUIET) if (NOT GTest_FOUND) message(STATUS "GTest not found, fetching googletest...") - FetchContent_Declare( + fetchcontent_declare( googletest GIT_REPOSITORY https://github.com/google/googletest.git GIT_TAG v1.14.0 ) set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) - FetchContent_MakeAvailable(googletest) + fetchcontent_makeavailable(googletest) endif() file(GLOB_RECURSE VIX_TEST_SOURCES CONFIGURE_DEPENDS @@ -1250,8 +1310,8 @@ find_package(OpenSSL QUIET) if (OpenSSL_FOUND AND TARGET OpenSSL::Crypto) set(VIX_WITH_OPENSSL ON) endif() -set(VIX_WITH_MYSQL OFF) # do NOT require MySQL for consumers by default -set(VIX_WITH_SQLITE OFF) # will be set ON only if websocket exists +set(VIX_WITH_MYSQL OFF) +set(VIX_WITH_SQLITE OFF) set(VIX_WITH_BOOST_FS OFF) set(VIX_WITH_ZLIB OFF) @@ -1279,13 +1339,13 @@ if (VIX_HAS_P2P_HTTP) set(VIX_WITH_P2P_HTTP ON) endif() -if (TARGET vix::websocket OR TARGET vix_websocket) +if ((VIX_HAS_DB AND VIX_DB_USE_SQLITE) OR VIX_HAS_WEBSOCKET) set(VIX_WITH_SQLITE ON) endif() if (VIX_ENABLE_INSTALL) - foreach(_m error path fs io env os log tests core utils json websocket webrpc crypto time conversion validation async process net cache sync p2p p2p_http middleware db orm template) + foreach(_m error path fs io env os log tests core utils json websocket webrpc crypto time conversion validation async process threadpool kv net cache sync p2p p2p_http middleware db orm template) if (EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/modules/${_m}/include") install(DIRECTORY "modules/${_m}/include/" DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}" @@ -1369,6 +1429,19 @@ if (VIX_ENABLE_INSTALL) set(VIX_HAS_TIME ${VIX_HAS_TIME}) set(VIX_HAS_TEMPLATE ${VIX_HAS_TEMPLATE}) + set(VIX_WITH_THREADPOOL OFF) + if (VIX_HAS_THREADPOOL) + set(VIX_WITH_THREADPOOL ON) + endif() + + set(VIX_WITH_KV OFF) + if (VIX_HAS_KV) + set(VIX_WITH_KV ON) + endif() + +set(VIX_HAS_KV ${VIX_HAS_KV}) +set(VIX_WITH_KV ${VIX_WITH_KV}) + set(VIX_WITH_CONVERSION OFF) if (VIX_HAS_CONVERSION) set(VIX_WITH_CONVERSION ON) @@ -1447,6 +1520,8 @@ message(STATUS "Time module : ${VIX_HAS_TIME}") message(STATUS "WebRPC built : ${VIX_HAS_WEBRPC}") message(STATUS "WebSocket built : ${VIX_HAS_WEBSOCKET}") message(STATUS "Process built : ${VIX_HAS_PROCESS}") +message(STATUS "ThreadPool built : ${VIX_HAS_THREADPOOL}") +message(STATUS "KV built : ${VIX_HAS_KV}") message(STATUS "ORM packaged : ${VIX_HAS_ORM}") message(STATUS "DB built : ${VIX_HAS_DB}") message(STATUS "P2P built : ${VIX_HAS_P2P}") @@ -1460,5 +1535,5 @@ message(STATUS "Sanitizers : ${VIX_ENABLE_SANITIZERS}") message(STATUS "Coverage : ${VIX_ENABLE_COVERAGE}") message(STATUS "LTO : ${VIX_ENABLE_LTO}") message(STATUS "Install/Export : ${VIX_ENABLE_INSTALL}") -message(STATUS "Flags exported : JSON=${VIX_WITH_JSON} CONVERSION=${VIX_WITH_CONVERSION} VALIDATION=${VIX_WITH_VALIDATION} WEBRPC=${VIX_WITH_WEBRPC} CRYPTO=${VIX_WITH_CRYPTO} BOOST_FS=${VIX_WITH_BOOST_FS} OPENSSL=${VIX_WITH_OPENSSL} SQLITE=${VIX_WITH_SQLITE} MYSQL=${VIX_WITH_MYSQL} HAS_ORM=${VIX_HAS_ORM} CACHE=${VIX_WITH_CACHE} P2P=${VIX_WITH_P2P} TEMPLATE=${VIX_WITH_TEMPLATE}") +message(STATUS "Flags exported : JSON=${VIX_WITH_JSON} CONVERSION=${VIX_WITH_CONVERSION} VALIDATION=${VIX_WITH_VALIDATION} WEBRPC=${VIX_WITH_WEBRPC} CRYPTO=${VIX_WITH_CRYPTO} BOOST_FS=${VIX_WITH_BOOST_FS} OPENSSL=${VIX_WITH_OPENSSL} SQLITE=${VIX_WITH_SQLITE} MYSQL=${VIX_WITH_MYSQL} HAS_ORM=${VIX_HAS_ORM} CACHE=${VIX_WITH_CACHE} P2P=${VIX_WITH_P2P} TEMPLATE=${VIX_WITH_TEMPLATE} THREADPOOL=${VIX_WITH_THREADPOOL} KV=${VIX_WITH_KV}") message(STATUS "------------------------------------------------------") diff --git a/README.md b/README.md index c5bd24c..a2b6ab0 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,8 @@ Vix.cpp is a modern C++ runtime for building and running real-world applications Learn more about the Vix runtime in the [documentation](https://docs.vixcpp.com). +Vix is also the runtime foundation used by [Softadastra](https://docs.softadastra.com), a local-first and offline-first system for building reliable applications that continue working when the network is slow, unstable, or unavailable. + ## Production Proof Vix is not a concept. It runs real systems. @@ -96,6 +98,27 @@ vix run main.cpp Done. +## Replay a previous run + +Vix can record a run and replay it later. + +```bash +vix run main.cpp +vix replay +``` + +Replay the latest failed or interrupted run: +```bash +vix replay failed +``` + +Inspect a recorded run before replaying it: +```bash +vix replay show last +``` + +Vix stores replay data locally under .vix/runs/, including the command, working directory, exit status, and captured logs. + ## Build a server ```cpp @@ -120,10 +143,10 @@ vix run server.cpp → http://localhost:8080 -For production deployments, Vix is optimized to run behind a TLS-terminating reverse proxy such as Nginx, Caddy or Traefik, while built-in HTTPS is available for local development, internal tools and simple self-hosted deployments. - ## Install a framework in 1 command +Framework: https://cnerium.dev + ```bash vix install -g cnerium/app ``` diff --git a/cmake/VixConfig.cmake.in b/cmake/VixConfig.cmake.in index 8983c58..15e3e3a 100644 --- a/cmake/VixConfig.cmake.in +++ b/cmake/VixConfig.cmake.in @@ -215,6 +215,26 @@ if (VIX_HAS_TIME AND NOT TARGET vix::time) set(VIX_HAS_TIME OFF) endif() +set(VIX_HAS_THREADPOOL @VIX_HAS_THREADPOOL@) +if (VIX_HAS_THREADPOOL AND NOT TARGET vix::threadpool) + set(VIX_HAS_THREADPOOL OFF) +endif() + +set(VIX_WITH_THREADPOOL @VIX_WITH_THREADPOOL@) +if (VIX_WITH_THREADPOOL AND NOT TARGET vix::threadpool) + set(VIX_WITH_THREADPOOL OFF) +endif() + +set(VIX_HAS_KV @VIX_HAS_KV@) +if (VIX_HAS_KV AND NOT TARGET vix::kv) + set(VIX_HAS_KV OFF) +endif() + +set(VIX_WITH_KV @VIX_WITH_KV@) +if (VIX_WITH_KV AND NOT TARGET vix::kv) + set(VIX_WITH_KV OFF) +endif() + set(VIX_WITH_VALIDATION @VIX_WITH_VALIDATION@) if (VIX_WITH_VALIDATION AND NOT TARGET vix::validation) set(VIX_WITH_VALIDATION OFF) diff --git a/docs/api.md b/docs/api.md deleted file mode 100644 index 7796249..0000000 --- a/docs/api.md +++ /dev/null @@ -1,53 +0,0 @@ -# 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 index e69de29..d27d926 100644 --- a/docs/api/async.md +++ b/docs/api/async.md @@ -0,0 +1,325 @@ +# Async API + +The Async API is the low-level coroutine runtime of Vix. It provides C++20 `co_await`-based building blocks for asynchronous code. + +```txt +event loop — tasks — timers — signals — CPU pool — async TCP/UDP/DNS — when_all/when_any +``` + +## Public header + +```cpp +#include + +// Or specific headers +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +``` + +## Mental model + +```txt +io_context → scheduler → task → co_await +``` + +`io_context` owns: scheduler, timer service, CPU thread pool, signal service, network backend. + +A task starts **suspended**. Post it to the scheduler, then `ctx.run()` drives the event loop. + +## Main concepts + +| Concept | Purpose | +|---------|---------| +| `io_context` | Central async runtime context | +| `task` | Coroutine task returning T | +| `task` | Coroutine task with no return value | +| `scheduler` | Runs posted coroutine handles | +| `timer` | Non-blocking sleep and delayed callbacks | +| `thread_pool` | Offload CPU or blocking work | +| `signal_set` | Wait for POSIX signals | +| `spawn_detached` | Run a task without awaiting it | +| `when_all` | Wait for all tasks to complete | +| `when_any` | Wait for the first task to complete | +| `tcp_stream` | Async TCP connection | +| `tcp_listener` | Async TCP server listener | +| `udp_socket` | Async UDP socket | +| `dns_resolver` | Async DNS resolution | +| `cancel_token` | Observe cancellation | +| `cancel_source` | Request cancellation | + +## Minimal async task + +```cpp +#include +#include +#include +using vix::async::core::io_context; +using vix::async::core::task; + +static task app(io_context &ctx) +{ + std::cout << "[async] hello from task\n"; + ctx.stop(); + co_return; +} + +int main() +{ + io_context ctx; + auto t = app(ctx); + ctx.post(t.handle()); // task starts suspended — must post it + ctx.run(); // drive the event loop + std::cout << "[async] done\n"; + return 0; +} +``` + +## Timer service + +```cpp +// Non-blocking sleep — do not use std::this_thread::sleep_for in async code +co_await ctx.timers().sleep_for(std::chrono::milliseconds(100)); +co_await ctx.timers().sleep_for(std::chrono::milliseconds(200)); + +// Callback style +ctx.timers().after(std::chrono::milliseconds(150), []() { + /* runs later */ +}); + +// With cancellation token +co_await ctx.timers().sleep_for(std::chrono::seconds(5), token); +``` + +## CPU thread pool + +```cpp +// Never block the event loop with heavy work +int result = co_await ctx.cpu_pool().submit([](){ + return heavy_work(42); + }); + +// Fire-and-forget +ctx.cpu_pool().submit([]() { background_task(); }); +``` + +## Signals + +```cpp +#include + +auto &sig = ctx.signals(); +sig.add(SIGINT); +sig.add(SIGTERM); + +// Callback style +sig.on_signal([&](int s) { + std::cout << "signal " << s << "\n"; ctx.stop(); +}); + +// Coroutine style +int s = co_await sig.async_wait(); +ctx.stop(); +``` + +## Scheduler (minimal runtime) + +```cpp +#include +using vix::async::core::scheduler; + +static task app(scheduler &sched) +{ + co_await sched.schedule(); + std::cout << "running on scheduler\n"; + sched.stop(); + co_return; +} + +int main() +{ + scheduler sched; + + std::thread worker([&]() { + sched.run(); + }); + + std::move(app(sched)).start(sched); + + worker.join(); + return 0; +} +``` + +Use `scheduler` for minimal coroutine scheduling without timers/signals/networking. Use `io_context` when you need those services. + +## when_all + +```cpp +#include +using vix::async::core::when_all; + +auto result = co_await when_all(sched, a(), b()); +std::cout << std::get<0>(result) << ", " << std::get<1>(result) << "\n"; +``` + +## when_any + +```cpp +using vix::async::core::when_any; + +auto [idx, values] = co_await when_any(sched, a(), b()); +if (idx == 0) + std::cout << *std::get<0>(values); +else + std::cout << *std::get<1>(values); +``` + +## Detached tasks + +```cpp +#include + +// Useful for independent background work (e.g. one task per client) +vix::async::core::spawn_detached(ctx, handle_client(std::move(client))); +``` + +Do not use detached tasks when you must observe the result. + +## TCP echo server + +```cpp +#include + +static task handle_client(std::unique_ptr client) +{ + std::vector buffer(4096); + while (client && client->is_open()) + { + std::size_t n = co_await client->async_read(std::span(buffer.data(), buffer.size())); + if (n == 0) break; + co_await client->async_write(std::span(buffer.data(), n)); + } + client->close(); +} + +static task server(io_context &ctx) +{ + auto listener = vix::async::net::make_tcp_listener(ctx); + co_await listener->async_listen({"0.0.0.0", 9090}, 128); + std::cout << "listening on 9090\n"; + + while (ctx.is_running()) + { + auto client = co_await listener->async_accept(); + vix::async::core::spawn_detached(ctx, handle_client(std::move(client))); + } + + listener->close(); + ctx.stop(); +} +``` + +## UDP overview + +```cpp +#include +co_await socket->async_bind(endpoint); +co_await socket->async_send_to(data, peer); +co_await socket->async_recv_from(buffer); +``` + +## DNS overview + +```cpp +#include +auto resolver = vix::async::net::make_dns_resolver(ctx); +auto addresses = co_await resolver->async_resolve("example.com", 443); +// addresses[i].ip, addresses[i].port +``` + +## Cancellation + +```cpp +vix::async::core::cancel_source source; +vix::async::core::cancel_token token = source.token(); + +source.request_cancel(); +if (token.is_cancelled()) { + std::cout << "cancelled\n"; +} +``` + +## Common mistakes + +### Forgetting to post the task + +```cpp +auto t = app(ctx); +// ctx.post(t.handle()); // missing — task never runs +ctx.run(); +``` + +### Blocking the event loop + +```cpp +// Wrong — blocks the scheduler thread +std::this_thread::sleep_for(std::chrono::seconds(1)); + +// Correct — yields to event loop +co_await ctx.timers().sleep_for(std::chrono::seconds(1)); +``` + +### Heavy CPU work on event loop + +```cpp +// Wrong +auto result = heavy_work(); + +// Correct +auto result = co_await ctx.cpu_pool().submit([]() { + return heavy_work(); +}); +``` + +### Forgetting ctx.stop() + +If nothing calls `ctx.stop()`, `ctx.run()` keeps waiting. Short demos should stop when done. Servers intentionally keep running until a signal. + +## Production notes + +- Keep coroutine tasks small +- Use timers instead of blocking sleep +- Offload CPU-heavy work to the CPU pool +- Handle `std::system_error` in networking code +- Support cancellation where needed +- Stop the runtime cleanly on SIGINT/SIGTERM + +## What you should remember + +```cpp +io_context ctx; +auto t = app(ctx); +ctx.post(t.handle()); +ctx.run(); + +// Inside coroutine: +co_await ctx.timers().sleep_for(std::chrono::milliseconds(100)); + +int result = co_await ctx.cpu_pool().submit([]() { + return heavy_work(); +}); + +int sig = co_await ctx.signals().async_wait(); +``` + +The core idea: Vix async is coroutine-first. A task starts suspended, the scheduler resumes it, and `io_context` coordinates runtime services. + +Next: [P2P API](/api/p2p) diff --git a/docs/api/config.md b/docs/api/config.md index f9a974b..11f5be3 100644 --- a/docs/api/config.md +++ b/docs/api/config.md @@ -1,158 +1,257 @@ # Config API -This page documents the configuration loader in Vix. +The Config API loads application settings from environment files and exposes typed accessors. -Header: - -``` cpp -#include +```txt +.env → config::Config → App / Database / WebSocket / runtime ``` -Core type: +## Public header -``` cpp -vix::config::Config +```cpp +#include // for normal applications +#include // for config-only code ``` -The configuration system is: - -- JSON-based -- Strongly typed -- Path-addressable via dot notation -- Safe with fallback defaults +Main type: `vix::config::Config`. ------------------------------------------------------------------------- +## Basic example -# Constructor +```cpp +vix::config::Config cfg{".env"}; // or Config cfg; (defaults to .env) -``` cpp -vix::config::Config cfg{"config.json"}; +app.run(cfg); // start HTTP server using config ``` -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. +## Environment files ------------------------------------------------------------------------- +```cpp +vix::config::Config cfg{".env.production"}; +``` -# Getters +Typical files: `.env`, `.env.local`, `.env.production`. + +## Complete .env reference + +```dotenv +# Server +SERVER_PORT=8080 +SERVER_REQUEST_TIMEOUT=2000 +SERVER_IO_THREADS=0 +SERVER_SESSION_TIMEOUT_SEC=20 +SERVER_BENCH_MODE=false + +# TLS +SERVER_TLS_ENABLED=false +SERVER_TLS_CERT_FILE= +SERVER_TLS_KEY_FILE= + +# Database +DATABASE_ENGINE=sqlite +DATABASE_SQLITE_PATH=vix.db +DATABASE_DEFAULT_HOST=127.0.0.1 +DATABASE_DEFAULT_PORT=3306 +DATABASE_DEFAULT_USER=root +DATABASE_DEFAULT_PASSWORD= +DATABASE_DEFAULT_NAME=vix + +# Logging +LOGGING_ASYNC=true +LOGGING_QUEUE_MAX=20000 +LOGGING_DROP_ON_OVERFLOW=true + +# WAF +WAF_MODE=basic +WAF_MAX_TARGET_LEN=4096 +WAF_MAX_BODY_BYTES=1048576 + +# WebSocket +WEBSOCKET_MAX_MESSAGE_SIZE=65536 +WEBSOCKET_IDLE_TIMEOUT=60 +WEBSOCKET_ENABLE_DEFLATE=true +WEBSOCKET_PING_INTERVAL=30 +WEBSOCKET_AUTO_PING_PONG=true +``` -All getters accept: +## Typed accessors -- dot path string -- fallback value +### Server ------------------------------------------------------------------------- +```cpp +cfg.getServerPort() // SERVER_PORT +cfg.getRequestTimeout() // SERVER_REQUEST_TIMEOUT +cfg.getIOThreads() // SERVER_IO_THREADS +cfg.getSessionTimeoutSec() // SERVER_SESSION_TIMEOUT_SEC +cfg.isBenchMode() // SERVER_BENCH_MODE +cfg.setServerPort(9000); // override in code +``` -## get_string +### TLS -``` cpp -std::string value = cfg.get_string("server.host", "127.0.0.1"); +```cpp +cfg.isTlsEnabled() // SERVER_TLS_ENABLED +cfg.getTlsCertFile() // SERVER_TLS_CERT_FILE +cfg.getTlsKeyFile() // SERVER_TLS_KEY_FILE +cfg.getTlsConfig() // full TLS config object ``` ------------------------------------------------------------------------- +> When Nginx handles HTTPS, keep `SERVER_TLS_ENABLED=false`. Nginx terminates TLS, Vix stays plain HTTP locally. -## get_int +### Database -``` cpp -int port = cfg.get_int("server.port", 8080); +```cpp +cfg.getDbHost() // DATABASE_DEFAULT_HOST +cfg.getDbUser() // DATABASE_DEFAULT_USER +cfg.getDbName() // DATABASE_DEFAULT_NAME +cfg.getDbPort() // DATABASE_DEFAULT_PORT ``` ------------------------------------------------------------------------- +Password priority: `VIX_DB_PASSWORD` → `DATABASE_DEFAULT_PASSWORD` → `DB_PASSWORD` → `MYSQL_PASSWORD`. -## get_bool +### Logging -``` cpp -bool debug = cfg.get_bool("runtime.debug", false); +```cpp +cfg.getLogAsync() // LOGGING_ASYNC +cfg.getLogQueueMax() // LOGGING_QUEUE_MAX +cfg.getLogDropOnOverflow() // LOGGING_DROP_ON_OVERFLOW ``` ------------------------------------------------------------------------- - -## get_double +### WAF -``` cpp -double ratio = cfg.get_double("limits.ratio", 0.5); +```cpp +cfg.getWafMode() // WAF_MODE: off | basic | strict +cfg.getWafMaxTargetLen() // WAF_MAX_TARGET_LEN +cfg.getWafMaxBodyBytes() // WAF_MAX_BODY_BYTES ``` ------------------------------------------------------------------------- +## Generic accessors -# Dot Path Access +Dotted keys map to uppercase env names: -Nested JSON can be accessed using dot notation. +```cpp +cfg.getString("database.engine", "sqlite"); // DATABASE_ENGINE +cfg.getString("database.default.host", "127.0.0.1"); // DATABASE_DEFAULT_HOST +cfg.getInt("database.default.port", 3306); // DATABASE_DEFAULT_PORT +cfg.getBool("server.bench_mode", false); // SERVER_BENCH_MODE +cfg.has("database.default.host"); // check existence +cfg.set("server.port", 9090); // in-memory override +``` -Given: +## Config with database -``` json -{ - "database": { - "host": "localhost", - "port": 5432 - } -} +```cpp +vix::config::Config cfg{".env"}; +vix::db::Database db{cfg}; // creates database from config ``` -Access: +Build flags still required: `vix run main.cpp --with-sqlite` or `--with-mysql`. + +## Config with WebSocket -``` cpp -cfg.get_string("database.host", "127.0.0.1"); -cfg.get_int("database.port", 3306); +```cpp +vix::config::Config core_cfg{".env"}; +vix::websocket::Config ws_cfg = vix::websocket::Config::from_core(core_cfg); + +std::cout << ws_cfg.maxMessageSize << "\n"; // WEBSOCKET_MAX_MESSAGE_SIZE +std::cout << ws_cfg.idleTimeout.count() << "\n"; // WEBSOCKET_IDLE_TIMEOUT +std::cout << ws_cfg.pingInterval.count() << "\n"; // WEBSOCKET_PING_INTERVAL ``` ------------------------------------------------------------------------- +## Config with HTTP + WebSocket -# Minimal Usage Example +```cpp +vix::config::Config cfg{".env"}; +auto executor = std::make_shared(1u); +vix::App app{executor}; +vix::websocket::Server ws{cfg, executor}; +vix::run_http_and_ws(app, ws, executor, cfg); +``` -``` cpp -#include -#include +## Production recommendations -int main() -{ - vix::config::Config cfg{"config.json"}; +```dotenv +SERVER_PORT=8080 +SERVER_TLS_ENABLED=false # let Nginx handle TLS +WAF_MODE=basic +LOGGING_ASYNC=true +LOGGING_DROP_ON_OVERFLOW=true +``` - std::string host = cfg.get_string("server.host", "0.0.0.0"); - int port = cfg.get_int("server.port", 8080); +For direct TLS (without Nginx): - std::cout << host << ":" << port << "\n"; +```dotenv +SERVER_TLS_ENABLED=true +SERVER_TLS_CERT_FILE=/etc/letsencrypt/live/example.com/fullchain.pem +SERVER_TLS_KEY_FILE=/etc/letsencrypt/live/example.com/privkey.pem +``` - return 0; -} +## Recommended .env.example + +```dotenv +DATABASE_ENGINE=sqlite +DATABASE_SQLITE_PATH=vix.db +DATABASE_DEFAULT_HOST=127.0.0.1 +DATABASE_DEFAULT_PORT=3306 +DATABASE_DEFAULT_USER=root +DATABASE_DEFAULT_PASSWORD= +DATABASE_DEFAULT_NAME=vix +SERVER_PORT=8080 +SERVER_REQUEST_TIMEOUT=2000 +SERVER_IO_THREADS=0 +SERVER_TLS_ENABLED=false +SERVER_TLS_CERT_FILE= +SERVER_TLS_KEY_FILE= +LOGGING_ASYNC=true +LOGGING_QUEUE_MAX=20000 +LOGGING_DROP_ON_OVERFLOW=true +WAF_MODE=basic +WAF_MAX_TARGET_LEN=4096 +WAF_MAX_BODY_BYTES=1048576 +WEBSOCKET_MAX_MESSAGE_SIZE=65536 +WEBSOCKET_IDLE_TIMEOUT=60 +WEBSOCKET_ENABLE_DEFLATE=true +WEBSOCKET_PING_INTERVAL=30 +WEBSOCKET_AUTO_PING_PONG=true ``` ------------------------------------------------------------------------- +Commit `.env.example` (safe defaults, no secrets). Never commit `.env` with real credentials. -# Environment Override Pattern +## Common mistakes -Configuration files should not contain secrets in production. +### Forgetting database build flags -Example override: +```bash +vix run main.cpp --with-sqlite # SQLite +vix run main.cpp --with-mysql # MySQL +``` -``` cpp -#include -#include +### Using the wrong key style -int main() -{ - vix::config::Config cfg{"config.json"}; +```cpp +cfg.getInt("server.port", 8080); // dotted — for generic getters +// SERVER_PORT=8080 // uppercase — in .env file +``` - int port = cfg.get_int("server.port", 8080); +### Expecting Nginx TLS and Vix TLS simultaneously - if (const char* env = std::getenv("PORT")) - port = std::atoi(env); +If Nginx handles HTTPS, always keep `SERVER_TLS_ENABLED=false`. - return port; -} -``` +## What you should remember ------------------------------------------------------------------------- +```cpp +vix::config::Config cfg{".env"}; +app.run(cfg); -# Design Notes +cfg.getServerPort(); +cfg.isTlsEnabled(); +cfg.getDbHost(); +cfg.getLogAsync(); +cfg.getWafMode(); -- Configuration is read at startup. -- No global mutable config state. -- Safe access always requires fallback. -- Deterministic behavior: no implicit environment merging. +cfg.getString("database.engine", "sqlite"); +cfg.getInt("database.default.port", 3306); +``` -The Config API is intentionally small and predictable. +The core idea: configuration should live outside your C++ source code. +Next: [WebSocket API](/api/websocket) diff --git a/docs/api/http.md b/docs/api/http.md index b5cfee4..3507848 100644 --- a/docs/api/http.md +++ b/docs/api/http.md @@ -1,232 +1,438 @@ # HTTP API -This page documents the core HTTP primitives in Vix. +The HTTP API is the core application API of Vix. It gives you the main building blocks for web applications and APIs. -It covers: +```txt +App — Request — Response — Routes — Static files — Server lifecycle +``` -- App -- Routing methods -- Request -- Response +## Public header -All examples are minimal and placed entirely inside `main()`. +```cpp +#include +``` ------------------------------------------------------------------------- +## Basic example -# App +```cpp +#include +using namespace vix; -Header: +int main() +{ + App app; -``` cpp -#include -``` + app.get("/", [](Request &, Response &res) { + res.text("Hello from Vix"); + }); -Core type: + app.run(8080); -``` cpp -vix::App + return 0; +} ``` -### Constructor +## App + +`App` is the main application object. It owns the router, middleware chain, server lifecycle, static files, and runtime configuration. -``` cpp +```cpp App app; ``` -### Run +### Server lifecycle -``` cpp -app.run(8080); -``` +```cpp +app.run(8080); // start and block -Starts the HTTP server on the given port (blocking call). +app.listen(8080, []() { // start async with callback + std::cout << "listening\n"; +}); ------------------------------------------------------------------------- +app.wait(); // block until server stops +app.close(); // stop the server +``` -# Routing Methods +## Routes -## GET +A route connects an HTTP method and path to a C++ handler. -``` cpp -app.get(path, handler); +```cpp +app.get("/users/{id}", [](Request &req, Response &res){ + const std::string id = req.param("id"); + res.json({"id", id}); +}); ``` -## POST +### HTTP methods -``` cpp -app.post(path, handler); +```cpp +app.get("/users", list_handler); +app.post("/users", create_handler); +app.put("/users/{id}", replace_handler); +app.patch("/users/{id}", update_handler); +app.del("/users/{id}", delete_handler); +app.head("/health", head_handler); +app.options("/users", options_handler); ``` -## PUT +### Path parameters + +```cpp +// Matches /users/1, /users/42, /users/abc +app.get("/users/{id}", [](Request &req, Response &res){ + res.json({"id", req.param("id")}); +}); -``` cpp -app.put(path, handler); +// Multiple parameters +app.get("/posts/{year}/{slug}", [](Request &req, Response &res){ + res.json({ + "year", req.param("year"), + "slug", req.param("slug") + }); +}); ``` -## DELETE +### Query parameters -``` cpp -app.del(path, handler); -``` +```cpp +// /users?page=2&limit=10 +app.get("/users", [](Request &req, Response &res){ + const std::string page = req.query_value("page", "1"); + const std::string limit = req.query_value("limit", "20"); ------------------------------------------------------------------------- + res.json({ + "page", page, + "limit", limit + }); +}); +``` -## Minimal Example +Use path parameters to identify a resource (`/users/42`), query parameters to modify how resources are read (`/users?page=2`). + +## Request + +| Method | Purpose | +|--------|---------| +| `req.param("id")` | Read a path parameter | +| `req.param("id", "0")` | Read with fallback | +| `req.query_value("page", "1")` | Read a query parameter with fallback | +| `req.query()` | Read all query parameters | +| `req.header("Authorization")` | Read a request header | +| `req.body()` | Read the raw request body | +| `req.json()` | Read the parsed JSON body | +| `req.path()` | Read the request path | +| `req.method()` | Read the HTTP method | + +### Read JSON body + +```cpp +app.post("/users", [](Request &req, Response &res){ + + const auto &body = req.json(); + if (!body.is_object()) + { + res.status(400).json({ + "ok", false, + "error", "expected JSON object" + }); + return; + } + + const std::string name = body.value("name", ""); + if (name.empty()) + { + res.status(400).json({ + "ok", false, + "error", "name is required" + }); + return; + } + + res.status(201).json({ + "ok", true, + "name", name + }); -``` cpp -#include +}); +``` -using namespace vix; +## Response + +| Method | Purpose | +|--------|---------| +| `res.text("Hello")` | Send plain text | +| `res.send("Hello")` | Send a basic response body | +| `res.json({...})` | Send JSON | +| `res.status(201)` | Set HTTP status (chainable) | +| `res.header("X-Foo", "bar")` | Set a response header | +| `res.file("public/index.html")` | Send a file | + +```cpp +res.status(201).json({ + "ok", true, + "message", "created" +}); +res.header("X-Powered-By", "Vix.cpp"); +res.file("public/index.html"); +``` -int main() +## Status codes + +| Status | Meaning | +|--------|---------| +| 200 | OK | +| 201 | Created | +| 204 | No Content | +| 400 | Bad Request | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not Found | +| 409 | Conflict | +| 429 | Too Many Requests | +| 500 | Internal Server Error | + +## Error helper + +```cpp +static void respond_error(Response &res, int status, + const std::string &code, + const std::string &message) { - App app; - - app.get("/", [](Request&, Response& res) { - res.send("Hello"); + res.status(status).json({ + "ok", false, + "error", code, + "message", message }); +} - app.run(8080); +// Always return after sending an error +if (id == "0") { + respond_error(res, 404, "user_not_found", "User not found"); + return; } ``` ------------------------------------------------------------------------- +## Good response shapes -# Request - -Type: - -``` cpp -vix::Request +```json +{ "ok": true, "data": {} } +{ "ok": true, "count": 2, "data": [] } +{ "ok": false, "error": "validation_failed", "message": "name is required" } ``` -Provides access to: - -- Path parameters -- Query parameters -- Headers -- Body -- JSON payload +## Route order ------------------------------------------------------------------------- +Register specific routes before generic ones: -## Path parameter +```cpp +// Correct +app.get("/users/search", search_handler); +app.get("/users/{id}", user_handler); +app.get("/*", fallback_handler); -``` cpp -req.param("id", "0"); +// Wrong — wildcard catches everything first +app.get("/*", fallback_handler); +app.get("/users/{id}", user_handler); ``` -Returns string value or fallback. +## Wildcard and static files ------------------------------------------------------------------------- - -## Query parameter +```cpp +// Manual static file fallback +app.get("/*", [](Request &req, Response &res){ + res.header("Cache-Control", "public, max-age=86400"); + res.file("public" + req.path()); +}); -``` cpp -req.query_value("page", "1"); +// Simple static directory +app.static_dir("public"); ``` ------------------------------------------------------------------------- +## Route groups -## Headers +```cpp +app.group("/api", [](auto &api){ + api.get("/health", [](Request &, Response &res) { + res.json({"ok", true}); + }); + + api.get("/users", [](Request &, Response &res) { + res.json({"ok", true}); + }); -``` cpp -req.header("User-Agent"); -req.has_header("Authorization"); + api.group("/admin", [](auto &admin){ + admin.get("/stats", [](Request &, Response &res){ + res.json({"ok", true}); + }); + }); +}); +// Routes: GET /api/health, GET /api/users, GET /api/admin/stats ``` ------------------------------------------------------------------------- +## Organizing routes -## Body +```cpp +static void register_public_routes(App &app) +{ + app.get("/", [](Request &, Response &res) { + res.json({"message", "Hello"}); + }); -``` cpp -std::string body = req.body(); -``` + app.get("/health", [](Request &, Response &res) { + res.json({"ok", true}); + }); +} ------------------------------------------------------------------------- +int main() +{ + App app; -## JSON + register_public_routes(app); -``` cpp -const auto& j = req.json(); + app.run(8080); + + return 0; +} ``` -Returns parsed JSON (high-level JSON layer). +## Complete example ------------------------------------------------------------------------- +```cpp +#include +using namespace vix; -# Response +static void respond_error(Response &res, int status, + const std::string &code, const std::string &message) +{ + res.status(status).json({ + "ok", false, + "error", code, + "message", message + }); +} -Type: +static void register_user_routes(App &app) +{ + app.get("/users", [](Request &req, Response &res){ + res.json({ + "ok", true, + "page", req.query_value("page", "1"), + "data", vix::json::array({"Alice", "Bob"}) + }); + }); -``` cpp -vix::Response -``` + app.get("/users/{id}", [](Request &req, Response &res){ + const std::string id = req.param("id"); + if (id == "0") { + respond_error(res, 404, "user_not_found", "User not found"); + return; + } + + res.json({ + "ok", true, + "data", vix::json::o("id", id), + "name", "Ada" + }); + }); -Controls HTTP output. + app.post("/users", [](Request &req, Response &res){ + const auto &body = req.json(); + if (!body.is_object()) { + respond_error(res, 400, "invalid_body", "Expected JSON object"); + return; + } + + const std::string name = body.value("name", ""); + if (name.empty()) { + respond_error(res, 400, "validation_failed", "name is required"); + return; + } + + res.status(201).json({ + "ok", true, + "message", "user created" + }); + }); ------------------------------------------------------------------------- + app.del("/users/{id}", [](Request &req, Response &res){ + const std::string id = req.param("id"); + if (id == "0") { + respond_error(res, 404, "user_not_found", "User not found"); + return; + } + + res.json({ + "ok", true, + "message", "user deleted", + "id", id + }); + }); +} -## Status +int main() +{ + App app; -``` cpp -res.status(201); -res.set_status(404); -``` + app.get("/health", [](Request &, Response &res) { + res.json({"ok", true}); + }); ------------------------------------------------------------------------- + register_user_routes(app); -## Send text + app.run(8080); -``` cpp -res.send("Hello"); + return 0; +} ``` ------------------------------------------------------------------------- +## Test -## Send JSON - -``` cpp -res.json({"message", "ok"}); +```bash +curl -i http://127.0.0.1:8080/health +curl -i "http://127.0.0.1:8080/users?page=2" +curl -i http://127.0.0.1:8080/users/1 +curl -i -X POST http://127.0.0.1:8080/users \ + -H "Content-Type: application/json" -d '{"name":"Charlie"}' +curl -i -X DELETE http://127.0.0.1:8080/users/1 ``` ------------------------------------------------------------------------- +## Common mistakes -## Auto-send return +### Missing leading slash -If a route handler returns: +```cpp +app.get("health", handler); // Wrong +app.get("/health", handler); // Correct +``` -- `std::string` → text response -- JSON object → JSON response +### Forgetting to return after an error -Example: +```cpp +// Wrong — continues after error +respond_error(res, 400, "...", "..."); +res.status(201).json({"ok", true}); -``` cpp -app.get("/auto", [](Request&, Response&) { - return vix::json::o("message", "auto"); -}); +// Correct +respond_error(res, 400, "...", "..."); +return; ``` ------------------------------------------------------------------------- - -# 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. +### Using -- for runtime arguments ------------------------------------------------------------------------- +```bash +vix run main.cpp -- --port 8080 # Wrong — compiler flags +vix run main.cpp --run --port 8080 # Correct — runtime args +``` -# Design Philosophy +## What you should remember -The HTTP layer is: +The HTTP API is built around three types: `App`, `Request`, `Response`. -- Minimal -- Explicit -- Deterministic -- Zero hidden middleware stack by default +```txt +client → Request → route handler → Response +``` -You control exactly what happens in each handler. +The most common starting point: `#include ` +Next: [JSON API](/api/json) diff --git a/docs/api/index.md b/docs/api/index.md index 11fca02..f78bdf4 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -1,56 +1,114 @@ # 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. - +This section documents the public Vix APIs used to build applications. + +```cpp +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +``` + +## What this section is for + +The book teaches Vix step by step. The guides show practical recipes. The API reference explains the public API surface. + +Use this section when you want to answer: which header to include, which class to use, what an API does, how App/Request/Response work, how JSON/middleware/WebSocket/async/P2P fit together. + +## Main public API areas + +| Area | Header | Purpose | +|------|--------|---------| +| HTTP | `` | App, Request, Response, routes | +| JSON | `` | JSON values, objects, arrays, helpers | +| Middleware | `` | CORS, rate limiting, static files, adapters | +| Config | `` or `` | .env and runtime configuration | +| WebSocket | `` | Real-time bidirectional communication | +| Async | `` | Event loop, tasks, timers, signals, networking | +| Database | `` | SQLite, MySQL, queries, prepared statements | +| Cache | `` | Memory cache, file cache, TTL, stale data | +| Sync | `` | WAL, outbox, retry, offline-first sync | +| P2P | `` | Peer-to-peer runtime, discovery, messages | +| P2P HTTP | `` | HTTP control routes for P2P runtimes | +| Logging | `` | Structured logs and log levels | +| Validation | `` | Field validation, schemas, models | +| Filesystem | `` | Filesystem helpers | + +## The most common header + +For most HTTP applications: + +```cpp +#include + +using namespace vix; + +int main() +{ + App app; + app.get("/", [](Request &, Response &res) + { res.json({"message", "Hello from Vix"}); }); + app.run(8080); + return 0; +} +``` + +## How the APIs fit together + +```txt +HTTP routes → JSON responses → middleware → validation → database → logs + → cache or sync when needed + +HTTP API → WebSocket → async runtime (real-time systems) + +local write → cache → sync → P2P (offline-first systems) + +Vix app → release binary → systemd → Nginx (production systems) +``` + +## Public headers summary + +```txt +vix.hpp → core HTTP application API +vix/json.hpp → JSON API +vix/middleware.hpp → middleware API +vix/db.hpp → database API +vix/websocket.hpp → WebSocket API +vix/async.hpp → async runtime API +vix/cache.hpp → cache API +vix/sync.hpp → offline-first sync API +vix/p2p.hpp → P2P API +vix/p2p_http.hpp → P2P HTTP control API +vix/log.hpp → logging API +vix/validation.hpp → validation API +vix/fs.hpp → filesystem helpers +``` + +## Which page to read next + +| Need | Read | +|------|------| +| Build routes | [HTTP API](/api/http) | +| Return JSON | [JSON API](/api/json) | +| Add CORS or rate limiting | [Middleware API](/api/middleware) | +| Read .env settings | [Config API](/api/config) | +| Add realtime features | [WebSocket API](/api/websocket) | +| Learn runtime primitives | [Async API](/api/async) | +| Build distributed nodes | [P2P API](/api/p2p) | + +## What you should remember + +The API reference is not the best place to start learning Vix from zero — for that, read the book. Use this section when you already know what you want and need the exact public API surface. + +The most common starting point is `#include `. + +The core idea: Vix gives C++ a runtime-oriented application API. diff --git a/docs/api/json.md b/docs/api/json.md index 915a12a..bf102fe 100644 --- a/docs/api/json.md +++ b/docs/api/json.md @@ -1,260 +1,385 @@ # JSON API -This page documents the JSON facilities available in Vix. +The JSON API is used to build and parse structured data in Vix applications. -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 +```txt +API responses — request body parsing — objects — arrays — errors — sync payloads +``` -Header: +## Public headers -``` cpp -#include +```cpp +#include // for HTTP apps (includes JSON) +#include // for JSON-only code ``` -Namespace: +Common namespace: `vix::json`. With `using namespace vix`: `json::Json`, `json::obj(...)`, `json::array(...)`, `json::kv(...)`. -``` cpp -vix::json +## Basic JSON response + +```cpp +app.get("/health", [](Request &, Response &res){ + res.json({ + "ok", true, + "service", "json-api" + }); +}); ``` -Core types: +## The main JSON type -- `token` → generic JSON value -- `array_t` → JSON array -- `kvs` → JSON object storage +`vix::json::Json` represents: object, array, string, number, boolean, null. ------------------------------------------------------------------------- +```cpp +// Object +vix::json::Json data = vix::json::obj({ + {"ok", true}, + {"message", "Hello from Vix"} +}); -## token +// Array +vix::json::Json names = vix::json::array({"Alice", "Bob", "Charlie"}); -Represents any JSON value. +// Dynamic array +vix::json::Json items = vix::json::Json::array(); -Supported kinds: +items.push_back(vix::json::obj({ + {"id", 1}, + {"name", "Alice"} +})); -- null -- bool -- int64 -- double -- string -- array -- object +items.push_back(vix::json::obj({ + {"id", 2}, + {"name", "Bob"} +})); +``` -Example: +## Good response shapes -``` cpp -#include -#include +```json +{ "ok": true, "data": {} } +{ "ok": true, "count": 2, "data": [] } +{ "ok": false, "error": "validation_failed", "message": "name is required" } +``` -using namespace vix::json; +## res.json(...) -int main() -{ - token t = 42; +```cpp +res.json({ + "ok", true, + "message", "Hello" +}); - if (t.is_i64()) - std::cout << t.as_i64_or(0) << "\n"; +res.status(201).json({ + "ok", true, + "message", "created" +}); - return 0; -} +res.status(400).json({ + "ok", false, + "error", "bad_request" +}); ``` ------------------------------------------------------------------------- +## Build JSON with helpers -## array_t +```cpp +struct User { + std::int64_t id{}; + std::string name; + std::string role; +}; -Dynamic JSON array. - -Key methods: +static vix::json::Json user_to_json(const User &u) +{ + return vix::json::obj({ + {"id", u.id}, + {"name", u.name}, + {"role", u.role} + }); +} -- `push_int(...)` -- `push_string(...)` -- `push_bool(...)` -- `size()` -- `operator[]` +static vix::json::Json users_to_json(const std::vector &users) +{ + vix::json::Json items = vix::json::Json::array(); + for (const auto &u : users) + items.push_back(user_to_json(u)); + return items; +} -Example: +// Usage in route +res.json({ + "ok", true, + "count", static_cast(users.size()), + "data", users_to_json(users) +}); +``` -``` cpp -#include +## Read JSON request body + +```cpp +app.post("/users", [](Request &req, Response &res){ + const auto &body = req.json(); + + // Always check shape first + if (!body.is_object()) + { + res.status(400).json({ + "ok", false, + "error", "expected JSON object" + }); + return; + } + + // Read fields with fallback + const std::string name = body.value("name", ""); + const std::string role = body.value("role", "user"); + + if (name.empty()) + { + res.status(400).json({ + "ok", false, + "error", "name is required" + }); + return; + } + + res.status(201).json({ + "ok", true, + "name", name, + "role", role + }); +}); +``` -using namespace vix::json; +## Error helper -int main() +```cpp +static void respond_error(Response &res, int status, + const std::string &code, const std::string &message) { - array_t arr; - arr.push_int(1); - arr.push_string("two"); - - return 0; + res.status(status).json({ + "ok", false, + "error", code, + "message", message + }); } ``` ------------------------------------------------------------------------- - -## kvs +## Stable error codes -Object-like key/value storage. +```txt +invalid_body, validation_failed, not_found, unauthorized, forbidden, conflict, internal_error +``` -Key methods: +## JSON and validation -- `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()` +```cpp +const std::string email = body.value("email", ""); -Example: +auto result = vix::validation::validate("email", email) + .required().email().result(); +``` -``` cpp -#include +JSON answers: "what did the client send?" — Validation answers: "is this data valid?" -using namespace vix::json; +## JSON and database -int main() +```cpp +static json::Json user_row_to_json(const vix::db::Row &row) { - kvs obj; - obj.set_string("name", "Vix"); - - return 0; + return json::obj({ + {"id", row.getInt64(0)}, + {"name", row.getString(1)}, + {"role", row.getString(2)} + }); } ``` ------------------------------------------------------------------------- +Flow: `database row → C++ object → json::Json → res.json(...)` -# 2) High-Level JSON +## JSON and sync -Header: - -``` cpp -#include +```cpp +// Payloads stored as JSON strings must be replayable +Operation op; +op.kind = "message.send"; +op.payload = R"({"id":"msg_1","text":"hello","created_at":1710000000000})"; ``` -Powered by nlohmann::json. +## Complete example -Namespace helpers: +```cpp +#include +#include +#include +#include +#include +using namespace vix; -``` cpp -vix::json -``` +struct User { std::int64_t id{}; std::string name; std::string role; }; -Core type: - -``` cpp -Json -``` - ------------------------------------------------------------------------- +static json::Json user_to_json(const User &u) +{ + return json::obj({ + {"id", u.id}, + {"name", u.name}, + {"role", u.role} + }); +} -## Object Builder +static json::Json users_to_json(const std::vector &users) +{ + json::Json items = json::Json::array(); + for (const auto &u : users) + items.push_back(user_to_json(u)); + return items; +} -``` cpp -#include +static void respond_error(Response &res, int status, + const std::string &code, const std::string &message) +{ + res.status(status).json({ + "ok", false, + "error", code, + "message", message + }); +} -using namespace vix::json; +static std::optional parse_id(const std::string &text) +{ + try { + return std::stoll(text); + } catch (...) { + return std::nullopt; + } +} int main() { - auto j = o( - "name", "Vix", - "version", 1 - ); - + App app; + std::vector users{ + {1, "Alice", "admin"}, + {2, "Bob", "user"} + }; + + app.get("/health", [](Request &, Response &res) { + res.json({"ok", true}); + }); + + app.get("/users", [&users](Request &, Response &res){ + res.json({ + "ok", true, + "count", static_cast(users.size()), + "data", users_to_json(users) + }); + }); + + app.get("/users/{id}", [&users](Request &req, Response &res){ + const auto id = parse_id(req.param("id")); + if (!id) { respond_error(res, 400, "invalid_id", "Invalid user id"); return; } + for (const auto &u : users) + if (u.id == *id) { + res.json({ + "ok", true, + "data", user_to_json(u) + }); + return; + } + respond_error(res, 404, "user_not_found", "User not found"); + }); + + app.post("/users", [&users](Request &req, Response &res){ + const auto &body = req.json(); + if (!body.is_object()) { + respond_error(res, 400, "invalid_body", "Expected JSON object"); + return; + } + + const std::string name = body.value("name", ""); + if (name.empty()) { + respond_error(res, 400, "validation_failed", "name is required"); + return; + } + + const std::int64_t next_id = users.empty() ? 1 : users.back().id + 1; + users.push_back({next_id, name, body.value("role", "user")}); + + res.status(201).json({ + "ok", true, + "message", "user created" + }); + }); + + app.run(8080); return 0; } ``` ------------------------------------------------------------------------- - -## Array Builder - -``` cpp -auto arr = a(1, 2, 3); -``` - ------------------------------------------------------------------------- - -## kv initializer - -``` cpp -auto j = kv({ - {"a", 1}, - {"b", true} -}); -``` - ------------------------------------------------------------------------- +## Common mistakes -## dumps +### Returning different shapes everywhere -Serialize with indentation: +```cpp +// Bad — inconsistent +{ "success": true } +{ "status": "ok", "items": [] } -``` cpp -std::string s = dumps(j, 2); +// Good — predictable +{ "ok": true, "data": {} } ``` ------------------------------------------------------------------------- +### Reading fields without checking shape -## loads +```cpp +// Wrong +const std::string name = body.value("name", ""); -Parse string: - -``` cpp -auto j = loads(R"({"a":1})"); +// Correct +if (!body.is_object()) { + respond_error(res, 400, "invalid_body", "Expected JSON object"); + return; +} +const std::string name = body.value("name", ""); ``` ------------------------------------------------------------------------- +### Building JSON strings manually -## File IO +```cpp +// Wrong +std::string body = "{\"ok\":true,\"name\":\"" + name + "\"}"; -``` cpp -dump_file("out.json", j, 2); -auto j2 = load_file("out.json"); +// Correct +res.json({ + "ok", true, + "name", name +}); ``` ------------------------------------------------------------------------- - -## jset / jget (Path Access) +## What you should remember -Mutate nested structures using path strings. - -``` cpp -Json j = obj(); +```cpp +// success +res.json({ + "ok", true, + "data", {} +}); -jset(j, "user.profile.name", "Ada"); +// error +res.status(400).json({ + "ok", false, + "error", "code" +}); -if (auto v = jget(j, "user.profile.name")) -{ - // value exists +const auto &body = req.json(); +if (!body.is_object()) { + /* error */ + return; } -``` - ------------------------------------------------------------------------- - -# 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 +const std::string name = body.value("name", ""); +``` -The JSON API is explicit and deterministic. +The core idea: JSON is the public data shape of your API — keep it predictable. +Next: [Middleware API](/api/middleware) diff --git a/docs/api/kv.md b/docs/api/kv.md new file mode 100644 index 0000000..2a2b291 --- /dev/null +++ b/docs/api/kv.md @@ -0,0 +1,1583 @@ +# Vix KV API + +Vix KV is a small durable key-value engine for Vix applications. + +It gives you a simple public API: + +```cpp +auto kv = vix::kv::open("data/app"); + +kv.put("hello", "world"); + +auto value = kv.get("hello"); +``` + +The goal is simple: + +- simple API for application code +- durable storage when a path is used +- memory-only mode for tests and temporary data +- structured keys for nested data +- predictable errors for advanced usage +- optional direct API for fast prototyping + +Vix KV is designed for local-first and offline-first systems where local writes must be safe, observable, and recoverable. + +## Include + +Use the main public header: + +```cpp +#include +``` + +This header exposes the public API: + +- `vix::kv::Kv` +- `vix::kv::KvOptions` +- `vix::kv::KvValue` +- `vix::kv::KeyPath` +- `vix::kv::KvError` +- `vix::kv::KvErrorCode` +- `vix::kv::KvStats` +- `vix::kv::KvResult` + +## Quick example + +```cpp +#include + +#include + +int main() +{ + auto kv = vix::kv::open("data/app"); + + kv.put("hello", "world"); + + auto value = kv.get("hello"); + + if (value.has_value()) + { + std::cout << *value << '\n'; + } + + kv.close(); + + return 0; +} +``` + +Output: + +``` +world +``` + +## Two API styles + +Vix KV exposes two public styles. + +The first style is the simple direct API: + +```cpp +auto kv = vix::kv::open("data/app"); + +kv.put("hello", "world"); + +auto value = kv.get("hello"); +``` + +This style is useful for application code, examples, scripts, and fast prototyping. + +The second style is the explicit result API: + +```cpp +auto opened = vix::kv::open_memory(); + +if (opened.is_err()) +{ + std::cerr << opened.error().message() << '\n'; + return 1; +} + +auto kv = opened.move_value(); + +auto written = kv.set({"users", "1", "name"}, "Ada"); + +if (written.is_err()) +{ + std::cerr << written.error().message() << '\n'; + return 1; +} + +auto value = kv.get({"users", "1", "name"}); + +if (value.is_ok()) +{ + std::cout << value.value().to_string() << '\n'; +} +``` + +This style is useful for libraries, tests, backend systems, and code that needs explicit error handling. + +## Opening a database + +### Durable database + +```cpp +auto kv = vix::kv::open("data/app"); +``` + +This opens a durable KV database at `data/app`. + +This direct overload returns a `vix::kv::Kv` handle directly. If opening fails, it throws `std::runtime_error`. + +Use it when you want a very simple API: + +```cpp +auto kv = vix::kv::open("data/app"); + +kv.put("name", "Ada"); + +auto name = kv.get("name"); +``` + +### Durable database with explicit result + +```cpp +auto opened = vix::kv::open_durable("data/app"); + +if (opened.is_err()) +{ + std::cerr << opened.error().message() << '\n'; + return 1; +} + +auto kv = opened.move_value(); +``` + +This returns: + +``` +vix::kv::KvResult +``` + +Use it when you do not want exceptions. + +### Memory-only database + +```cpp +auto opened = vix::kv::open_memory(); + +if (opened.is_ok()) +{ + auto kv = opened.move_value(); +} +``` + +Memory-only mode does not persist data after the handle is closed. + +It is useful for: + +- unit tests +- temporary data +- examples +- benchmarks +- in-memory application state + +Example: + +```cpp +auto opened = vix::kv::open_memory(); + +if (opened.is_err()) +{ + return 1; +} + +auto kv = opened.move_value(); + +kv.set({"session", "token"}, "abc123"); +``` + +### Fast durable database + +```cpp +auto opened = vix::kv::open_fast("data/cache"); +``` + +Fast mode keeps durable storage enabled, but disables automatic flush. + +This is useful when you want to control when data is flushed: + +```cpp +auto opened = vix::kv::open_fast("data/cache"); + +if (opened.is_err()) +{ + return 1; +} + +auto kv = opened.move_value(); + +kv.set({"a"}, "one"); +kv.set({"b"}, "two"); + +kv.flush(); +``` + +### Opening with options + +Use `KvOptions` when you want full control. + +```cpp +auto options = vix::kv::KvOptions::durable("data/app"); + +options.auto_flush = false; + +auto opened = vix::kv::open(options); + +if (opened.is_err()) +{ + std::cerr << opened.error().message() << '\n'; + return 1; +} + +auto kv = opened.move_value(); +``` + +## KvOptions + +`KvOptions` describes how the database should be opened. + +### Memory-only options + +```cpp +auto options = vix::kv::KvOptions::memory_only(); +``` + +Memory-only options create a database that does not write to disk. + +```cpp +auto opened = vix::kv::open(options); +``` + +### Durable options + +```cpp +auto options = vix::kv::KvOptions::durable("data/app"); +``` + +Durable options create a database that can recover data after restart. + +```cpp +auto opened = vix::kv::open(options); +``` + +### Fast options + +```cpp +auto options = vix::kv::KvOptions::fast("data/cache"); +``` + +Fast options use durable storage but disable automatic flush. + +```cpp +auto opened = vix::kv::open(options); +``` + +## Public handle: vix::kv::Kv + +`vix::kv::Kv` is the public database handle. + +```cpp +vix::kv::Kv kv; +``` + +Most users should create a handle with: + +```cpp +auto kv = vix::kv::open("data/app"); +``` + +or: + +```cpp +auto opened = vix::kv::open_memory(); +auto kv = opened.move_value(); +``` + +## Simple direct API + +The direct API is designed for simple usage. + +### put + +```cpp +kv.put("hello", "world"); +``` + +Stores a string value. + +```cpp +auto kv = vix::kv::open("data/app"); + +kv.put("hello", "world"); +kv.put("users/1/name", "Ada"); +``` + +The direct API throws `std::runtime_error` when the operation fails. + +### get + +```cpp +auto value = kv.get("hello"); +``` + +Returns: + +``` +std::optional +``` + +Example: + +```cpp +auto value = kv.get("hello"); + +if (value.has_value()) +{ + std::cout << *value << '\n'; +} +else +{ + std::cout << "not found\n"; +} +``` + +### Missing key + +```cpp +auto value = kv.get("missing"); + +if (!value.has_value()) +{ + std::cout << "missing\n"; +} +``` + +Missing keys return `std::nullopt`. + +### Slash keys + +The direct API supports slash-separated keys: + +```cpp +kv.put("users/1/name", "Ada"); +``` + +This behaves like a structured key: + +```cpp +kv.set({"users", "1", "name"}, "Ada"); +``` + +So this works: + +```cpp +kv.put("users/1/name", "Ada"); + +auto value = kv.get("users/1/name"); +``` + +And this also works: + +```cpp +auto value = kv.get({"users", "1", "name"}); +``` + +## Explicit result API + +The explicit API returns `KvResult`. + +It does not throw for normal errors. + +### set + +```cpp +auto result = kv.set({"hello"}, "world"); +``` + +Returns: + +``` +vix::kv::KvResult +``` + +Example: + +```cpp +auto result = kv.set({"users", "1", "name"}, "Ada"); + +if (result.is_err()) +{ + std::cerr << result.error().message() << '\n'; +} +``` + +### get + +```cpp +auto value = kv.get({"hello"}); +``` + +Returns: + +``` +vix::kv::KvResult +``` + +Example: + +```cpp +auto value = kv.get({"hello"}); + +if (value.is_ok()) +{ + std::cout << value.value().to_string() << '\n'; +} +else +{ + std::cerr << value.error().message() << '\n'; +} +``` + +### erase + +```cpp +auto result = kv.erase(vix::kv::KeyPath{"hello"}); +``` + +Returns: + +``` +vix::kv::KvResult +``` + +Example: + +```cpp +auto result = kv.erase(vix::kv::KeyPath{"users", "1", "name"}); + +if (result.is_err()) +{ + std::cerr << result.error().message() << '\n'; +} +``` + +### contains + +```cpp +bool exists = kv.contains({"hello"}); +``` + +Example: + +```cpp +if (kv.contains({"users", "1", "name"})) +{ + std::cout << "exists\n"; +} +``` + +### list + +```cpp +auto entries = kv.list({"users"}); +``` + +Returns all live entries matching the prefix. + +Example: + +```cpp +auto entries = kv.list({"users"}); + +if (entries.is_ok()) +{ + for (const auto &[key, value] : entries.value()) + { + std::cout << key.at(0) << " = " + << value.to_string() + << '\n'; + } +} +``` + +### List all entries + +```cpp +auto entries = kv.list(); +``` + +This lists all live entries. + +```cpp +auto entries = kv.list(); + +if (entries.is_ok()) +{ + std::cout << entries.value().size() << '\n'; +} +``` + +## Keys + +Vix KV supports structured keys. + +A key is represented by: + +``` +vix::kv::KeyPath +``` + +Example: + +```cpp +vix::kv::KeyPath key{"users", "1", "name"}; +``` + +This key has three segments: + +- `users` +- `1` +- `name` + +### Creating keys + +#### Initializer list + +```cpp +vix::kv::KeyPath key{"users", "1", "name"}; +``` + +#### Single segment + +```cpp +auto key = vix::kv::KeyPath::from("hello"); +``` + +#### Append segments + +```cpp +vix::kv::KeyPath key; + +key.push_back("users"); +key.push_back("1"); +key.push_back("name"); +``` + +#### Append another path + +```cpp +vix::kv::KeyPath base{"users", "1"}; +vix::kv::KeyPath field{"profile", "name"}; + +base.append(field); +``` + +Result: + +``` +users/1/profile/name +``` + +### Key rules + +A valid key should: + +- contain at least one segment +- not contain empty segments +- not exceed the configured key limits +- be valid according to the key validator + +Example of an invalid key: + +```cpp +vix::kv::KeyPath key{}; +``` + +Example of another invalid key: + +```cpp +vix::kv::KeyPath key{"users", "", "name"}; +``` + +## Values + +A value is represented by: + +``` +vix::kv::KvValue +``` + +The simple API uses strings: + +```cpp +kv.put("hello", "world"); +``` + +The explicit API uses `KvValue` internally: + +```cpp +auto value = vix::kv::KvValue::from_string("world"); + +kv.set({"hello"}, value); +``` + +### Creating values + +#### From string + +```cpp +auto value = vix::kv::KvValue::from_string("hello"); +``` + +#### Empty value + +Empty values are allowed: + +```cpp +kv.set({"empty"}, ""); +``` + +Reading it returns an empty string: + +```cpp +auto value = kv.get({"empty"}); + +if (value.is_ok()) +{ + std::cout << value.value().to_string() << '\n'; +} +``` + +## Persistence + +When you open Vix KV with a path, data is durable. + +```cpp +{ + auto kv = vix::kv::open("data/app"); + + kv.put("hello", "world"); + + kv.flush(); + + kv.close(); +} + +{ + auto kv = vix::kv::open("data/app"); + + auto value = kv.get("hello"); + + if (value.has_value()) + { + std::cout << *value << '\n'; + } + + kv.close(); +} +``` + +Output: + +``` +world +``` + +## Recovery + +Vix KV uses a WAL internally for durable writes. + +When a durable database is opened again, Vix KV can recover previous writes from the WAL. + +Example: + +```cpp +const auto path = "data/app"; + +{ + auto kv = vix::kv::open(path); + + kv.put("users/1/name", "Ada"); + kv.put("users/2/name", "Grace"); + + kv.flush(); + kv.close(); +} + +{ + auto kv = vix::kv::open(path); + + auto user1 = kv.get("users/1/name"); + auto user2 = kv.get("users/2/name"); + + if (user1.has_value()) + { + std::cout << *user1 << '\n'; + } + + if (user2.has_value()) + { + std::cout << *user2 << '\n'; + } + + kv.close(); +} +``` + +Output: + +``` +Ada +Grace +``` + +### Delete persistence + +Deletes are also durable. + +```cpp +const auto path = "data/app"; + +{ + auto kv = vix::kv::open(path); + + kv.put("hello", "world"); + + kv.erase(vix::kv::KeyPath{"hello"}); + + kv.flush(); + kv.close(); +} + +{ + auto kv = vix::kv::open(path); + + auto value = kv.get("hello"); + + if (!value.has_value()) + { + std::cout << "deleted\n"; + } + + kv.close(); +} +``` + +Output: + +``` +deleted +``` + +### Put after delete + +A later write can restore a deleted key. + +```cpp +const auto path = "data/app"; + +{ + auto kv = vix::kv::open(path); + + kv.put("hello", "old"); + + kv.erase(vix::kv::KeyPath{"hello"}); + + kv.put("hello", "new"); + + kv.flush(); + kv.close(); +} + +{ + auto kv = vix::kv::open(path); + + auto value = kv.get("hello"); + + if (value.has_value()) + { + std::cout << *value << '\n'; + } + + kv.close(); +} +``` + +Output: + +``` +new +``` + +## Flush + +`flush()` forces pending durable data to be flushed. + +```cpp +auto result = kv.flush(); + +if (result.is_err()) +{ + std::cerr << result.error().message() << '\n'; +} +``` + +In memory-only mode, `flush()` succeeds as a no-op. + +In durable mode, it flushes durable state. + +In fast mode, it is important because automatic flush is disabled. + +## Close + +`close()` closes the database handle. + +```cpp +auto result = kv.close(); + +if (result.is_err()) +{ + std::cerr << result.error().message() << '\n'; +} +``` + +Calling `close()` more than once is safe. + +```cpp +kv.close(); +kv.close(); +``` + +After a handle is closed, explicit operations return `NotOpen`: + +```cpp +auto result = kv.set({"hello"}, "world"); + +if (result.is_err()) +{ + std::cerr << result.error().message() << '\n'; +} +``` + +The direct API throws after close: + +```cpp +try +{ + kv.put("hello", "world"); +} +catch (const std::runtime_error &error) +{ + std::cerr << error.what() << '\n'; +} +``` + +## Size and empty + +### size + +```cpp +std::size_t count = kv.size(); +``` + +Returns the number of live keys. + +```cpp +kv.put("a", "one"); +kv.put("b", "two"); + +std::cout << kv.size() << '\n'; +``` + +Output: + +``` +2 +``` + +### empty + +```cpp +bool is_empty = kv.empty(); +``` + +Returns `true` when no live key is visible. + +```cpp +if (kv.empty()) +{ + std::cout << "empty\n"; +} +``` + +## Stats + +`stats()` returns runtime information about the database. + +```cpp +auto stats = kv.stats(); +``` + +The public type is: + +``` +vix::kv::KvStats +``` + +Example: + +```cpp +auto stats = kv.stats(); + +std::cout << "open: " << stats.open << '\n'; +std::cout << "memory_only: " << stats.memory_only << '\n'; +std::cout << "wal_enabled: " << stats.wal_enabled << '\n'; +std::cout << "keys: " << stats.key_count << '\n'; +std::cout << "tombstones: " << stats.tombstone_count << '\n'; +std::cout << "last_sequence: " << stats.last_sequence << '\n'; +``` + +Useful fields include: + +- `stats.open` +- `stats.memory_only` +- `stats.wal_enabled` +- `stats.auto_flush` +- `stats.key_count` +- `stats.tombstone_count` +- `stats.memtable_entries` +- `stats.last_sequence` +- `stats.set_count` +- `stats.get_count` +- `stats.get_miss_count` +- `stats.erase_count` +- `stats.list_count` +- `stats.flush_count` +- `stats.error_count` +- `stats.wal_records_recovered` +- `stats.last_recovered_sequence` + +### Stats example + +```cpp +auto opened = vix::kv::open_memory(); + +if (opened.is_err()) +{ + return 1; +} + +auto kv = opened.move_value(); + +kv.set({"a"}, "one"); +kv.set({"b"}, "two"); +kv.erase(vix::kv::KeyPath{"a"}); + +auto stats = kv.stats(); + +std::cout << stats.key_count << '\n'; +std::cout << stats.tombstone_count << '\n'; +std::cout << stats.last_sequence << '\n'; +``` + +Expected behavior: + +``` +1 +1 +3 +``` + +The exact values can grow depending on previous operations and implementation details, but the visible state should show one live key and at least one tombstone. + +## Error handling + +The explicit API uses: + +``` +vix::kv::KvResult +``` + +Example: + +```cpp +auto result = kv.set({"hello"}, "world"); + +if (result.is_ok()) +{ + std::cout << "written\n"; +} + +if (result.is_err()) +{ + std::cerr << result.error().message() << '\n'; +} +``` + +### KvResult + +A successful result: + +```cpp +vix::kv::KvResult result = + vix::kv::KvResult::ok(42); +``` + +An error result: + +```cpp +auto result = vix::kv::KvResult::err( + vix::kv::KvError::not_found("missing key")); +``` + +Using a result: + +```cpp +if (result.is_ok()) +{ + std::cout << result.value() << '\n'; +} +else +{ + std::cerr << result.error().message() << '\n'; +} +``` + +Moving a value out: + +```cpp +auto value = result.move_value(); +``` + +### Error codes + +Public error codes are represented by: + +``` +vix::kv::KvErrorCode +``` + +Common error codes: + +- `vix::kv::KvErrorCode::Ok` +- `vix::kv::KvErrorCode::InvalidArgument` +- `vix::kv::KvErrorCode::InvalidKey` +- `vix::kv::KvErrorCode::NotFound` +- `vix::kv::KvErrorCode::AlreadyExists` +- `vix::kv::KvErrorCode::AlreadyOpen` +- `vix::kv::KvErrorCode::NotOpen` +- `vix::kv::KvErrorCode::IoError` +- `vix::kv::KvErrorCode::Corruption` +- `vix::kv::KvErrorCode::ChecksumMismatch` +- `vix::kv::KvErrorCode::WalError` +- `vix::kv::KvErrorCode::StorageError` +- `vix::kv::KvErrorCode::SnapshotError` +- `vix::kv::KvErrorCode::CompactionError` +- `vix::kv::KvErrorCode::ConfigError` +- `vix::kv::KvErrorCode::Unsupported` +- `vix::kv::KvErrorCode::InternalError` +- `vix::kv::KvErrorCode::Unknown` + +Convert an error code to a stable string: + +```cpp +auto text = vix::kv::core::to_string( + vix::kv::KvErrorCode::NotFound); +``` + +Output: + +``` +not_found +``` + +### Missing keys + +With the direct API: + +```cpp +auto value = kv.get("missing"); + +if (!value.has_value()) +{ + std::cout << "missing\n"; +} +``` + +With the explicit API: + +```cpp +auto value = kv.get({"missing"}); + +if (value.is_err() && + value.error().code() == vix::kv::KvErrorCode::NotFound) +{ + std::cout << "missing\n"; +} +``` + +## Direct API versus explicit API + +Use the direct API when you want simple application code: + +```cpp +auto kv = vix::kv::open("data/app"); + +kv.put("hello", "world"); + +auto value = kv.get("hello"); +``` + +Use the explicit API when you need precise error handling: + +```cpp +auto opened = vix::kv::open_durable("data/app"); + +if (opened.is_err()) +{ + return 1; +} + +auto kv = opened.move_value(); + +auto written = kv.set({"hello"}, "world"); + +if (written.is_err()) +{ + return 1; +} +``` + +## Complete direct API example + +```cpp +#include + +#include + +int main() +{ + auto kv = vix::kv::open("data/app"); + + kv.put("users/1/name", "Ada"); + kv.put("users/2/name", "Grace"); + + auto first = kv.get("users/1/name"); + auto second = kv.get("users/2/name"); + + if (first.has_value()) + { + std::cout << "user 1: " << *first << '\n'; + } + + if (second.has_value()) + { + std::cout << "user 2: " << *second << '\n'; + } + + kv.close(); + + return 0; +} +``` + +Output: + +``` +user 1: Ada +user 2: Grace +``` + +## Complete explicit API example + +```cpp +#include + +#include + +int main() +{ + auto opened = vix::kv::open_memory(); + + if (opened.is_err()) + { + std::cerr << opened.error().message() << '\n'; + return 1; + } + + auto kv = opened.move_value(); + + auto written = kv.set({"users", "1", "name"}, "Ada"); + + if (written.is_err()) + { + std::cerr << written.error().message() << '\n'; + return 1; + } + + auto value = kv.get({"users", "1", "name"}); + + if (value.is_err()) + { + std::cerr << value.error().message() << '\n'; + return 1; + } + + std::cout << value.value().to_string() << '\n'; + + auto closed = kv.close(); + + if (closed.is_err()) + { + std::cerr << closed.error().message() << '\n'; + return 1; + } + + return 0; +} +``` + +Output: + +``` +Ada +``` + +## Listing example + +```cpp +#include + +#include + +int main() +{ + auto opened = vix::kv::open_memory(); + + if (opened.is_err()) + { + return 1; + } + + auto kv = opened.move_value(); + + kv.set({"users", "1", "name"}, "Ada"); + kv.set({"users", "2", "name"}, "Grace"); + kv.set({"settings", "theme"}, "dark"); + + auto users = kv.list({"users"}); + + if (users.is_err()) + { + std::cerr << users.error().message() << '\n'; + return 1; + } + + for (const auto &[key, value] : users.value()) + { + for (std::size_t index = 0; index < key.size(); ++index) + { + if (index > 0) + { + std::cout << "/"; + } + + std::cout << key.at(index); + } + + std::cout << " = " << value.to_string() << '\n'; + } + + kv.close(); + + return 0; +} +``` + +Possible output: + +``` +users/1/name = Ada +users/2/name = Grace +``` + +## Persistence example + +```cpp +#include + +#include + +int main() +{ + const auto path = "data/app"; + + { + auto kv = vix::kv::open(path); + + kv.put("hello", "world"); + + kv.flush(); + kv.close(); + } + + { + auto kv = vix::kv::open(path); + + auto value = kv.get("hello"); + + if (value.has_value()) + { + std::cout << *value << '\n'; + } + + kv.close(); + } + + return 0; +} +``` + +Output: + +``` +world +``` + +## Fast mode example + +```cpp +#include + +#include + +int main() +{ + auto opened = vix::kv::open_fast("data/cache"); + + if (opened.is_err()) + { + std::cerr << opened.error().message() << '\n'; + return 1; + } + + auto kv = opened.move_value(); + + kv.set({"a"}, "one"); + kv.set({"b"}, "two"); + + auto flushed = kv.flush(); + + if (flushed.is_err()) + { + std::cerr << flushed.error().message() << '\n'; + return 1; + } + + kv.close(); + + return 0; +} +``` + +## API reference + +### Open helpers + +```cpp +vix::kv::open(); +vix::kv::open(std::filesystem::path path); +vix::kv::open(const vix::kv::KvOptions &options); +vix::kv::open_memory(); +vix::kv::open_durable(std::filesystem::path path); +vix::kv::open_fast(std::filesystem::path path); +``` + +### Kv handle + +```cpp +kv.put("key", "value"); +kv.get("key"); + +kv.set({"key"}, "value"); +kv.get({"key"}); +kv.erase(vix::kv::KeyPath{"key"}); +kv.contains({"key"}); +kv.list({"prefix"}); +kv.list(); + +kv.flush(); +kv.close(); + +kv.size(); +kv.empty(); +kv.is_open(); +kv.options(); +kv.stats(); +``` + +### KeyPath + +```cpp +vix::kv::KeyPath key{"users", "1", "name"}; + +key.push_back("profile"); +key.append("avatar"); + +key.size(); +key.empty(); +key.byte_size(); + +key.at(0); +key.front(); +key.back(); +key.segments(); + +key.clear(); +``` + +### KvValue + +```cpp +auto value = vix::kv::KvValue::from_string("hello"); + +value.to_string(); +value.size(); +value.empty(); +value.bytes(); +``` + +### KvStats + +```cpp +auto stats = kv.stats(); + +stats.open; +stats.memory_only; +stats.wal_enabled; +stats.auto_flush; + +stats.key_count; +stats.tombstone_count; +stats.memtable_entries; +stats.memtable_bytes; + +stats.last_sequence; +stats.set_count; +stats.get_count; +stats.get_miss_count; +stats.erase_count; +stats.list_count; +stats.flush_count; +stats.error_count; + +stats.wal_records_recovered; +stats.last_recovered_sequence; + +stats.empty(); +stats.has_tombstones(); +stats.has_errors(); +stats.recovered_from_wal(); +stats.write_count(); +stats.read_count(); +stats.operation_count(); +``` + +## Recommended usage + +For application code: + +```cpp +auto kv = vix::kv::open("data/app"); + +kv.put("hello", "world"); + +auto value = kv.get("hello"); +``` + +For libraries and production systems: + +```cpp +auto opened = vix::kv::open_durable("data/app"); + +if (opened.is_err()) +{ + return 1; +} + +auto kv = opened.move_value(); + +auto result = kv.set({"hello"}, "world"); + +if (result.is_err()) +{ + return 1; +} +``` + +For tests: + +```cpp +auto opened = vix::kv::open_memory(); + +if (opened.is_err()) +{ + return 1; +} + +auto kv = opened.move_value(); +``` + +For high-throughput durable writes where you want manual flush: + +```cpp +auto opened = vix::kv::open_fast("data/cache"); + +if (opened.is_err()) +{ + return 1; +} + +auto kv = opened.move_value(); + +kv.set({"a"}, "one"); +kv.set({"b"}, "two"); + +kv.flush(); +``` + +## Design notes + +Vix KV keeps the public API small. + +The direct API is intentionally simple: + +```cpp +kv.put("hello", "world"); +auto value = kv.get("hello"); +``` + +The explicit API is intentionally precise: + +```cpp +auto result = kv.set({"hello"}, "world"); +``` + +The same engine supports both styles. + +This lets Vix KV work well in small examples, backend applications, tests, and durable local-first systems. diff --git a/docs/api/middleware.md b/docs/api/middleware.md index d28f95c..58a57a2 100644 --- a/docs/api/middleware.md +++ b/docs/api/middleware.md @@ -1,227 +1,306 @@ # Middleware API -This page documents the middleware system in Vix. +Middleware is code that runs around your routes. -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 +```txt +request → middleware → route handler → response ``` -Legacy middleware: +## Public header -``` cpp -#include +```cpp +#include +#include ``` ------------------------------------------------------------------------- - -# 1) Context Middleware (Recommended) +## Middleware shape -Type: - -``` cpp -vix::middleware::MiddlewareFn +```cpp +app.use([](Request &req, Response &res, App::Next next){ + res.header("X-Powered-By", "Vix.cpp"); + next(); // continue to next middleware or route +}); ``` -Signature: +Call `next()` to continue. Do not call `next()` if you've already sent the final response. + +## Middleware order -``` cpp -(Context& ctx, Next next) +```cpp +app.use(cors_middleware); // 1st +app.use(rate_limit_middleware); // 2nd +app.use(auth_middleware); // 3rd +register_routes(app); // always configure middleware before routes ``` ------------------------------------------------------------------------- +Recommended order: CORS → rate limit → security headers → body limit → authentication → routes. -## Minimal Example +## Global middleware -``` cpp -#include -#include +```cpp +app.use([](Request &, Response &res, App::Next next){ + res.header("X-App", "Vix"); + next(); +}); +``` -using namespace vix; +## Prefix middleware -int main() -{ - App app; +```cpp +app.use("/api", [](Request &, Response &res, App::Next next){ + res.header("X-API", "true"); + next(); +}); +``` - vix::middleware::MiddlewareFn mw = - [](vix::middleware::Context& ctx, vix::middleware::Next next) - { - // pre logic - next(); - // post logic - }; +Only applies to routes under `/api`. - app.use(mw); +## Route protection - app.get("/", [](Request&, Response& res) +```cpp +app.use("/admin", [](Request &req, Response &res, App::Next next){ + if (req.header("x-admin-token") != "secret") { - res.send("Hello"); - }); - - app.run(8080); -} + res.status(401).json({ + "ok", false, + "error", "unauthorized" + }); + return; // do not call next() + } + next(); +}); + +// Convenience APIs +app.protect("/admin", middleware); // prefix +app.protect_exact("/admin/hook", middleware); // exact path ``` ------------------------------------------------------------------------- +## CORS middleware -# Registering Middleware +```cpp +// Development +app.use(vix::middleware::app::cors_dev({ + "http://localhost:5173", + "http://127.0.0.1:5173" +})); -## Global +// Production with explicit options +#include -``` cpp -app.use(mw); +vix::middleware::security::CorsOptions options; +options.allowed_origins = {"https://app.example.com"}; +options.allow_any_origin = false; +options.allow_credentials = true; +options.allow_methods = {"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}; +options.allow_headers = {"Content-Type", "Authorization", "Accept"}; +options.expose_headers = {"X-Request-Id"}; + +app.use(vix::middleware::app::adapt_ctx( + vix::middleware::security::cors(std::move(options)))); ``` -Applies to all routes. +CORS is not authentication — it is a browser rule about which origins can call your API. ------------------------------------------------------------------------- +## Rate limiting middleware -## Prefix +```cpp +// Global limit +app.use(vix::middleware::app::rate_limit({ + .max_requests = 60, + .window_seconds = 60 +})); -``` cpp -app.use("/api/", mw); +// Stricter limit for auth routes +vix::middleware::app::use_on_prefix( + app, "/auth", + vix::middleware::app::rate_limit({ + .max_requests = 5, + .window_seconds = 60 + })); ``` -Applies only to routes under `/api/`. - ------------------------------------------------------------------------- +When exceeded, returns `429 Too Many Requests`. + +## Static files middleware + +```cpp +// Simple +app.static_dir("public"); + +// Advanced +#include +app.use(vix::middleware::app::adapt_ctx( + vix::middleware::performance::static_files( + std::filesystem::path{"public"}, + { + .mount = "/", + .index_file = "index.html", + .add_cache_control = true, + .cache_control = "public, max-age=3600", + .fallthrough = true, + }))); +``` -## Exact Path +## Request logging middleware -``` cpp -app.use_exact("/ping", mw); +```cpp +app.use([](Request &req, Response &, App::Next next){ + std::cout << "[request] " << req.method() << " " << req.path() << "\n"; + next(); +}); ``` -Applies only to the exact path. +## Security headers middleware ------------------------------------------------------------------------- +```cpp +app.use([](Request &, Response &res, App::Next next){ + res.header("X-Content-Type-Options", "nosniff"); + res.header("X-Frame-Options", "DENY"); + res.header("Referrer-Policy", "no-referrer"); + next(); +}); +``` -# Chaining Middleware +## Middleware with groups -Multiple middleware can be chained: +```cpp +auto api = app.group("/api"); -``` cpp -auto chained = vix::middleware::chain(mw1, mw2, mw3); +api.use([](Request &, Response &res, App::Next next){ + res.header("X-API", "true"); next(); +}); -app.use(chained); +api.get("/health", [](Request &, Response &res) { + res.json({"ok", true}); +}); ``` -Execution order: +## Complete example -- mw1 pre -- mw2 pre -- mw3 pre -- handler -- mw3 post -- mw2 post -- mw1 post +```cpp +#include +#include +#include +using namespace vix; ------------------------------------------------------------------------- +static void respond_error(Response &res, int status, + const std::string &code, const std::string &message) +{ + res.status(status).json({ + "ok", false, + "error", code, + "message", message + }); +} -# Request State Storage +static void configure_middlewares(App &app) +{ + app.use(vix::middleware::app::cors_dev({ + "http://localhost:5173", "http://127.0.0.1:5173" + })); -Context allows storing typed state per request. + app.use(vix::middleware::app::rate_limit({.max_requests = 60, .window_seconds = 60})); -``` cpp -ctx.emplace_state(42); + app.use([](Request &req, Response &, App::Next next){ + std::cout << req.method() << " " << req.path() << "\n"; next(); + }); -int& value = ctx.state(); -``` + app.use([](Request &, Response &res, App::Next next){ + res.header("X-Powered-By", "Vix.cpp"); next(); + }); -Safe access: + app.protect("/admin", [](Request &req, Response &res, App::Next next){ + if (req.header("x-admin-token") != "secret"){ + respond_error(res, 401, "unauthorized", "Invalid admin token"); + return; + } + next(); + }); +} -``` cpp -if (auto* v = ctx.try_state()) +static void register_routes(App &app) { - // state exists + app.get("/api/health", [](Request &, Response &res) { + res.json({"ok", true}); + }); + + app.get("/admin/stats", [](Request &, Response &res){ + res.json({ + "ok", true, + "message", "private admin stats" + }); + }); } -``` ------------------------------------------------------------------------- +int main() +{ + App app; -# 2) Legacy HTTP Middleware + configure_middlewares(app); + register_routes(app); -Type: + app.run(8080); -``` cpp -vix::HttpMiddleware + return 0; +} ``` -Signature: +## When to use middleware -``` cpp -(Request&, Response&, Next) -``` +Use middleware when a behavior applies to many routes: headers, auth checks, rate limiting, logging, CORS, static files, route group protection. -Example: +Avoid middleware when logic belongs to one route only — keep it in the route or service layer. -``` cpp -#include -#include +## Common mistakes -using namespace vix; +### Forgetting next() -int main() -{ - App app; +```cpp +// Wrong — request is silently dropped +app.use([](Request &, Response &res, App::Next next) { + res.header("X-App", "Vix"); +}); - HttpMiddleware mw = - [](Request& req, Response& res, auto next) - { - (void)req; - (void)res; - next(); - }; +// Correct +app.use([](Request &, Response &res, App::Next next) { + res.header("X-App", "Vix"); + next(); +}); +``` - app.use(mw); +### Calling next() after an error - app.get("/", [](Request&, Response& res) - { - res.send("Hello"); - }); +```cpp +// Wrong +if (!authorized) { + res.status(401).json({"ok", false}); +} +next(); - app.run(8080); +// Correct +if (!authorized) { + res.status(401).json({"ok", false}); + return; } +next(); ``` ------------------------------------------------------------------------- - -# Adapters +### Wrong CORS origin -Adapt legacy to context: +```cpp +// Wrong — that's the API, not the frontend +app.use(vix::middleware::app::cors_dev({"http://localhost:8080"})); -``` cpp -auto adapted = vix::middleware::adapt(mw); -app.use(adapted); +// Correct +app.use(vix::middleware::app::cors_dev({"http://localhost:5173"})); ``` -Adapt context to legacy: +## What you should remember -``` cpp -auto adapted = vix::middleware::adapt_ctx(ctx_mw); -app.use(adapted); +```txt +request → middleware → route handler → response ``` ------------------------------------------------------------------------- - -# 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. +Call `next()` only when the request should continue. Use middleware for shared behavior: CORS, rate limiting, security headers, authentication, logging, static files. +Next: [Config API](/api/config) diff --git a/docs/api/p2p.md b/docs/api/p2p.md index e69de29..d36258e 100644 --- a/docs/api/p2p.md +++ b/docs/api/p2p.md @@ -0,0 +1,453 @@ +# P2P API + +The P2P API is the distributed networking API of Vix. It provides building blocks for nodes that need to discover peers, connect, exchange protocol messages, and support offline-first replication. + +```txt +peer discovery — peer connection — message framing — handshake — routing — WAL replication — HTTP control +``` + +## Public headers + +```cpp +#include // core P2P runtime, protocol, routing, discovery +#include // HTTP control routes for P2P runtime +``` + +Lower-level headers when needed: + +```cpp +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +``` + +## Architecture + +```txt +Discovery → Peer endpoint → Transport → Framing → Envelope → Message dispatch → Runtime state +``` + +For offline-first replication: + +```txt +local write → WAL → WalPush → peer applies → WalAck → convergence +``` + +## Main namespaces + +| Namespace | Purpose | +|-----------|---------| +| `vix::p2p` | Core P2P runtime, protocol, routing, discovery | +| `vix::p2p::msg` | Typed P2P protocol messages | +| `vix::p2p::pack` | Envelope packing helpers | +| `vix::p2p::framing` | Framing implementations | +| `vix::p2p_http` | HTTP control routes for P2P runtime | + +## Runtime API + +### NodeConfig + +```cpp +vix::p2p::NodeConfig cfg; +cfg.node_id = "node-a"; // stable unique identity +cfg.listen_port = 9101; // TCP listen port + +cfg.on_log = [](std::string_view line) { + std::cout << line << "\n"; +}; +``` + +### Node + +```cpp +auto node = vix::p2p::make_tcp_node(cfg); +node->start(); // begin listening +node->wait(); // block until stopped +node->stop(); // shutdown +auto stats = node->stats(); +``` + +### P2PRuntime + +```cpp +vix::p2p::P2PRuntime runtime(node); +runtime.start(); + +// Connect to a peer +vix::p2p::PeerEndpoint endpoint; +endpoint.host = "127.0.0.1"; +endpoint.port = 9101; +endpoint.scheme = "tcp"; +const bool started = runtime.connect(endpoint); + +auto stats = runtime.stats(); +runtime.stop(); +``` + +### NodeStats fields + +| Field | Meaning | +|-------|---------| +| `peers_total` | Number of known peers | +| `peers_connected` | Number of connected peers | +| `handshakes_started` | Started handshakes | +| `handshakes_completed` | Completed handshakes | +| `connect_attempts` | Connection attempts | +| `connect_failures` | Failed connections | +| `backoff_skips` | Skipped due to backoff | + +## Minimal node + +```cpp +#include + +auto node = vix::p2p::make_tcp_node(cfg); +node->start(); +std::cout << "listening on port 9101\n"; +node->wait(); +``` + +## Discovery API + +```cpp +vix::p2p::DiscoveryConfig cfg; +cfg.self_node_id = "node-a"; +cfg.self_tcp_port = 9201; +cfg.discovery_port = 37020; +cfg.mode = vix::p2p::DiscoveryMode::Broadcast; +cfg.announce_interval_ms = 1000; +cfg.seen_ttl_ms = 15000; +cfg.connect_cooldown_ms = 4000; + +auto on_peer = [](const vix::p2p::DiscoveryAnnouncement &peer) +{ + std::cout << "discovered " << peer.node_id << " at " << peer.host << ":" << peer.port << "\n"; +}; + +auto discovery = vix::p2p::make_udp_discovery(cfg, on_peer); +discovery->start(); +const auto peers = discovery->snapshot(); +discovery->stop(); +``` + +### Discovery message + +```cpp +vix::p2p::msg::DiscoveryAnnounce announce; +announce.node_id = "node-a"; +announce.tcp_port = 9001; +announce.ts_ms = 1710000000000ULL; +announce.capabilities["proto"] = "1.0"; + +const std::string json = announce.to_json(); +const auto parsed = vix::p2p::msg::DiscoveryAnnounce::from_json(json); +``` + +## Bootstrap API + +```cpp +vix::p2p::BootstrapConfig cfg; +cfg.self_node_id = "bootstrap-client"; +cfg.self_tcp_port = 9300; +cfg.registry_url = "http://127.0.0.1:8080/peers"; +cfg.mode = vix::p2p::BootstrapMode::PullOnly; +cfg.poll_interval_ms = 2000; + +auto bootstrap = vix::p2p::make_http_bootstrap(cfg, on_peer); +bootstrap->start(); +const auto peers = bootstrap->snapshot(); +bootstrap->stop(); +``` + +Registry response shape: + +```json +{ "peers": [{ "host": "127.0.0.1", "tcp_port": 9301, "node_id": "node-x" }] } +``` + +## Router API + +```cpp +vix::p2p::MemoryRouter router; +router.upsert_route("node-b", vix::p2p::Route{"edge-1", false, 8}); // next_hop, via_relay, ttl +router.upsert_route("node-c", vix::p2p::Route{"relay-7", true, 4}); + +const auto route = router.resolve("node-b"); +if (route) + std::cout << "next_hop=" << route->next_hop << "\n"; + +router.remove_route("node-c"); +``` + +## Protocol API + +### Envelope + +```cpp +vix::p2p::msg::Ping ping; +ping.nonce = 42; + +vix::p2p::Envelope envelope = vix::p2p::pack::make_envelope( + vix::p2p::MessageType::Ping, ping); + +const std::vector bytes = envelope.encode(); +vix::p2p::Envelope decoded = vix::p2p::Envelope::decode_or_throw(bytes); + +std::cout << vix::p2p::to_string(envelope.type) << "\n"; +bool encrypted = vix::p2p::has_flag(envelope.flags, vix::p2p::EnvelopeFlag::Encrypted); +``` + +### Framing + +TCP is a byte stream — framing marks where each message begins and ends. + +```cpp +vix::p2p::framing::LengthPrefixVarint framer; + +vix::p2p::Frame frame = framer.encode(envelope_bytes); +vix::p2p::FrameDecodeResult result = framer.decode(frame.bytes); + +if (!result.frames.empty()) +{ + // result.frames.front().bytes — first complete frame + // result.remaining — leftover bytes for next read +} +``` + +### Dispatch + +```cpp +vix::p2p::msg::AnyMessage any = vix::p2p::msg::decode_payload_or_throw( + vix::p2p::MessageType::Ping, payload); + +if (std::holds_alternative(any)) +{ + const auto &ping = std::get(any); + std::cout << "nonce=" << ping.nonce << "\n"; +} +``` + +## Message types + +| Message | Purpose | +|---------|---------| +| `Hello` | First handshake message | +| `HelloAck` | Handshake response | +| `HelloFinish` | Final handshake message | +| `Ping` / `Pong` | Liveness check | +| `WalPush` | Push WAL bytes to a peer | +| `WalAck` | Acknowledge applied WAL sequence | +| `OutboxPull` | Ask peer for pending outbox items | + +### Handshake + +```cpp +// A → B → A +vix::p2p::msg::Hello hello; +hello.nonce_a = 1001; hello.node_id = "node-a"; +hello.capabilities["proto"] = "1.0"; + +vix::p2p::msg::HelloAck ack; +ack.nonce_a = 1001; ack.nonce_b = 2002; + +vix::p2p::msg::HelloFinish finish; +finish.nonce_a = 1001; finish.nonce_b = 2002; finish.signature = signature; + +// Encode/decode all messages the same way +const auto bytes = hello.encode(); +const auto decoded = vix::p2p::msg::Hello::decode_or_throw(bytes); +``` + +### WAL replication messages + +```cpp +// Push WAL records to peer +vix::p2p::msg::WalPush push; +push.seq_begin = 10; push.seq_end = 12; push.wal_bytes = wal_bytes; + +// Acknowledge what peer has applied +vix::p2p::msg::WalAck ack; +ack.last_applied_seq = 12; + +// Ask peer for pending operations +vix::p2p::msg::OutboxPull pull; +pull.target_node_id = "node-b"; pull.max_items = 64; +``` + +## Crypto API + +```cpp +// Secure envelope packing +vix::p2p::Envelope env = vix::p2p::pack::make_envelope_secure( + vix::p2p::MessageType::Ping, plaintext, session_key, crypto, 1); + +const auto aad = vix::p2p::pack::make_aad(env); +const auto decrypted = crypto.aead_decrypt(session_key, nonce, aad, env.payload, env.tag); + +// NullCrypto for testing +vix::p2p::NullCrypto crypto; +``` + +## P2P HTTP API + +```cpp +#include + +vix::p2p_http::P2PHttpOptions options; +options.prefix = "/p2p"; +options.enable_ping = true; +options.enable_status = true; +options.enable_peers = true; +options.enable_logs = true; +options.enable_live_logs = true; + +vix::App app; +vix::p2p_http::registerRoutes(app, runtime, options); +// Don't forget on shutdown: +vix::p2p_http::shutdown_live_logs(); +``` + +### HTTP control routes + +| Route | Purpose | +|-------|---------| +| `GET /p2p/ping` | Smoke test | +| `GET /p2p/status` | Runtime stats | +| `POST /p2p/connect` | Connect to a peer | +| `GET /p2p/peers` | Known peers | +| `GET /p2p/logs` | In-memory log buffer | +| `POST /p2p/admin/hook` | Admin extension hook | + +### Connect via HTTP + +```bash +curl -X POST http://127.0.0.1:8080/p2p/connect \ + -H "content-type: application/json" \ + -d '{"host":"127.0.0.1","port":9101,"scheme":"tcp"}' +``` + +### Auth hooks + +```cpp +// Preferred: middleware context +options.auth_ctx = [](vix::mw::Context &ctx) -> bool +{ + if (ctx.req().header("x-auth-token") == "secret") + return true; + + ctx.res().status(401).json(vix::json::obj({ + {"ok", false}, + {"error", "unauthorized"} + })); + + return false; +}; + +// Fallback: legacy hook +options.auth_legacy = [](vix::http::Request &req, vix::http::ResponseWrapper &res) -> bool +{ + if (req.header("x-auth-token") == "secret") + return true; + + res.status(401).json(vix::json::obj({ + {"ok", false}, + {"error", "unauthorized"} + })); + + return false; +}; +``` + +### Custom log sink + +```cpp +options.log_sink = [](std::string_view line) { + std::cout << "[p2p_http] " << line << "\n"; +}; + +vix::p2p_http::set_live_log_sink([](std::string line) { + std::cout << "[live] " << line << "\n"; +}); + +vix::p2p_http::shutdown_live_logs(); // always call on shutdown +``` + +## P2P CLI + +```bash +vix p2p --id A --listen 9001 +vix p2p --id B --listen 9002 --connect 127.0.0.1:9001 +vix p2p --id A --listen 9001 --discovery on --disc-port 37020 +vix p2p --id A --listen 9001 --bootstrap on --registry http://127.0.0.1:8080 +``` + +## How P2P fits with Sync + +```txt +local write → WAL → outbox → P2P message → peer → ack → convergence +``` + +`WalPush`, `WalAck`, `OutboxPull` are protocol messages designed for distributed replication. + +## Common mistakes + +### Reusing node id or listen port + +Each node must have a unique `node_id` and its own `listen_port`. + +### Connecting before peer is ready + +Start the target node first, then connect from the second node. + +### Forgetting runtime.start() + +```cpp +runtime.start(); // must call before runtime.connect() +runtime.connect(endpoint); +``` + +### Exposing control routes without auth + +Routes like `POST /p2p/connect`, `GET /p2p/peers`, `POST /p2p/admin/hook` must be protected in production. + +### Confusing WebSocket and P2P + +WebSocket → browser clients to a server. P2P → nodes to nodes. + +## Production notes + +- Use stable node ids +- Protect all P2P HTTP control routes +- Monitor `peers_connected`, handshake failures, `connect_failures` +- Treat P2P messages as untrusted input — validate every payload +- Keep discovery ports consistent across the fleet +- HTTP control routes can go through Nginx; P2P transport requires direct TCP reachability or a relay + +## What you should remember + +```cpp +// Runtime +vix::p2p::NodeConfig cfg; +cfg.node_id = "node-a"; +cfg.listen_port = 9101; +auto node = vix::p2p::make_tcp_node(cfg); +vix::p2p::P2PRuntime runtime(node); +runtime.start(); +runtime.connect(endpoint); +runtime.stop(); + +// Protocol flow +typed message → envelope → frame → transport → decode → dispatch + +// Sync flow +WAL → WalPush → WalAck → convergence +``` + +The core idea: P2P is the node-to-node layer that makes Vix suitable for distributed and offline-first systems. diff --git a/docs/api/websocket.md b/docs/api/websocket.md index 8590501..cd8e975 100644 --- a/docs/api/websocket.md +++ b/docs/api/websocket.md @@ -1,203 +1,331 @@ # WebSocket API -This page documents the WebSocket server API in Vix. +The WebSocket API lets a Vix application keep a real-time connection open between a client and the server. -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 +```txt +HTTP → request/response +WebSocket → bidirectional live connection ``` -Core type: +## Public headers -``` cpp -vix::websocket::Server +```cpp +#include +#include // for HTTP routes +#include // for combined HTTP + WebSocket ``` ------------------------------------------------------------------------- - -## Constructor +## Basic architecture -``` cpp -Server ws(cfg, executor); +```txt +Config → RuntimeExecutor → App + WebSocket Server → run_http_and_ws(...) ``` -Parameters: - -- `cfg` → configuration object -- `executor` → thread pool executor +## Minimal HTTP + WebSocket app ------------------------------------------------------------------------- - -# Minimal Standalone Example - -``` cpp -#include -#include +```cpp +#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)); + vix::config::Config cfg{".env"}; + auto executor = std::make_shared(1u); + vix::App app{executor}; + vix::websocket::Server ws{cfg, executor}; + + app.get("/health", [](vix::Request &, vix::Response &res){ + res.json({ + "ok", true, + "service", "websocket-api" + }); + }); - ws.on_open([](auto& session) { - (void)session; + ws.on_open([](vix::websocket::Session &) { + std::cout << "client connected\n"; }); - ws.on_typed_message([](auto& session, - const std::string& type, - const vix::json::kvs& payload) - { - (void)session; - (void)type; - (void)payload; + ws.on_close([](vix::websocket::Session &) { + std::cout << "client disconnected\n"; }); - ws.listen_blocking(); + ws.on_error([](vix::websocket::Session &, const std::string &e) { + std::cout << "error: " << e << "\n"; + }); + vix::run_http_and_ws(app, ws, executor, cfg); return 0; } ``` ------------------------------------------------------------------------- +## .env for WebSocket + +```dotenv +SERVER_PORT=8080 +SERVER_TLS_ENABLED=false +WEBSOCKET_MAX_MESSAGE_SIZE=65536 +WEBSOCKET_IDLE_TIMEOUT=60 +WEBSOCKET_ENABLE_DEFLATE=true +WEBSOCKET_PING_INTERVAL=30 +WEBSOCKET_AUTO_PING_PONG=true +``` -# Event Hooks +## WebSocket lifecycle -## on_open +```cpp +ws.on_open([&ws](vix::websocket::Session &session) { + /* client connected */ +}); -Called when a client connects. +ws.on_close([](vix::websocket::Session &session) { + /* client disconnected */ +}); -``` cpp -ws.on_open([](auto& session) { - // session is active +ws.on_error([](vix::websocket::Session &session, const std::string &error) { + /* error */ }); ``` ------------------------------------------------------------------------- - -## on_close +## Typed messages -Called when a client disconnects. +Client sends: -``` cpp -ws.on_close([](auto& session) { - // cleanup -}); +```json +{ "type": "chat.message", "payload": { "user": "Ada", "text": "Hello" } } ``` ------------------------------------------------------------------------- +Server handles: -## on_typed_message +```cpp +ws.on_typed_message( + [&ws](vix::websocket::Session &session, + const std::string &type, + const vix::json::kvs &payload) + { + if (type == "app.ping") + { + ws.broadcast_json( + "app.pong", {"status", "ok", "transport", "websocket"} + ); + return; + } + + if (type == "chat.message") + { + const std::string text = payload.get_string_or("text", ""); + if (text.empty()) + { + ws.broadcast_json( + "chat.error", {"error", "text is required"} + ); + return; + } + + ws.broadcast_json( + "chat.message", payload + ); + return; + } + + ws.broadcast_json( + "app.unknown", {"type", type, "message", "Unknown event type" + }); +}); +``` -Called when a typed message is received. +## Broadcast messages -Signature: +```cpp +ws.broadcast_json( + "system.connected", {"message", "A client connected"} +); -``` cpp -(session, type, payload) +ws.broadcast_json( + "chat.message", payload +); ``` -- `type` → string -- `payload` → `vix::json::kvs` +## Recommended event protocol + +| Event | Direction | Purpose | +|-------|-----------|---------| +| `app.ping` | client → server | Health check | +| `app.pong` | server → client | Health response | +| `chat.join` | client → server | User joins | +| `chat.leave` | client → server | User leaves | +| `chat.message` | both | Chat message | +| `chat.error` | server → client | Chat error | +| `system.connected` | server → client | Client connected | +| `system.disconnected` | server → client | Client disconnected | + +## Run HTTP and WebSocket together + +```cpp +// Full control +vix::run_http_and_ws(app, ws, executor, cfg); + +// Compact form +vix::serve_http_and_ws(".env", 8080, [](auto &app, auto &ws){ + app.get("/", [](auto &, auto &res) { res.json({{"framework", "Vix.cpp"}}); }); + 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 + ); + + else + ws.broadcast_json( + "app.unknown", {"type", type} + ); + }); +}); +``` ------------------------------------------------------------------------- +## WebSocket config fields -# Typed Message Protocol +| Variable | Purpose | Default | +|----------|---------|---------| +| `WEBSOCKET_MAX_MESSAGE_SIZE` | Max message size in bytes | 65536 | +| `WEBSOCKET_IDLE_TIMEOUT` | Idle timeout in seconds | 60 | +| `WEBSOCKET_ENABLE_DEFLATE` | Per-message deflate | true | +| `WEBSOCKET_PING_INTERVAL` | Ping interval in seconds | 30 | +| `WEBSOCKET_AUTO_PING_PONG` | Automatic ping/pong | true | -Expected message shape: +```cpp +vix::websocket::Config ws_cfg = vix::websocket::Config::from_core(core_cfg); +std::cout << ws_cfg.maxMessageSize << "\n"; +``` -``` json -{ - "type": "event.name", - "payload": { ... } +## WebSocket behind Nginx + +```nginx +location /ws/ { + proxy_pass http://127.0.0.1:8080; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_read_timeout 3600; + proxy_send_timeout 3600; } ``` -Vix automatically parses and routes this to `on_typed_message`. +Without upgrade headers, WebSocket connections fail through Nginx. ------------------------------------------------------------------------- +## WebSocket vs HTTP vs P2P -# Broadcast +| | HTTP | WebSocket | P2P | +|-|------|-----------|-----| +| Pattern | Request/response | Bidirectional | Node-to-node | +| Best for | CRUD APIs | Live UI updates | Distributed sync | +| Discovery | Manual | Manual | Auto (UDP) | -## Broadcast to all +## Complete chat example -``` cpp -ws.broadcast_json("chat.message", { - "user", "Alice", - "text", "Hello" -}); -``` +```cpp +static void register_ws_lifecycle(vix::websocket::Server &ws) +{ + ws.on_open([&ws](vix::websocket::Session &){ + ws.broadcast_json("system.connected", {"message", "A client connected"}); + }); ------------------------------------------------------------------------- + ws.on_close([&ws](vix::websocket::Session &){ + ws.broadcast_json("system.disconnected", {"message", "A client disconnected"}); + }); -## Broadcast to room + ws.on_error([](vix::websocket::Session &, const std::string &e){ + std::cout << "websocket error: " << e << "\n"; + }); +} -``` cpp -ws.broadcast_room_json("room1", "chat.message", payload); +static void register_ws_protocol(vix::websocket::Server &ws) +{ + ws.on_typed_message([&ws](vix::websocket::Session &, + const std::string &type, + const vix::json::kvs &payload) + { + if (type == "app.ping") { + ws.broadcast_json( + "app.pong", {"status", "ok"} + ); + return; + } + + if (type == "chat.join") { + ws.broadcast_json( + "chat.join", payload + ); + return; + } + + if (type == "chat.leave") { + ws.broadcast_json( + "chat.leave", payload + ); + return; + } + + if (type == "chat.message") + { + if (payload.get_string_or("text", "").empty()) + { + ws.broadcast_json( + "chat.error", {"error", "text is required"} + ); + return; + } + ws.broadcast_json( + "chat.message", payload + ); + return; + } + + ws.broadcast_json( + "app.unknown", {"type", type, "message", "Unknown event type"} + ); +}); +} ``` ------------------------------------------------------------------------- - -# Long Polling Bridge - -Header: +## Production checklist -``` cpp -#include -``` - -Attach bridge: +- Validate message type and payload fields +- Set message size limits +- Configure idle timeout and ping/pong +- Authenticate clients (token, cookie, or signed handshake) +- Never trust user-supplied identity from payload +- Avoid broadcasting private data +- Configure Nginx upgrade headers +- Use HTTPS/WSS publicly -``` cpp -ws.attach_long_polling_bridge(bridge); -``` +## Common mistakes -Purpose: +### Broadcasting unvalidated payloads -- Fallback when WebSocket is blocked -- HTTP endpoints simulate WebSocket behavior +Always validate before broadcasting. ------------------------------------------------------------------------- +### Using WebSocket for everything -# HTTP + WS Combined +Use HTTP for CRUD APIs. Use WebSocket for live events only. -Use helper: +### Forgetting /health -``` cpp -vix::serve_http_and_ws([](auto& app, auto& ws) { - // register HTTP routes - // register WebSocket handlers -}); -``` +Even real-time apps need HTTP health checks. -This creates a single runtime handling both protocols. +### Not handling unknown event types ------------------------------------------------------------------------- +Always have a fallback returning `app.unknown`. -# Notes +## What you should remember -- `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. +```cpp +vix::config::Config cfg{".env"}; +auto executor = std::make_shared(1u); +vix::App app{executor}; +vix::websocket::Server ws{cfg, executor}; +vix::run_http_and_ws(app, ws, executor, cfg); +``` -The WebSocket API is minimal by design. +HTTP handles requests. WebSocket handles live events. +Next: [Async API](/api/async) diff --git a/docs/book/01-introduction.md b/docs/book/01-introduction.md new file mode 100644 index 0000000..b5c0848 --- /dev/null +++ b/docs/book/01-introduction.md @@ -0,0 +1,203 @@ +# Introduction + +Welcome to the Vix book. + +This book teaches Vix step by step, like a story. +You will start with the simplest idea: + +```txt +Run C++ code quickly. +``` + +Then you will grow toward real backend systems: +- HTTP APIs, +- JSON, +- middleware, +- validation, +- database, +- WebSocket, +- async runtime, +- cache, +- offline-first sync, +- P2P, +- and production deployment. + +The goal is not only to show commands or APIs. The goal is to help you understand the mental model behind Vix. + +## What is Vix ? + +Vix is a modern C++ runtime for building fast and reliable applications. It gives C++ a more direct development experience: + +```bash +vix run main.cpp +``` + +or: + +```bash +vix new api +cd api +vix dev +``` + +Instead of forcing you to manually configure everything before writing your first line of useful code, Vix gives you a runtime-oriented workflow. You write the app. Vix handles the development loop. + +## The big idea + +C++ is powerful. +But building real applications in C++ often requires too much setup before the developer can even begin. You may need to think about CMake, compiler flags, linker flags, dependencies, build directories, runtime arguments, logs, tests, server startup, database flags, and project structure. + +Vix tries to make this smoother. The idea is simple: + +```txt +C++ should be able to feel direct without losing its power. +``` + +Vix does not remove C++. Vix gives C++ a better application runtime around it. + +## Why a runtime ? + +A language alone is not enough to build modern applications comfortably. + +JavaScript became widely used for backend development not only because of the language, but because Node.js gave developers a runtime, package workflow, and fast feedback loop. + +Python became popular for scripting and backend work because running a file is simple: + +```bash +python app.py +node server.js +``` + +Vix brings that kind of workflow to C++: + +```bash +vix run main.cpp +``` + +But with the performance, control, and explicitness of C++. + +## What Vix is not + +Vix is not a replacement for C++. +It is not a garbage-collected language. +It is not trying to hide how systems work or turn C++ into JavaScript or Python. + +Vix is a runtime and toolkit that makes C++ application development more practical. +It keeps the C++ philosophy — explicit, fast, deterministic, close to the system — but adds a smoother workflow. + +## A tiny example + +```cpp +#include +using namespace vix; + +int main() +{ + App app; + + app.get("/", [](Request &, Response &res){ + res.json({ + "message", "Hello from Vix", + "framework", "Vix.cpp" + }); + }); + + app.run(8080); + return 0; +} +``` + +Run it: + +```bash +vix run main.cpp +``` + +Then open `http://localhost:8080`. + +This small example already shows the core Vix style: +- `App`, +- route, +- `Request`, +- `Response`, +- JSON, +- runtime. + +## The development experience + +```bash +vix new api # Create a project +vix dev # Run in development mode +vix build # Build it +vix check # Validate it +vix tests # Run tests +vix fmt # Format code +``` + +## The structure of this book + +**Part 1: Understand Vix** +Introduction, Why Vix, Mental model + +**Part 2: Start building** +Installation, Run your first file, Create your first project, First HTTP server + +**Part 3: Build APIs** +Routes, Request and Response, JSON API + +**Part 4: Add professional layers** +Middleware, Validation, Errors and logging + +**Part 5: Connect real systems** +Database, Real-time WebSocket, Async runtime + +**Part 6: Advanced runtime features** +Cache, Offline-first sync, P2P + +**Part 7: Production** +Production deployment, Next steps + +## How to read this book + +Read it in order the first time. +Each chapter builds on the previous one. + +You only need basic C++ knowledge: +- functions, +- classes, +- headers, +- `std::string`, +- `std::vector`, +- lambdas, +- and basic CMake awareness. + +## The main mental shift + +Traditional C++ development often starts with the build system. +Vix starts with the application. + +Instead of thinking first about how to configure, link, and build, Vix wants the first question to be: + +```txt +What do I want to build? +``` + +## Vix and production + +A Vix app can run as a normal Linux service: + +```txt +browser → Nginx → Vix app → systemd +``` + +The production chapter will show how to deploy with a release build, systemd, Nginx, TLS, logs, and health checks. + +## What you should remember + +Vix is a modern C++ runtime for building fast and reliable applications. It gives C++ a direct run workflow, a project workflow, an HTTP application model, JSON APIs, middleware, validation, database access, WebSocket support, runtime-oriented tools, and a production deployment path. + +The core idea: keep the power of C++, make the application workflow simpler. + +## Next chapter + +[Next: Why Vix](/book/02-why-vix) diff --git a/docs/book/02-why-vix.md b/docs/book/02-why-vix.md new file mode 100644 index 0000000..df4182b --- /dev/null +++ b/docs/book/02-why-vix.md @@ -0,0 +1,175 @@ +# Why Vix ? + +Vix exists because building real applications in C++ should be faster, clearer, and more practical. + +C++ gives you: performance, control, deterministic lifetimes, low-level access, strong types, native binaries, and systems-level power. +But for many developers, the experience of building applications in C++ feels heavier than it should. + +## The problem + +To build a simple backend, you may need to think about compiler setup, CMake configuration, dependency installation, include paths, linker flags, build directories, runtime arguments, server lifecycle, logging, JSON, HTTP routing, database configuration, tests, and deployment. + +Each piece is manageable alone. The problem is that you must wire everything together before the application becomes useful. + +## The first step should be simple + +In many ecosystems, the first step is direct: + +```bash +python app.py +node server.js +deno run server.ts +``` + +Vix gives C++ a similar starting point: + +```bash +vix run main.cpp +``` + +The goal is not to copy these ecosystems. +The goal is to make C++ feel more direct for application development. + +## C++ does not lack power + +The problem is not the language itself. +The problem is the missing application workflow around the language. +C++ already has great compilers, excellent performance, RAII, templates, zero-cost abstractions, native concurrency, mature tooling, and portable binaries. +What developers often miss is a unified runtime experience. + +## The missing layer + +When you build a backend in C++, you usually combine many pieces: +- HTTP library, +- JSON library, +- build system, +- logging library, +- database layer, +- middleware logic, +- validation layer, +- WebSocket library, +- and deployment scripts. + +This creates friction. +Vix answers these questions with one coherent workflow. + +## The Vix approach + +```bash +vix run main.cpp # Run a file +vix new api && vix dev # Create and run a project +vix build # Build +vix check && vix tests # Validate and test +``` + +## Why not just use CMake ? + +Vix does not replace CMake. +CMake answers: "How do I configure and build this C++ project ?" + +Vix answers: "How do I build, run, test, develop, package, and operate this C++ application ?" + +Vix can use CMake underneath while giving the developer a simpler top-level experience. + +## Why not just use a C++ web framework ? + +A web framework usually focuses on HTTP. +Vix is broader. + +Vix also includes the surrounding application workflow: +- CLI, +- project creation, +- direct file execution, +- build commands, +- dependency workflow, +- tests, +- formatting, +- logging, +- database access, +- WebSocket runtime, +- middleware, +- validation, +- cache, +- sync, +- P2P, +- and deployment path. + +## The application comes first + +Traditional C++ project setup often begins with build configuration. Vix wants the first step to be: write the application, run it. + +```cpp +#include +using namespace vix; + +int main() +{ + App app; + + app.get("/", [](Request &, Response &res){ + res.text("Hello Vix"); + }); + + app.run(8080); + return 0; +} +``` + +```bash +vix run main.cpp +``` + +## Vix is explicit + +A route is explicit: + +```cpp +app.get("/users/{id}", [](Request &req, Response &res){ + const std::string id = req.param("id"); + res.json({"id", id}); +}); +``` + +A database query is explicit: + +```cpp +auto stmt = conn->prepare("SELECT id, name FROM users WHERE id = ?"); +stmt->bind(1, id); +auto rows = stmt->query(); +``` + +This matters because serious systems must be understandable. Vix should reduce friction without hiding behavior. + +## Why reliability matters + +Vix is also connected to a bigger idea: +applications should keep working under real-world conditions. + +Real systems face network failure, process restart, slow servers, partial writes, timeouts, offline clients, retries, duplicate requests, and lost connections. + +This is why Vix includes deeper runtime ideas like cache, offline-first sync, WAL, outbox, retry, and P2P. + +## What you should remember + +Vix exists because C++ deserves a smoother application runtime. +The core problem is not that C++ is weak. +The problem is that building applications in C++ often starts with too much friction. + +Vix provides: +- direct execution, +- project workflow, +- HTTP app model, +- JSON support, +- middleware, +- validation, +- database access, +- WebSocket runtime, +- production deployment path, +- and advanced reliability modules. + +The core idea: +keep C++ powerful, make building applications with it feel direct. + +## Next chapter + +[Next: Mental model](/book/03-mental-model) diff --git a/docs/book/03-mental-model.md b/docs/book/03-mental-model.md new file mode 100644 index 0000000..879809a --- /dev/null +++ b/docs/book/03-mental-model.md @@ -0,0 +1,249 @@ +# Mental model + +This chapter explains how to think about Vix. + +Before learning every command and module, you need one clear picture: + +```txt +Vix is a runtime workflow for C++ applications. +``` + +It connects four layers: + +```txt +CLI + ↓ +Runtime + ↓ +Application + ↓ +Modules +``` + +The CLI helps you work. +The runtime runs your code. +The application layer exposes APIs such as `App`, `Request`, and `Response`. +The modules add capabilities like JSON, database, middleware, validation, WebSocket, cache, sync, and P2P. + +## The simplest mental model + +```cpp +#include +using namespace vix; + +int main() +{ + App app; + + app.get("/", [](Request &, Response &res){ + res.text("Hello Vix"); + }); + + app.run(8080); + return 0; +} +``` + +```bash +vix run main.cpp +``` + +This example contains most of the Vix mental model: +- `vix run` → CLI workflow, +- `App` → application object, +- `app.get` → route registration, +- `Request` → incoming request, +- `Response` → outgoing response, +- `app.run` → runtime starts the server. + +## Layer 1: The CLI + +The CLI is the developer entrypoint. +It provides commands such as: +`vix run`, +`vix new`, +`vix dev`, +`vix build`, +`vix check`, +`vix tests`, +`vix fmt`, +`vix doctor`. + +It gives a consistent development loop: create → run → edit → reload → check → test → build → deploy. + +| Command | Purpose | +|-------------|----------------------------------------------| +| `vix run` | Builds and runs a file, project, or manifest.| +| `vix dev` | Starts a development loop with watch reload. | +| `vix build` | Configures, compiles, and links the project. | +| `vix check` | Validates builds, tests, and sanitizers. | + +## Layer 2: The runtime + +The runtime is what makes the app actually run. In a simple HTTP program: + +```cpp +app.run(8080); +``` + +For advanced apps with HTTP + WebSocket together: + +```cpp +struct Runtime +{ + vix::config::Config config{".env"}; + + std::shared_ptr executor{ + std::make_shared(1u) + }; + + vix::App app{executor}; + vix::websocket::Server ws{config, executor}; +}; + +vix::run_http_and_ws(runtime.app, runtime.ws, runtime.executor, http_port); +``` + +## Layer 3: The application + +```cpp +App app; + +app.get("/users/{id}", [](Request &req, Response &res){ + const std::string id = req.param("id"); + res.json({"id", id}); +}); +``` + +**Request** is read-only input from the client: +- path params, +- query params, +- headers, +- body, +- JSON body. + +**Response** is how the route sends output back: +- text, +- JSON, +- files, +- status codes, +- headers. + +### Keep `main()` small + +```cpp +int main() +{ + App app; + + app.get("/", [](Request &, Response &res){ + res.json({"message", "Hello"}); + }); + + app.run(8080); + return 0; +} +``` + +## Layer 4: Modules + +```cpp +#include // core +#include // database +#include // WebSocket +#include // middleware +#include // validation +``` + +Use the smallest public module header that gives the feature you need. + +### Key modules + +**Middleware** runs around routes — CORS, rate limiting, authentication, security headers, static files. + +**Validation** checks input before business logic: + +```cpp +auto result = vix::validation::validate("email", email).required().email().result(); +``` + +**Database** gives explicit access: + +```cpp +auto db = vix::db::Database::sqlite("vix.db"); +vix::db::PooledConn conn(db.pool()); +auto stmt = conn->prepare("SELECT id, name FROM users WHERE id = ?"); +stmt->bind(1, id); +``` + +**WebSocket** adds real-time events: + +```cpp +ws.on_typed_message([&ws](auto &, const std::string &type, const auto &payload){ + if (type == "chat.message") ws.broadcast_json("chat.message", payload); +}); +``` + +## Configuration + +```cpp +vix::config::Config cfg{".env"}; +const int port = cfg.getServerPort(); +vix::db::Database db{cfg}; +``` + +Environment variables: +```dotenv +SERVER_PORT=8080 +DATABASE_ENGINE=sqlite +DATABASE_DEFAULT_NAME=vix.db +``` + +## Request lifecycle + +```txt +client request → Nginx (production) → Vix runtime → middleware → route handler → Response +``` + +With database: request → middleware → route → validate → query database → return JSON + +## Error flow + +```txt +validation failure → 400 Bad Request +missing token → 401 Unauthorized +not allowed → 403 Forbidden +missing resource → 404 Not Found +too many requests → 429 Too Many Requests +``` + +## Application growth + +```txt +Start: src/main.cpp + +Later: src/ + ├── main.cpp + ├── routes/ + ├── validation/ + ├── database/ + └── services/ +``` + +Start small. Move code into modules when it earns it. Keep `main()` as wiring. + +## Production shape + +```txt +browser → Nginx → Vix app on localhost → systemd +``` + +## What you should remember + +The Vix mental model has four layers: **CLI** controls the workflow, **Runtime** executes the app, **Application** is built around `App`, `Request`, and `Response`, **Modules** add capabilities. + +The best way to grow a Vix app: start with one file, keep `main()` small, register routes through functions, add modules when needed, move logic into services as the app grows. + +## Next chapter + +[Next: Installation](/book/04-installation) diff --git a/docs/book/04-installation.md b/docs/book/04-installation.md new file mode 100644 index 0000000..8a00b48 --- /dev/null +++ b/docs/book/04-installation.md @@ -0,0 +1,189 @@ +# Installation + +Now you will install Vix. + +## Install + +**Linux and macOS:** + +```bash +curl -fsSL https://vixcpp.com/install.sh | bash +``` + +**Windows PowerShell:** + +```powershell +irm https://vixcpp.com/install.ps1 | iex +``` + +## SDK mode vs CLI-only mode + +| Mode | What it installs | Use when | +|----------------------|----------------------------------|-------------------------------------------| +| SDK mode, default | CLI, headers, and libraries. | You want to compile Vix applications. | +| CLI-only mode | The `vix` binary only. | You only need the CLI without compiling. | + +For this book, install the full SDK (default). CLI-only mode cannot compile projects that use `#include `. + +```bash +# CLI-only (not recommended for this book) +VIX_INSTALL_KIND=cli curl -fsSL https://vixcpp.com/install.sh | bash +``` + +## Verify the CLI + +```bash +vix --version +``` + +Expected output shape: + +```txt +Vix.cpp CLI +version : 2.5.2 +author : Gaspard Kirira +source : https://github.com/vixcpp/vix +``` + +## Verify the SDK headers + +```bash +find ~/.local/include -name vix.hpp 2>/dev/null +# Expected: ~/.local/include/vix.hpp +``` + +## PATH check + +If `vix: command not found`: + +```bash +# Bash +echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc +source ~/.bashrc + +# Zsh +echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc +source ~/.zshrc +``` + +## Build prerequisites + +### Ubuntu / Debian + +```bash +sudo apt update +sudo apt install -y \ + build-essential cmake ninja-build pkg-config \ + libssl-dev libsqlite3-dev zlib1g-dev libbrotli-dev \ + nlohmann-json3-dev libspdlog-dev libfmt-dev +``` + +### macOS (Homebrew) + +```bash +brew install cmake ninja pkg-config openssl@3 spdlog fmt nlohmann-json brotli +``` + +### Windows + +Use Visual Studio Build Tools with MSVC or clang-cl. For extra dependencies, use vcpkg. + +## Create your first project + +```bash +vix new app +cd app +vix dev +``` + +## Run a single file + +```cpp +#include + +using namespace vix; + +int main() +{ + App app; + + app.get("/", [](Request &, Response &res){ + res.json({"message", "Hello from Vix"}); + }); + + app.run(8080); + return 0; +} +``` + +```bash +vix run main.cpp +curl -i http://127.0.0.1:8080/ +``` + +## Common issues + +### `vix: command not found` + +Fix `PATH` — add `~/.local/bin` and restart your terminal. + +### `#include ` not found + +```bash +find ~/.local/include -name vix.hpp 2>/dev/null +``` + +If nothing appears, reinstall the full SDK. Do not use CLI-only mode. + +### Compiler or CMake missing + +```bash +c++ --version +cmake --version +ninja --version +``` + +Install missing tools with `apt install build-essential cmake ninja-build`. + +## Build from source + +```bash +git clone --recursive https://github.com/vixcpp/vix.git +cd vix + +cmake -S . -B build -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DVIX_ENABLE_INSTALL=ON + +cmake --build build -j +cmake --install build +``` + +## Upgrade later + +```bash +vix upgrade # upgrade the CLI +vix doctor # check environment +vix info # inspect paths and caches +``` + +## What you should remember + +For this book, install the full SDK: + +```bash +curl -fsSL https://vixcpp.com/install.sh | bash +``` + +Then verify: + +```bash +vix --version +find ~/.local/include -name vix.hpp 2>/dev/null +``` + +CLI-only is for the binary. SDK mode is for building Vix applications. + +## Next chapter + +[Next: Run your first file](/book/05-run-your-first-file) diff --git a/docs/book/05-run-your-first-file.md b/docs/book/05-run-your-first-file.md new file mode 100644 index 0000000..cf14530 --- /dev/null +++ b/docs/book/05-run-your-first-file.md @@ -0,0 +1,243 @@ +# Run your first file + +Now you will run your first C++ file. + +```bash +vix run main.cpp +``` + +## The goal + +Understand: +- how to run a single file, +- how script mode works, +- how to pass runtime arguments, +- how to pass compiler flags, +- and how to avoid common mistakes. + +## Create a workspace + +```bash +mkdir -p ~/tmp/vix-first-file +cd ~/tmp/vix-first-file +touch main.cpp +``` + +## First C++ program + +```cpp +#include + +int main() +{ + std::cout << "Hello from Vix\n"; + return 0; +} +``` + +```bash +vix run main.cpp +# Output: Hello from Vix +``` + +Vix detects a single `.cpp` file → uses **script mode** → compiles → runs. No CMake, no build directories needed. + +## Add Vix headers + +```cpp +#include + +using namespace vix; + +int main() +{ + App app; + + app.get("/", [](Request &, Response &res){ + res.text("Hello Vix"); + }); + + app.run(8080); + return 0; +} +``` + +```bash +vix run main.cpp +curl -i http://127.0.0.1:8080/ +``` + +Stop with `Ctrl+C`. + +> If you see `error: header file not found` for `vix.hpp`, reinstall the full SDK: `curl -fsSL https://vixcpp.com/install.sh | bash` + +## Add a JSON route + +```cpp +#include +using namespace vix; + +int main() +{ + App app; + + app.get("/", [](Request &, Response &res){ + res.json({ + "message", "Hello from Vix", + "framework", "Vix.cpp" + }); + }); + + app.get("/health", [](Request &, Response &res){ + res.json({"ok", true}); + }); + + app.run(8080); + return 0; +} +``` + +## Add route params + +```cpp +app.get("/hello/{name}", [](Request &req, Response &res){ + const std::string name = req.param("name"); + + res.json({ + "greeting", "Hello " + name, + "powered_by", "Vix.cpp" + }); +}); +``` + +```bash +curl -i http://127.0.0.1:8080/hello/Gaspard +``` + +## Add query params + +```cpp +app.get("/users/{id}", [](Request &req, Response &res){ + const std::string id = req.param("id"); + const std::string page = req.query_value("page", "1"); + + res.json({ + "id", id, + "page", page + }); +}); +``` + +```bash +curl -i "http://127.0.0.1:8080/users/42?page=2" +``` + +## Passing runtime arguments + +Use `--run` for runtime arguments: + +```bash +vix run main.cpp --run --port 8080 +``` + +> **Do NOT use `--` for runtime args.** In script mode, `--` forwards to the compiler/linker. + +```bash +# Wrong — sends --port to the compiler +vix run main.cpp -- --port 8080 + +# Correct — sends --port to your program +vix run main.cpp --run --port 8080 +``` + +## Passing compiler flags + +Use `--` for compiler or linker flags: + +```bash +vix run main.cpp -- -O2 -DNDEBUG +vix run main.cpp -- -lssl -lcrypto + +# Combine both +vix run main.cpp -- -O2 -DNDEBUG --run hello 123 +``` + +## Sanitizers + +```bash +vix run main.cpp --san # AddressSanitizer + UBSan +vix run main.cpp --ubsan # UBSan only +``` + +## SQLite / MySQL support + +```bash +vix run main.cpp --with-sqlite +vix run main.cpp --with-mysql +``` + +## Other script mode options + +```bash +vix run main.cpp --auto-deps # include local Vix dependencies +vix run main.cpp --local-cache # use local script cache +vix run main.cpp --no-clear # preserve terminal output +vix run main.cpp --verbose # debug logs +vix run main.cpp --quiet # minimal output +vix run main.cpp --watch # rebuild on file changes +vix run main.cpp --force-server # treat as long-running server +vix run main.cpp --force-script # treat as short-lived tool +``` + +## When to move from one file to a project + +A single file is perfect for learning. +Move to a project when you need multiple source files, headers, tests, dependencies, configuration, or a stable app structure: + +```bash +vix new api +cd api +vix dev +``` + +## Common mistakes + +### Using CLI-only install + +`#include ` requires the full SDK. + +### Passing runtime args after `--` + +```bash +# Wrong +vix run main.cpp -- --port 8080 + +# Correct +vix run main.cpp --run --port 8080 +``` + +### Port already in use + +```bash +sudo lsof -i :8080 +``` + +### Running from the wrong directory + +Relative paths (like `res.file("public/index.html")`) depend on where you run `vix`. + +## What you should remember + +The most important command: `vix run main.cpp` + +- `--run` → runtime arguments +- `--` → compiler or linker flags +- `--watch` or `vix dev main.cpp` → faster development loop + +The core idea: +- start with one file, +- move to a project when the app grows. + +## Next chapter + +[Next: Create your first project](/book/06-create-your-first-project) diff --git a/docs/book/06-create-your-first-project.md b/docs/book/06-create-your-first-project.md new file mode 100644 index 0000000..1c890a6 --- /dev/null +++ b/docs/book/06-create-your-first-project.md @@ -0,0 +1,283 @@ +# Create your first project + +In the previous chapter, you ran a single C++ file. Now you will create a real Vix project. + +A project gives you: source files, tests, CMake configuration, Vix manifest, tasks, environment files, development workflow, and release workflow. + +```bash +vix new app +``` + +## The goal + +Understand: how to create a Vix project, what files Vix generates, how to run it, how to use tasks, how `.env` works, and how `vix.json` works. + +## Create a project + +```bash +vix new app +cd app +``` + +For your first project, choose the **Application** template. + +## Generated files + +```txt +app/ +├── app.vix +├── CMakeLists.txt +├── CMakePresets.json +├── README.md +├── .env +├── .env.example +├── src/ +│ └── main.cpp +├── tests/ +│ └── test_basic.cpp +└── vix.json +``` + +| File/Folder | Purpose | +|-------------|---------| +| `src/` | Application source code | +| `tests/` | Project tests | +| `.env` | Local runtime configuration | +| `app.vix` | Vix project manifest for running | +| `vix.json` | Package and task manifest | +| `CMakeLists.txt` | C++ build configuration | +| `CMakePresets.json` | Build presets | + +## `src/main.cpp` + +The generated entrypoint: + +```cpp +#include +using namespace vix; + +int main() +{ + App app; + + app.get("/", [](Request &, Response &res){ + res.send("Hello world"); + }); + + app.run(8080); +} +``` + +## Run the project + +```bash +vix run # build and run +vix dev # development mode (hot reload) +vix build # compile only +vix tests # run tests +vix check --tests # validate project +vix fmt # format source files +``` + +## Project tasks + +```bash +vix task dev +vix task test +vix task check +vix task ci +vix task release +``` + +## `vix.json` + +```json +{ + "name": "app", + "deps": [], + "vars": { + "preset": "dev-ninja", + "release_preset": "release", + "log_level": "info" + }, + "tasks": { + "fmt": "vix fmt", + "dev": { + "description": "Start dev mode", + "command": "vix dev" + }, + "test": { + "description": "Run tests", + "command": "vix tests --preset ${preset} --fail-fast" + }, + "check": { + "description": "Validate project", + "command": "vix check --preset ${preset} --tests" + } + } +} +``` + +## Dependencies + +```bash +vix add namespace/package # add a dependency +vix install # install from vix.lock +``` + +`vix.json` → declared dependencies, +`vix.lock` → exact resolved versions, +`.vix/deps` → installed packages. + +## `app.vix` + +```toml +.vix +version = 1 + +[app] +kind = "project" +dir = "." +name = "app" +entry = "src/main.cpp" + +[build] +preset = "dev-ninja" +run_preset = "run-dev-ninja" +``` + +run it: + +```bash +vix dev api.vix +# or +vix run api.vix +``` + +## `.env` + +```dotenv +SERVER_PORT=8080 +DATABASE_ENGINE=mysql +DATABASE_DEFAULT_HOST=127.0.0.1 +DATABASE_DEFAULT_PORT=3306 +DATABASE_DEFAULT_USER=root +DATABASE_DEFAULT_PASSWORD= +DATABASE_DEFAULT_NAME=appdb +``` + +## Using config in code + +```cpp +#include +using namespace vix; + +int main() +{ + config::Config cfg{".env"}; + + App app; + + app.get("/", [](Request &, Response &res){ + res.send("Hello world"); + }); + + app.run(cfg.getServerPort()); +} +``` + +## Recommended first edit + +```cpp +#include +using namespace vix; + +int main() +{ + config::Config cfg{".env"}; + App app; + + app.get("/", [](Request &, Response &res){ + res.json({ + "message", "Hello from your first Vix project", + "framework", "Vix.cpp" + }); + }); + + app.get("/health", [](Request &, Response &res){ + res.json({ + "ok", true, + "service", "first-vix-project" + }); + }); + + app.run(cfg.getServerPort()); + + return 0; +} +``` + +```bash +vix dev +curl -i http://127.0.0.1:8080/health +``` + +## Daily workflow + +```bash +# Development +vix dev + +# Before committing +vix task fmt +vix task check +vix task test +# or +vix task ci +``` + +## Common mistakes + +### Running from the wrong folder + +```bash +# Wrong +vix dev # from outside the project + +# Correct +cd app +vix dev +``` + +### Forgetting `.env` + +```bash +cp .env.example .env +``` + +### Adding source files without updating CMake + +If you create `src/routes/PublicRoutes.cpp`, add it to `CMakeLists.txt`: + +```cmake +add_executable(app + src/main.cpp + src/routes/PublicRoutes.cpp +) +``` + +## What you should remember + +A Vix project gives you a real structure with +`src/`, +`tests/`, +`.env`, +`app.vix`, +`vix.json`, +`CMakeLists.txt`. + +The core idea: a Vix project is the point where quick experiments become a real application. + +## Next chapter + +[Next: First HTTP server](/book/07-first-http-server) diff --git a/docs/book/07-first-http-server.md b/docs/book/07-first-http-server.md new file mode 100644 index 0000000..21b7f0e --- /dev/null +++ b/docs/book/07-first-http-server.md @@ -0,0 +1,302 @@ +# First HTTP server + +Now you will build your first HTTP server and understand the core Vix application model: +`App`, +`Request`, +`Response`, +`routes`, +and `server startup`. + +## The smallest HTTP server + +```cpp +#include +using namespace vix; + +int main() +{ + App app; + + app.get("/", [](Request &, Response &res){ + res.send("Hello world"); + }); + + app.run(8080); + return 0; +} +``` + +```bash +vix dev +curl -i http://127.0.0.1:8080/ +``` + +## Core concepts + +| Part | Purpose | +|------|---------| +| `#include ` | Import Vix | +| `using namespace vix;` | Use `App`, `Request`, `Response` without prefix | +| `App app;` | Create the application | +| `app.get("/", handler)` | Register a GET route | +| `app.run(8080)` | Start the server | + +**Request** — reads what the client sent: path params, query params, headers, body, JSON body. + +**Response** — sends what your app returns: text, JSON, files, status codes, headers. + +## Response methods + +```cpp +res.text("Hello Vix"); // plain text +res.json({"message", "Hello"}); // JSON +res.status(201).json({"ok", true}); // with status code +res.header("Cache-Control", "no-cache"); // set header +res.file("public/index.html"); // serve file +``` + +## Path parameters + +```cpp +app.get("/hello/{name}", [](Request &req, Response &res){ + const std::string name = req.param("name"); + res.json({ + "greeting", "Hello " + name, + "powered_by", "Vix.cpp" + }); +}); +``` + +```bash +curl -i http://127.0.0.1:8080/hello/Gaspard +``` + +## Multiple path parameters + +```cpp +app.get("/posts/{year}/{slug}", [](Request &req, Response &res){ + const std::string year = req.param("year"); + const std::string slug = req.param("slug"); + res.json({ + "year", year, + "slug", slug, + "title", "Post: " + slug + }); +}); +``` + +## Query parameters + +```cpp +app.get("/users/{id}", [](Request &req, Response &res){ + const std::string id = req.param("id"); + const std::string page = req.query_value("page", "1"); + const std::string limit = req.query_value("limit", "20"); + + res.json({ + "id", id, + "page", page, + "limit", limit}); +}); +``` + +```bash +curl -i "http://127.0.0.1:8080/users/42?page=2&limit=10" +``` + +## Status codes + +```cpp +app.get("/not-found", [](Request &, Response &res){ + res.status(404).json({ + "ok", false, + "error", "not found" + }); +}); +``` + +| Status | Meaning | +|--------|-----------------------| +| 200 | OK | +| 201 | Created | +| 400 | Bad Request | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not Found | +| 429 | Too Many Requests | +| 500 | Internal Server Error | + +## Keep routes organized + +```cpp +int main() +{ + App app; + + app.get("/", [](Request &, Response &res){ + res.json({"message", "Hello from Vix"}); + }); + + app.get("/health", [](Request &, Response &res){ + res.json({"ok", true}); + }); + + app.run(8080); + return 0; +} +``` + +## Run with config + +```cpp +int main() +{ + config::Config cfg{".env"}; + App app; + + app.get("/", [](Request &, Response &res){ + res.json({"message", "Hello from Vix"}); + }); + + app.get("/health", [](Request &, Response &res){ + res.json({"ok", true}); + }); + + app.run(cfg.getServerPort()); // port from .env + return 0; +} +``` + +## Alternative: `app.run(cfg)` + +```cpp +app.run(cfg); // reads server config directly from Config (port, TLS, etc.) +``` + +## Listen in the background + +```cpp +app.listen(8080, [](){ + console.log("Server ready"); +}); + +app.wait(); +``` + +## Dynamic port + +```cpp +app.listen_port(0, [](int port){ + console.log("Listening on http://localhost:", port); +}); + +app.wait(); +``` + +## Complete example + +```cpp +#include +using namespace vix; + +int main() +{ + App app; + + app.get("/", [](Request &, Response &res){ + res.json({ + "message", "Hello from your first Vix HTTP server", + "framework", "Vix.cpp" + }); + }); + + app.get("/health", [](Request &, Response &res){ + res.json({ + "ok", true, "service", + "first-http-server" + }); + }); + + app.get("/hello/{name}", [](Request &req, Response &res){ + res.json({ + "greeting", "Hello " + req.param("name"), + "powered_by", "Vix.cpp" + }); + }); + + app.get("/users/{id}", [](Request &req, Response &res){ + res.json({ + "id", req.param("id"), + "page", req.query_value("page", "1"), + "limit", req.query_value("limit", "20") + }); + }); + + app.get("/not-found", [](Request &, Response &res){ + res.status(404).json({ + "ok", false, + "error", "not found" + }); + }); + + app.run(8080); + + return 0; +} +``` + +```bash +vix dev +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/hello/Gaspard +curl -i "http://127.0.0.1:8080/users/42?page=2&limit=10" +``` + +## Common mistakes + +### Forgetting `app.run(...)` + +Routes won't start without it. + +### Forgetting to return after an error + +```cpp +// Wrong +if (bad) { + res.status(400).json({"error", "bad request"}); +} + +res.json({"ok", true}); + +// Correct +if (bad) { + res.status(400).json({"error", "bad request"}); + return; +} + +res.json({"ok", true}); +``` + +### Confusing path params and query params + +```cpp +// Path param: /users/{id} +req.param("id") + +// Query param: /users/42?page=2 +req.query_value("page", "1") +``` + +## What you should remember + +```txt +App registers routes. +Request reads input. +Response sends output. +app.run starts the server. +``` + +## Next chapter + +[Next: Routes](/book/08-routes) diff --git a/docs/book/08-routes.md b/docs/book/08-routes.md new file mode 100644 index 0000000..2b2241a --- /dev/null +++ b/docs/book/08-routes.md @@ -0,0 +1,348 @@ +# Routes + +Routes are the heart of a Vix HTTP application. +They connect an HTTP request to C++ code: + +```txt +GET /users/42 → app.get("/users/{id}", handler); +``` + +## Route anatomy + +```cpp +app.get("/", [](Request &req, Response &res){ + res.text("Hello"); +}); +``` + +| Part | Purpose | +|------|---------| +| `app.get` | HTTP method | +| `"/"` | Path pattern | +| Lambda | Handler | +| `Request &req` | Incoming request | +| `Response &res` | Outgoing response | + +## HTTP methods + +```cpp +app.get("/users", list_handler); // read +app.post("/users", create_handler); // create +app.put("/users/{id}", replace_handler); // replace +app.patch("/users/{id}", update_handler); // partial update +app.del("/users/{id}", delete_handler); // delete +``` + +## Path parameters + +```cpp +app.get("/users/{id}", [](Request &req, Response &res){ + const std::string id = req.param("id"); + res.json({"id", id}); +}); +``` + +Matches: `/users/1`, `/users/42`, `/users/abc` + +### Multiple path parameters + +```cpp +app.get("/posts/{year}/{slug}", [](Request &req, Response &res){ + res.json({"year", req.param("year"), "slug", req.param("slug")}); +}); +``` + +## Query parameters + +Query params are NOT part of the route pattern — they come after `?`: + +```cpp +app.get("/users", [](Request &req, Response &res){ + const std::string page = req.query_value("page", "1"); + const std::string limit = req.query_value("limit", "20"); + res.json({"page", page, "limit", limit}); +}); +``` + +```bash +curl -i "http://127.0.0.1:8080/users?page=2&limit=10" +``` + +## Path params vs query params + +```txt +Path params → identify the resource: /users/42 +Query params → modify how it's read: /users?page=2&limit=10 +``` + +## Error routes + +```cpp +app.get("/users/{id}", [](Request &req, Response &res){ + const std::string id = req.param("id"); + + if (id == "0"){ + es.status(404).json({"ok", false, "error", "user not found"}); + return; // always return after error + } + + res.json({"ok", true, "id", id}); +}); +``` + +## Organizing routes by feature + +```cpp +static void public_routes(App &app) { /* / and /health */ } +static void user_routes(App &app) { /* /users */ } +static void auth_routes(App &app) { /* /auth/login */ } +static void admin_routes(App &app) { /* /admin */ } + +int main() +{ + App app; + + public_routes(app); + user_routes(app); + auth_routes(app); + admin_routes(app); + + app.run(8080); + + return 0; +} +``` + +## Route order matters + +Routes are matched in registration order. + +Specific routes should be registered before generic fallback routes. + +```cpp +// Correct +app.get("/users/search", search_handler); +app.get("/users/{id}", user_handler); +app.get("/*", fallback_handler); + +// Wrong — wildcard catches everything +app.get("/*", fallback_handler); +app.get("/users/{id}", user_handler); + +// In the wrong version, the wildcard route can catch requests before /users/{id} gets a chance to run. +``` + +## Wildcard routes + +A wildcard route matches many paths and is useful as a fallback. + +```cpp +app.get("/*", [](Request &req, Response &res){ + res.json({"path", req.path()}); +}); +``` + +Used for: + - static file fallback + - SPA fallback + - custom 404 behavior + +Used for: SPA fallback, static file fallback, custom 404. + +## Static file fallback + +```cpp +#include +using namespace vix; + +int main() +{ + App app; + + app.get("/*", [](Request &req, Response &res){ + std::string path = "public" + req.path(); + + res.header("Cache-Control", "public, max-age=86400"); + res.file(path); + + }); + + app.run(8080); +} +``` + +With this structure: + +``` +project/ +├── main.cpp +└── public/ + └── index.html +``` + +A request to: + +```txt +/index.html +``` +serves: +```txt +public/index.html +``` + +Run: +```bash +vix run main.cpp +curl http://localhost:8080/index.html +``` + +## SPA fallback + +```cpp +app.get("/api/users", users_handler); +app.static_dir("public"); + +app.get("/*", [](Request &, Response &res){ + res.file("public/index.html"); +}); +``` + +## Split routes into files + +**Header (`src/routes/PublicRoutes.hpp`):** + +```cpp +#pragma once +#include +void public_routes(vix::App &app); +``` + +**Source (`src/routes/PublicRoutes.cpp`):** + +```cpp +#include "PublicRoutes.hpp" + +void public_routes(vix::App &app) +{ + app.get("/", [](vix::Request &, vix::Response &res){ + res.json({"message", "Hello"}); + }); +} +``` + +Update `CMakeLists.txt`: + +```cmake +add_executable(app src/main.cpp src/routes/PublicRoutes.cpp) +``` + +## Complete example + +```cpp +#include +using namespace vix; + +static void public_routes(App &app) +{ + app.get("/", [](Request &, Response &res){ + res.json({"message", "Vix routes example"}); + }); + + app.get("/health", [](Request &, Response &res){ + res.json({"ok", true, "service", "routes-example"}); + }); +} + +static void user_routes(App &app) +{ + app.get("/users", [](Request &req, Response &res){ + res.json({ + "ok", true, + "page", req.query_value("page", "1"), + "items", vix::json::array({"Alice", "Bob"}) + }); + }); + + app.get("/users/{id}", [](Request &req, Response &res){ + const std::string id = req.param("id"); + + if (id == "0") { + res.status(404).json({ + "ok", false, "error", + "not found" + }); + return; + } + + res.json({ + "ok", true, + "user", vix::json::o("id", id), + "name", "Ada" + }); + + }); + + app.post("/users", [](Request &req, Response &res){ + res.status(201).json({ + "ok", true, + "body", req.json() + }); + }); + +} + +int main() +{ + App app; + + public_routes(app); + user_routes(app); + + app.run(8080); + + return 0; +} +``` + +## Common mistakes + +### Missing slash + +```cpp +// Wrong +app.get("health", handler); + +// Correct +app.get("/health", handler); +``` + +### Confusing path params and query params + +```cpp +// Path: /users/{id} → req.param("id") +// Query: /users?page=2 → req.query_value("page", "1") +``` + +### Forgetting to return after error + +```cpp +// Always return after sending an error +if (bad) { + res.status(400).json(...); + return; +} +``` + +### Wildcard route too early + +Specific routes must be registered before wildcard routes. + +## What you should remember + +A route connects: HTTP method + path → C++ handler. +Use path params for resource identity, query params for options, grouped functions as the app grows. +The core idea: routes are the public shape of your application — keep them clear, predictable, and organized. + +## Next chapter + +[Next: Request and Response](/book/09-request-response) diff --git a/docs/book/09-request-response.md b/docs/book/09-request-response.md new file mode 100644 index 0000000..6b51c44 --- /dev/null +++ b/docs/book/09-request-response.md @@ -0,0 +1,239 @@ +# Request and Response + +They are the core of the Vix HTTP model: + +```txt +Request reads what the client sent. +Response sends what your app returns. +``` + +## What is Request? + +| Method | Purpose | +|---------------------------------|----------------------------------------| +| `req.param("id")` | Reads a route path parameter. | +| `req.param("id", "0")` | Reads a route parameter with fallback. | +| `req.query_value("page", "1")` | Reads a query parameter with fallback. | +| `req.query()` | Reads all query parameters. | +| `req.header("Authorization")` | Reads a request header value. | +| `req.body()` | Reads the raw request body. | +| `req.json()` | Reads the parsed JSON body. | +| `req.path()` | Reads the current request path. | + +## What is Response? + +| Method | Purpose | +|--------------------------------|---------------------------------| +| `res.text("Hello")` | Sends a plain text response. | +| `res.json({"ok", true})` | Sends a JSON response. | +| `res.status(201).json(...)` | Sets status before sending. | +| `res.header("X-Foo", "bar")` | Sets a response header value. | +| `res.file("public/index.html")`| Sends a static file response. | + +## Reading path parameters + +```cpp +app.get("/users/{id}", [](Request &req, Response &res){ + const std::string id = req.param("id"); + res.json({"id", id}); +}); +``` + +## Reading query parameters + +```cpp +app.get("/search", [](Request &req, Response &res){ + const std::string q = req.query_value("q", ""); + const std::string page = req.query_value("page", "1"); + + res.json({ + "q", q, + "page", page + }); +}); +``` + +```bash +curl -i "http://127.0.0.1:8080/search?q=vix&page=2" +``` + +## Reading all query parameters + +```cpp +app.get("/debug/query", [](Request &req, Response &res){ + res.json({"query", req.query()}); +}); +``` + +## Reading headers + +```cpp +app.get("/debug/headers", [](Request &req, Response &res){ + res.json({ + "user_agent", req.header("User-Agent"), + "authorization", req.header("Authorization") + }); +}); +``` + +## Reading the raw body + +```cpp +app.post("/debug/body", [](Request &req, Response &res){ + res.json({"body", req.body()}); +}); +``` + +## Reading JSON body + +```cpp +app.post("/users", [](Request &req, Response &res){ + const auto &body = req.json(); + + if (!body.is_object()){ + res.status(400).json({ + "ok", false, + "error", "expected JSON object" + }); + + return; + } + + const std::string name = body.value("name", ""); + const std::string role = body.value("role", "user"); + + if (name.empty()){ + res.status(400).json({ + "ok", false, + "error", "name is required" + }); + + return; + } + + res.status(201).json({ + "ok", true, + "user", vix::json::o("name", name), + "role", role + }); + +}); +``` + +## Setting headers + +```cpp +app.get("/health", [](Request &, Response &res){ + res.header("X-Powered-By", "Vix.cpp"); + res.json({"ok", true}); +}); +``` + +## Cache-Control header + +```cpp +res.header("Cache-Control", "public, max-age=3600"); +res.json({"ok", true}); +``` + +## Download response + +```cpp +res.header("Content-Disposition", "attachment; filename=\"hello.txt\""); +res.file("public/hello.txt"); +``` + +## Error helper + +```cpp +static void respond_error(Response &res, int status, const std::string &message){ + res.status(status).json({ + "ok", false, + "error", message + }); +} +``` + +## Good response shape + +```json +// Success +{ "ok": true, "data": {} } + +// Error +{ "ok": false, "error": "message" } + +// List +{ "ok": true, "count": 2, "data": [] } +``` + +## Request and Response lifecycle + +```txt +client sends request + ↓ +Vix creates Request + ↓ +route handler reads Request + ↓ +route handler writes Response + ↓ +Vix sends response to client +``` + +## Common mistakes + +### Forgetting to name Request when you need it + +```cpp +// Wrong — req is unnamed but used +app.get("/users/{id}", [](Request &, Response &res){ + const std::string id = req.param("id"); +}); // error! + +// Correct +app.get("/users/{id}", [](Request &req, Response &res){ + const std::string id = req.param("id"); + res.json({"id", id}); +}); +``` + +### Forgetting to return after errors + +```cpp +// Wrong +if (name.empty()) { + respond_error(res, 400, "name is required"); +} + +res.status(201).json({"ok", true}); + +// Correct +if (name.empty()) { + respond_error(res, 400, "name is required"); + return; +} + +res.status(201).json({"ok", true}); +``` + +### Trusting JSON body without checking shape + +```cpp +// Better +if (!body.is_object()) { + respond_error(res, 400, "expected JSON object"); + return; +} +``` + +## What you should remember + +**Request** is the input: params, query, headers, body, json. +**Response** is the output: status, headers, text, json, file. + +The core route flow: read Request → validate input → write Response → return. + +## Next chapter + +[Next: JSON API](/book/10-json-api) diff --git a/docs/book/10-json-api.md b/docs/book/10-json-api.md new file mode 100644 index 0000000..bdc1452 --- /dev/null +++ b/docs/book/10-json-api.md @@ -0,0 +1,420 @@ +# JSON API + +Now you will build a complete JSON API. +JSON APIs are one of the most common things you will build with Vix. + +```txt +client sends JSON → Vix reads Request → route validates input → route returns JSON Response +``` + +## Routes to build + +```txt +GET / +GET /health +GET /api/users +GET /api/users/{id} +POST /api/users +``` + +## Recommended response shape + +```json +{ "ok": true, "data": {} } +{ "ok": true, "count": 2, "data": [] } +{ "ok": false, "error": "message" } +``` + +## User struct + +```cpp +struct User +{ + std::int64_t id{}; + std::string name; + std::string role; +}; + +static std::vector make_seed_users() +{ + return { {1, "Alice", "admin"}, {2, "Bob", "user"} }; +} +``` + +> Data is in-memory for now. The database chapter will replace this with SQLite or MySQL. + +## JSON helpers + +```cpp +static json::Json user_to_json(const User &user) +{ + return json::kv({ + {"id", json::Json(user.id)}, + {"name", json::Json(user.name)}, + {"role", json::Json(user.role)}, + }); +} + +static json::Json users_to_json(const std::vector &users) +{ + json::Json items = json::Json::array(); + for (const auto &user : users) + items.push_back(user_to_json(user)); + + return items; +} + +static void respond_error(Response &res, int status, const std::string &message) +{ + res.status(status).json(json::kv({ + {"ok", json::Json(false)}, + {"error", json::Json(message)}, + })); +} + +static std::optional find_user_by_id(const std::vector &users, std::int64_t id) +{ + for (const auto &user : users) + if (user.id == id) + return user; + + return std::nullopt; +} + +static std::optional parse_id(const std::string &text) +{ + try { + return std::stoll(text); + } + catch (...) { + return std::nullopt; + } +} +``` + +## GET /api/users + +```cpp +app.get("/api/users", [&users](Request &, Response &res){ + const auto data = users_to_json(users); + res.json(json::kv({ + {"ok", json::Json(true)}, + {"count", json::Json(static_cast(users.size()))}, + {"data", data} + })); +}); +``` + +```bash +curl -i http://127.0.0.1:8080/api/users +``` + +## GET /api/users/{id} + +```cpp +app.get("/api/users/{id}", [&users](Request &req, Response &res){ + + const auto id = parse_id(req.param("id")); + if (!id) { + respond_error(res, 400, "invalid user id"); + return; + } + + const auto user = find_user_by_id(users, *id); + if (!user) { + respond_error(res, 404, "user not found"); + return; + } + + res.json(json::kv({ + {"ok", json::Json(true)}, + {"data", user_to_json(*user)} + })); + +}); +``` + +```bash +curl -i http://127.0.0.1:8080/api/users/1 +curl -i http://127.0.0.1:8080/api/users/999 # 404 +curl -i http://127.0.0.1:8080/api/users/abc # 400 +``` + +## POST /api/users + +```cpp +app.post("/api/users", [&users](Request &req, Response &res){ + + const auto &body = req.json(); + if (!body.is_object()) + { + respond_error(res, 400, "expected JSON object body"); + return; + } + + const std::string name = body.value("name", ""); + const std::string role = body.value("role", "user"); + + if (name.empty()) { + respond_error(res, 400, "field 'name' is required"); + return; + } + + const std::int64_t next_id = users.empty() ? 1 : users.back().id + 1; + User user{next_id, name, role.empty() ? "user" : role}; + users.push_back(user); + + res.status(201).json(json::kv({ + {"ok", json::Json(true)}, + {"message", json::Json("user created")}, + {"data", user_to_json(user)} + })); + +}); +``` + +```bash +curl -i -X POST http://127.0.0.1:8080/api/users \ + -H "Content-Type: application/json" \ + -d '{"name":"Charlie","role":"user"}' +``` + +## Complete example + +```cpp +#include +#include +#include +#include +#include + +using namespace vix; + +struct User { + std::int64_t id{}; + std::string name; + std::string role; +}; + +static std::vector make_seed_users(){ + return { + {1, "Alice", "admin"}, + {2, "Bob", "user"} + }; +} + +static json::Json user_to_json(const User &u){ + return json::kv({ + {"id", json::Json(u.id)}, + {"name", json::Json(u.name)}, + {"role", json::Json(u.role)} + }); +} + +static json::Json users_to_json(const std::vector &users) +{ + json::Json items = json::Json::array(); + for (const auto &u : users) + items.push_back(user_to_json(u)); + + return items; +} + +static void respond_error(Response &res, int status, const std::string &msg){ + res.status(status).json(json::kv({ + {"ok", json::Json(false)}, + {"error", json::Json(msg)} + })); +} + +static std::optional find_user_by_id(const std::vector &users, std::int64_t id) +{ + for (const auto &u : users) + if (u.id == id) + return u; + + return std::nullopt; +} + +static std::optional parse_id(const std::string &text) +{ + try { + return std::stoll(text); + } catch (...) { + return std::nullopt; + } +} + +static void public_routes(App &app) +{ + app.get("/", [](Request &, Response &res){ + res.json(json::kv({ + {"message", json::Json("Vix JSON API")} + })); + }); + + app.get("/health", [](Request &, Response &res){ + res.json(json::kv({ + {"ok", json::Json(true)}, + {"service", json::Json("json-api")} + })); + }); + +} + +static void user_routes(App &app, std::vector &users) +{ + app.get("/api/users", [&users](Request &, Response &res){ + res.json(json::kv({ + {"ok", json::Json(true)}, + {"count", json::Json(static_cast(users.size()))}, + {"data", users_to_json(users)} + })); + }); + + app.get("/api/users/{id}", [&users](Request &req, Response &res){ + const auto id = parse_id(req.param("id")); + + if (!id) { + respond_error(res, 400, "invalid user id"); + return; + } + + const auto user = find_user_by_id(users, *id); + if (!user) { + respond_error(res, 404, "user not found"); + return; + } + + res.json(json::kv({ + {"ok", json::Json(true)}, + {"data", user_to_json(*user)} + })); + }); + + app.post("/api/users", [&users](Request &req, Response &res){ + const auto &body = req.json(); + + if (!body.is_object()) { + respond_error(res, 400, "expected JSON object body"); + return; + } + + const std::string name = body.value("name", ""); + const std::string role = body.value("role", "user"); + if (name.empty()) { + respond_error(res, 400, "field 'name' is required"); + return; + } + + const std::int64_t next_id = users.empty() ? 1 : users.back().id + 1; + User user{next_id, name, role.empty() ? "user" : role}; + users.push_back(user); + + res.status(201).json(json::kv({ + {"ok", json::Json(true)}, + {"message", json::Json("user created")}, + {"data", user_to_json(user)} + })); + + }); +} + +int main() +{ + std::vector users = make_seed_users(); + + App app; + + public_routes(app); + ruser_routes(app, users); + + app.run(8080); + + return 0; +} +``` + +## Test the complete API + +```bash +curl -i http://127.0.0.1:8080/health +curl -i http://127.0.0.1:8080/api/users +curl -i http://127.0.0.1:8080/api/users/1 +curl -i http://127.0.0.1:8080/api/users/999 +curl -i http://127.0.0.1:8080/api/users/abc +curl -i -X POST http://127.0.0.1:8080/api/users \ + -H "Content-Type: application/json" \ + -d '{"name":"Charlie","role":"user"}' +curl -i -X POST http://127.0.0.1:8080/api/users \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +## Status codes for JSON APIs + +| Status | Meaning | +|--------|-----------------------------------| +| `200` | OK, request succeeded. | +| `201` | Created, resource added. | +| `400` | Bad Request, invalid input. | +| `401` | Unauthorized, auth required. | +| `403` | Forbidden, access denied. | +| `404` | Not Found, resource missing. | +| `409` | Conflict, state mismatch. | +| `429` | Too Many Requests, rate limited. | +| `500` | Internal Server Error. | + +## Route flow for JSON APIs + +```txt +read request → parse params or body → validate input → run logic → format JSON → send response +``` + +## Preparing for the next chapters + +- **Database:** the in-memory vector will be replaced by SQLite or MySQL. +- **Middleware:** CORS, rate limiting, and authentication will wrap the routes. +- **Validation:** manual checks will become declarative with `vix::validation`. + +## Common mistakes + +### Forgetting `Content-Type` with curl + +```bash +curl -i -X POST http://127.0.0.1:8080/api/users \ + -H "Content-Type: application/json" \ + -d '{"name":"Ada"}' +``` + +### Trusting body shape + +```cpp +if (!body.is_object()) { + respond_error(res, 400, "expected JSON object body"); + return; +} +``` + +### Forgetting to return after error + +```cpp +if (name.empty()) { + respond_error(res, 400, "field 'name' is required"); + return; +} +``` + +### Returning inconsistent errors + +Use one helper: `respond_error(res, 400, "message")`. + +## What you should remember + +A JSON API route follows: Request → validate → logic → JSON Response. +Use `res.json(...)` for responses, `req.json()` for JSON bodies, helpers for consistent errors and JSON formatting. +The core idea: JSON APIs become simple when request parsing, validation, logic, and response formatting stay separate. + +## Next chapter + +[Next: Middleware](/book/11-middleware) diff --git a/docs/book/11-middleware.md b/docs/book/11-middleware.md new file mode 100644 index 0000000..1bc40f4 --- /dev/null +++ b/docs/book/11-middleware.md @@ -0,0 +1,314 @@ +# Middleware + +In the previous chapter, you built a JSON API. +Now you will learn middleware. + +Middleware is code that runs around your routes. +It can inspect the request before the route handler runs, and modify the response before it is sent. + +```txt +request → middleware → route handler → response +``` + +## Why middleware exists + +Without middleware, shared logic like CORS, rate limiting, authentication, and security headers must be repeated in every route. Middleware lets you write common behavior once. + +## Public headers + +```cpp +#include +#include +``` + +## Middleware order + +Order matters. Configure middleware before registering routes: + +```cpp +int main() +{ + App app; + + configure_middlewares(app); + register_routes(app); + + app.run(8080); + return 0; +} +``` + +A common order: + +```txt +CORS → rate limit → security headers → body limit → authentication → routes +``` + +## CORS middleware + +```cpp +app.use(vix::middleware::app::cors_dev({ + "http://localhost:5173", + "http://127.0.0.1:5173" +})); +``` + +CORS is not authentication. It is a browser rule that answers: which browser origins are allowed to call this API? + +For production, allow only your real frontend domain. Do not use open CORS unless the API is intentionally public. + +## Rate limiting middleware + +```cpp +app.use(vix::middleware::app::rate_limit({ + .max_requests = 60, + .window_seconds = 60 +})); +``` + +Use rate limiting to protect public APIs, login endpoints, and small VPS deployments. When the limit is exceeded, the API returns `429 Too Many Requests`. + +### Test rate limiting + +```cpp +app.use(vix::middleware::app::rate_limit({ + .max_requests = 5, + .window_seconds = 30 +})); +``` + +```bash +for i in $(seq 1 10); do + curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8080/api/data +done +``` + +## Stricter middleware for auth routes + +```cpp +vix::middleware::app::use_on_prefix( + app, + "/auth", + vix::middleware::app::rate_limit({ + .max_requests = 5, + .window_seconds = 60 + })); +``` + +## Static files middleware + +```cpp +#include +#include + +app.use(vix::middleware::app::adapt_ctx( + vix::middleware::performance::static_files( + std::filesystem::path{"public"}, + { + .mount = "/", + .index_file = "index.html", + .add_cache_control = true, + .cache_control = "public, max-age=3600", + .fallthrough = true, + } + ) +)); +``` + +## Manual auth check in a route + +```cpp +app.get("/api/private", [](Request &req, Response &res){ + + const std::string auth = req.header("Authorization"); + if (auth != "Bearer dev-token") + { + res.status(401).json({ + "ok", false, + "error", "unauthorized" + }); + + return; + } + + res.json({ + "ok", true, + "message", "private data" + }); + +}); +``` + +## Complete example + +```cpp +#include +#include +using namespace vix; + +static void respond_error(Response &res, int status, const std::string &message) +{ + res.status(status).json({"ok", false, "error", message}); +} + +static void configure_middlewares(App &app) +{ + app.use(vix::middleware::app::cors_dev({ + "http://localhost:5173", + "http://127.0.0.1:5173" + })); + + app.use(vix::middleware::app::rate_limit({ + .max_requests = 60, + .window_seconds = 60 + })); + + vix::middleware::app::use_on_prefix( + app, + "/auth", + vix::middleware::app::rate_limit({ + .max_requests = 5, + .window_seconds = 60 + }) + ); +} + +static void public_routes(App &app) +{ + app.get("/", [](Request &, Response &res){ + res.json({"message", "Vix middleware example"}); + }); + + app.get("/health", [](Request &, Response &res){ + res.json({ + "ok", true, + "service", "middleware-example" + }); + }); +} + +static void api_routes(App &app) +{ + app.get("/api/data", [](Request &, Response &res){ + res.json({ + "ok", true, + "data", vix::json::o("name", "Vix.cpp"), + "type", "runtime" + }); + }); + + app.get("/api/private", [](Request &req, Response &res){ + if (req.header("Authorization") != "Bearer dev-token"){ + respond_error(res, 401, "unauthorized"); + return; + } + res.json({"ok", true, "message", "private data"}); + }); +} + +static void auth_routes(App &app) +{ + app.post("/auth/login", [](Request &req, Response &res) + { + const auto &body = req.json(); + if (!body.is_object()) { respond_error(res, 400, "expected JSON object body"); return; } + const std::string email = body.value("email", ""); + const std::string password = body.value("password", ""); + if (email != "ada@example.com" || password != "password123") + { + respond_error(res, 401, "invalid credentials"); + return; + } + res.json({"ok", true, "token", "dev-token"}); + }); +} + +int main() +{ + App app; + + configure_middlewares(app); + public_routes(app); + api_routes(app); + auth_routes(app); + + app.run(8080); + return 0; +} +``` + +## Test + +```bash +curl -i http://127.0.0.1:8080/health +curl -i http://127.0.0.1:8080/api/data +curl -i http://127.0.0.1:8080/api/private +curl -i http://127.0.0.1:8080/api/private -H "Authorization: Bearer dev-token" +curl -i -X POST http://127.0.0.1:8080/auth/login \ + -H "Content-Type: application/json" \ + -d '{"email":"ada@example.com","password":"password123"}' + +# Test CORS +curl -i http://127.0.0.1:8080/api/data -H "Origin: http://localhost:5173" +``` + +## Middleware and route responsibility + +| Middleware | Example route action | +|------------------|-------------------------------| +| CORS | Allows browser API calls. | +| Rate limiting | Protects public endpoints. | +| Authentication | Guards dashboard routes. | +| Request IDs | Tracks each request in logs. | +| Logging | Records request activity. | +| Body limits | Rejects oversized requests. | +## Common mistakes + +### Registering middleware after routes + +```cpp +// Wrong +register_routes(app); +configure_middlewares(app); + +// Correct +configure_middlewares(app); +register_routes(app); +``` + +### Making CORS too open in production + +Development CORS can allow localhost. +Production should allow your real frontend only. + +### Using one rate limit for everything + +Login needs a stricter limit than normal API routes. + +### Returning 403 instead of 429 for rate limits + +Rate limit failures should return `429 Too Many Requests`. + +## What you should remember + +Middleware wraps route handlers. Use it for shared behavior: +CORS, rate limiting, authentication, logging, security, body limits, static files. + +```cpp +int main() +{ + App app; + + configure_middlewares(app); + register_routes(app); + + app.run(8080); +} +``` + +The core idea: +middleware keeps route handlers clean by moving shared request behavior into one reusable layer. + +## Next chapter + +[Next: Validation](/book/12-validation) diff --git a/docs/book/12-validation.md b/docs/book/12-validation.md new file mode 100644 index 0000000..46845d9 --- /dev/null +++ b/docs/book/12-validation.md @@ -0,0 +1,252 @@ +# Validation + +In the previous chapter, you learned middleware. +Now you will learn validation. + +Validation checks whether incoming data is correct before your application uses it. + +```txt +request data → validation → business logic → response +``` + +## Why validation exists + +A route that trusts input blindly can receive missing fields, invalid emails, weak passwords, wrong types, or unsafe data. Validation prevents bad input from reaching real application logic. + +## Public header + +```cpp +#include +``` + +## Validate one string + +```cpp +auto result = vix::validation::validate("email", email) + .required() + .email() + .length_max(120) + .result(); + +if (!result.ok()) +{ + for (const auto &error : result.errors.all()) + { + std::cout << "field=" << error.field << " message=" << error.message << "\n"; + } +} +``` + +## Common rules + +| Rule | Purpose | +|-------------------|----------------------------------------------| +| `required()` | Requires a present and non-empty value. | +| `email()` | Requires a valid email address format. | +| `length_min(n)` | Requires a string length of at least `n`. | +| `length_max(n)` | Requires a string length of at most `n`. | +| `min(n)` | Requires a numeric value of at least `n`. | +| `max(n)` | Requires a numeric value of at most `n`. | +| `between(a, b)` | Requires a value between `a` and `b`. | +| `in_set({...})` | Requires one of the allowed values. | + +## Validate numbers + +```cpp +int age = 17; +auto result = vix::validation::validate("age", age) + .min(18, "must be adult") + .max(120) + .result(); +``` + +## Validate allowed values + +```cpp +auto result = vix::validation::validate("role", role) + .required() + .in_set({"admin", "user", "guest"}) + .result(); +``` + +## Parsed validation (string → number) + +```cpp +auto result = vix::validation::validate_parsed("age", input) + .between(18, 120) + .result("age must be a number"); +``` + +Useful for query params, route params, and form fields that arrive as strings. + +## Schema validation + +```cpp +struct UserInput +{ + std::string email; + std::string password; + + static vix::validation::Schema schema() + { + return vix::validation::schema() + .field("email", &UserInput::email, + vix::validation::field().required().email().length_max(120)) + + .field("password", &UserInput::password, + vix::validation::field().required().length_min(8).length_max(64)); + } +}; + +UserInput input; +input.email = "bad-email"; +input.password = "123"; + +auto result = UserInput::schema().validate(input); +``` + +## BaseModel + +```cpp +struct RegisterForm : vix::validation::BaseModel +{ + std::string email; + std::string password; + + static vix::validation::Schema schema() + { + return vix::validation::schema() + .field("email", &RegisterForm::email, + vix::validation::field().required().email().length_max(120)) + + .field("password", &RegisterForm::password, + vix::validation::field().required().length_min(8).length_max(64)); + } +}; + +RegisterForm form; +auto result = form.validate(); // call on object +auto result2 = RegisterForm::validate(form); // static call +``` + +## Cross-field validation + +```cpp +.check([](const ResetPassword &obj, vix::validation::ValidationErrors &errors){ + if (!obj.password.empty() && !obj.confirm.empty() && obj.password != obj.confirm){ + errors.add("confirm", vix::validation::ValidationErrorCode::Custom, + "passwords do not match"); + } +}); +``` + +## Validation in a route + +```cpp +static json::Json validation_errors_to_json( + const vix::validation::ValidationErrors &errors) +{ + json::Json items = json::Json::array(); + for (const auto &error : errors.all()) + { + items.push_back(json::kv({ + {"field", json::Json(error.field)}, + {"code", json::Json(vix::validation::to_string(error.code))}, + {"message", json::Json(error.message)}, + })); + } + return items; +} + +template +static void respond_validation_error(Response &res, const Result &result) +{ + res.status(400).json(json::kv({ + {"ok", json::Json(false)}, + {"error", json::Json("validation failed")}, + {"errors", validation_errors_to_json(result.errors)} + })); +} + +app.post("/api/register", [](Request &req, Response &res){ + + const auto &body = req.json(); + if (!body.is_object()) { + res.status(400).json({ + "ok", false, + "error", "expected JSON object body" + }); + return; + } + + RegisterInput input; + input.email = body.value("email", ""); + input.password = body.value("password", ""); + + auto result = input.validate(); + if (!result.ok()) { + respond_validation_error(res, result); + return; + } + + res.status(201).json({ + "ok", true, + "message", "registered" + }); + +}); +``` + +## Structured error shape + +```json +{ + "ok": false, + "error": "validation failed", + "errors": [ + { "field": "email", "code": "format", "message": "invalid email format" }, + { "field": "password", "code": "length_min", "message": "password too short" } + ] +} +``` + +## Common mistakes + +### Validating after business logic + +```cpp +// Wrong: create user then validate +// Correct: validate then create user +``` + +### Forgetting body shape check + +```cpp +if (!body.is_object()) { + respond_error(res, 400, "expected JSON object body"); + return; +} +``` + +### Forgetting to return after validation failure + +```cpp +if (!result.ok()) { + respond_validation_error(res, result); + return; +} +``` + +### Returning only one validation error + +For forms, return all field errors at once so users can fix everything together. + +## What you should remember + +The normal flow: Request → validation → business logic → Response. +Use single-value validation for simple fields, schemas for structs, structured errors for APIs. +The core idea: bad input should stop at the boundary of your application. + +## Next chapter + +[Next: Errors and logging](/book/13-errors-and-logging) diff --git a/docs/book/13-errors-and-logging.md b/docs/book/13-errors-and-logging.md new file mode 100644 index 0000000..5be8298 --- /dev/null +++ b/docs/book/13-errors-and-logging.md @@ -0,0 +1,363 @@ +# Errors and logging + +In the previous chapter, you learned validation. +Now you will learn errors and logging. + +```txt +request → validation → business logic → error or success → structured response → structured logs +``` + +A production application should return clear responses to clients and keep useful logs for developers. + +## Why error handling matters + +If every route returns a different error shape, the API becomes hard to use. +A Vix API should use one predictable shape. + +## Recommended error shape + +```json +{ "ok": false, "error": "message" } +{ "ok": false, "error": "validation_failed", "errors": [] } +{ "ok": true, "data": {} } +``` + +## HTTP status codes + +| Status | Meaning | +|--------|-----------------------------------| +| `200` | OK, request succeeded. | +| `201` | Created, resource added. | +| `400` | Bad Request, invalid input. | +| `401` | Unauthorized, auth required. | +| `403` | Forbidden, access denied. | +| `404` | Not Found, resource missing. | +| `409` | Conflict, state mismatch. | +| `429` | Too Many Requests, rate limited. | +| `500` | Internal Server Error. | + +Do not return 200 for errors. + +## Basic error helper + +```cpp +static void respond_error( + vix::Response &res, + int status, + const std::string &code, + const std::string &message) +{ + res.status(status).json(vix::json::kv({ + {"ok", vix::json::Json(false)}, + {"error", vix::json::Json(code)}, + {"message", vix::json::Json(message)}, + })); +} +``` + +Always return after sending an error: + +```cpp +if (name.empty()) { + respond_error(res, 400, "validation_failed", "name is required"); + return; +} +``` + +## Error codes + +Use stable codes for production APIs: + +```txt +invalid_request, +validation_failed, +unauthorized, +forbidden, +not_found, +conflict, +rate_limited, +internal_error, +user_not_found, +email_already_used, +invalid_credentials, +product_not_found, +invalid_token, +session_expired +``` + +## Do not leak internal errors + +```cpp +catch (const std::exception &e) +{ + vix::log::error("unhandled route error", "details", e.what()); + res.status(500).json({ + "ok", false, + "error", "internal_error", + "message", "Internal server error" + }); +} +``` + +## Public logging header + +```cpp +#include +``` + +## Basic logs + +```cpp +vix::log::set_level(vix::log::LogLevel::Trace); +vix::log::trace("trace message"); +vix::log::debug("debug message"); +vix::log::info("info message"); +vix::log::warn("warn message"); +vix::log::error("error message"); +vix::log::critical("critical message"); +``` + +## Log levels + +| Level | Use | +|------------|------------------------------------------| +| `trace` | Records very detailed debugging events. | +| `debug` | Records useful debugging information. | +| `info` | Records normal application events. | +| `warn` | Records unusual but non-fatal events. | +| `error` | Records failed operations. | +| `critical` | Records serious system failures. | + +Recommended: `info` in production, `debug` or `trace` during development. + +## Structured logs + +```cpp +vix::log::logf( + vix::log::LogLevel::Info, + "user authenticated successfully", + "status", 200, + "method", "POST", + "path", "/login" +); +``` + +## Log formats + +```cpp +vix::log::set_format(vix::log::LogFormat::KV); // local development +vix::log::set_format(vix::log::LogFormat::JSON); // production log collectors +``` + +## Log context + +```cpp +vix::log::LogContext ctx; +ctx.request_id = "req-abc-123"; +ctx.module = "auth"; +ctx.fields["user_id"] = "42"; +vix::log::set_context(ctx); +vix::log::info("user authenticated successfully"); +vix::log::clear_context(); +``` + +## Set log level + +```cpp +vix::log::set_level(vix::log::LogLevel::Info); // in code +// vix run main.cpp --log-level debug // via CLI +// VIX_LOG_LEVEL=info // via environment +``` + +## What to log + +Good: +app started, +user registered, +login failed, +database connection failed, +unexpected exception. + +Never log: +passwords, +tokens, +private keys, +sensitive personal data. + +## Complete example + +```cpp +#include +#include +#include + +using namespace vix; + +struct RegisterInput : vix::validation::BaseModel +{ + std::string email; + std::string password; + + static vix::validation::Schema schema() + { + return vix::validation::schema() + .field("email", &RegisterInput::email, + vix::validation::field().required().email().length_max(120)) + + .field("password", &RegisterInput::password, + vix::validation::field().required().length_min(8).length_max(64)); + } +}; + +static json::Json validation_errors_to_json(const vix::validation::ValidationErrors &errors) +{ + json::Json items = json::Json::array(); + + for (const auto &error : errors.all()) + items.push_back(json::kv({ + {"field", json::Json(error.field)}, + {"code", json::Json(vix::validation::to_string(error.code))}, + {"message", json::Json(error.message)} + })); + + return items; +} + +static void respond_error(Response &res, int status, const std::string &code, const std::string &message) +{ + res.status(status).json(json::kv({ + {"ok", json::Json(false)}, + {"error", json::Json(code)}, + {"message", json::Json(message)} + })); +} + +int main() +{ + vix::log::set_level(vix::log::LogLevel::Info); + vix::log::set_format(vix::log::LogFormat::KV); + vix::log::info("starting errors and logging example"); + + App app; + + app.get("/health", [](Request &, Response &res){ + vix::log::debug("health check requested"); + res.json({ + "ok", true, + "service", "errors-logging-example" + }); + }); + + app.post("/api/register", [](Request &req, Response &res){ + + try{ + const auto &body = req.json(); + if (!body.is_object()) { + respond_error(res, 400, "invalid_request", "Expected JSON object body"); + return; + } + + RegisterInput input; + input.email = body.value("email", ""); + input.password = body.value("password", ""); + + auto result = input.validate(); + if (!result.ok()){ + vix::log::warn("register validation failed", "email", input.email); + + res.status(400).json(json::kv({ + {"ok", json::Json(false)}, + {"error", json::Json("validation_failed")}, + {"errors", validation_errors_to_json(result.errors)} + })); + + return; + } + + vix::log::logf(vix::log::LogLevel::Info, "user registered", "email", input.email); + + res.status(201).json({ + "ok", true, + "message", "registered" + }); + + } + catch (const std::exception &e){ + vix::log::error("register failed with exception", "details", e.what()); + respond_error(res, 500, "internal_error", "Internal server error"); + } + + }); + + app.run(8080); + + return 0; +} +``` + +## Test + +```bash +curl -i http://127.0.0.1:8080/health +curl -i -X POST http://127.0.0.1:8080/api/register \ + -H "Content-Type: application/json" \ + -d '{"email":"ada@example.com","password":"password123"}' +curl -i -X POST http://127.0.0.1:8080/api/register \ + -H "Content-Type: application/json" \ + -d '{"email":"bad-email","password":"123"}' +``` + +## Common mistakes + +### Returning HTTP 200 for errors + +Use the correct error status code always. + +### Logging secrets + +Never log passwords, tokens, private keys, or authorization headers. + +### Exposing internal exceptions + +```cpp +// Wrong +res.json({"error", e.what()}); + +// Correct +vix::log::error("failed", "details", e.what()); + +res.json({ + "ok", false, + "error", "internal_error" +}); +``` + +### Forgetting to return after an error + +```cpp +if (!result.ok()) { + respond_validation_error(res, result); + return; +} +``` + +## Production config + +```dotenv +VIX_LOG_LEVEL=info +VIX_LOG_FORMAT=json +VIX_COLOR=never +``` + +## What you should remember + +Errors are for clients. +Logs are for developers and operators. +API errors should be consistent: `{ "ok": false, "error": "stable_code", "message": "Safe message" }` +Logs should keep useful internal context with structured fields. +The core idea: +a reliable app does not only work when everything succeeds — it also explains what happened when something fails. + +## Next chapter + +[Next: Database](/book/14-database) diff --git a/docs/book/14-database.md b/docs/book/14-database.md new file mode 100644 index 0000000..d6b1f91 --- /dev/null +++ b/docs/book/14-database.md @@ -0,0 +1,410 @@ +# Database + +In the previous chapter, you learned errors and logging. +Now you will connect a Vix application to a database. + +```txt +Request → validation → database query → JSON Response +``` + +Memory disappears when the app restarts. +A database gives your application durable state. + +## Public header + +```cpp +#include +``` + +## SQLite or MySQL? + +| Criteria | SQLite | MySQL | +|--------------|------------------------------ |-----------------------------------| +| Best for | Local development, small apps, MVPs. | Multi-user production APIs. | +| Setup | Very simple, no server required. | Requires a database server. | +| Persistence | Stores data in a local file. | Stores data in a server database. | + +Start with SQLite for learning. + +## Build flags + +```bash +vix build --with-sqlite +vix run main.cpp --with-sqlite + +vix build --with-mysql +vix run main.cpp --with-mysql +``` + +## First SQLite connection + +```cpp +#include + +auto db = vix::db::Database::sqlite("vix.db"); +db.exec("CREATE TABLE IF NOT EXISTS healthcheck (id INTEGER PRIMARY KEY)"); +``` + +## First MySQL connection + +```cpp +auto db = vix::db::Database::mysql("tcp://127.0.0.1:3306", "root", "", "vixdb"); +``` + +## Database from .env + +```dotenv +DATABASE_ENGINE=sqlite +DATABASE_DEFAULT_NAME=vix.db +``` + +```cpp +vix::config::Config cfg{".env"}; +vix::db::Database db{cfg}; +``` + +## Create a table + +```cpp +// SQLite +db.exec( + "CREATE TABLE IF NOT EXISTS users (" + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + "name TEXT NOT NULL, " + "role TEXT NOT NULL)"); + +// MySQL +db.exec( + "CREATE TABLE IF NOT EXISTS users (" + "id BIGINT PRIMARY KEY AUTO_INCREMENT, " + "name VARCHAR(255) NOT NULL, " + "role VARCHAR(64) NOT NULL)"); +``` + +## Insert data + +```cpp +db.exec("INSERT INTO users (name, role) VALUES (?, ?)", "Alice", "admin"); +``` + +Always use parameterized queries. +Never build SQL with string concatenation. + +## Query data + +```cpp +auto rows = db.query("SELECT id, name, role FROM users"); +while (rows->next()) +{ + const auto &row = rows->row(); + std::cout << row.getInt64(0) << " " << row.getString(1) << " " << row.getString(2) << "\n"; +} +``` + +## Prepared statements + +```cpp +vix::db::PooledConn conn(db.pool()); +auto stmt = conn->prepare("SELECT id, name FROM users WHERE id = ?"); +stmt->bind(1, static_cast(1)); +auto rows = stmt->query(); +``` + +Use prepared statements for: +user input, +route parameters, +query filters, +inserts, +updates, +deletes. + +## Connection pool + +```cpp +vix::db::PooledConn conn(db.pool()); +// connection returns automatically when PooledConn is destroyed (RAII) +``` + +## Transactions + +```cpp +db.transaction([&](vix::db::Connection &conn){ + conn.prepare("INSERT INTO users (name, role) VALUES (?, ?)") + ->bind(1, "Alice")->bind(2, "admin")->exec(); + conn.prepare("INSERT INTO users (name, role) VALUES (?, ?)") + ->bind(1, "Bob")->bind(2, "user")->exec(); +}); +``` + +Use transactions for: +orders + items, +user + profile, +money transfers, +any multi-step write. + +## Complete database API + +```cpp +#include +#include +#include +#include +#include +#include +#include + +using namespace vix; + +struct User { + std::int64_t id{}; + std::string name; + std::string role; +}; + +static json::Json user_to_json(const User &u) +{ + return json::kv({ + {"id", json::Json(u.id)}, + {"name", json::Json(u.name)}, + {"role", json::Json(u.role)} + }); +} + +static void respond_error(Response &res, + int status, + const std::string &code, + const std::string &msg) +{ + res.status(status).json(json::kv({ + {"ok", json::Json(false)}, + {"error", json::Json(code)}, + {"message", json::Json(msg)} + })); +} + +static std::optional parse_id(const std::string &text) +{ + try { + return std::stoll(text); + } catch (...) { + return std::nullopt; + } +} + +static void initialize_schema(vix::db::Database &db) +{ + db.exec("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, role TEXT NOT NULL)"); +} + +static void seed_users(vix::db::Database &db) +{ + auto rows = db.query("SELECT COUNT(*) FROM users"); + + if (rows->next() && rows->row().getInt64(0) > 0) + return; + + db.exec("INSERT INTO users (name, role) VALUES (?, ?)", "Alice", "admin"); + db.exec("INSERT INTO users (name, role) VALUES (?, ?)", "Bob", "user"); +} + +static json::Json list_users(vix::db::Database &db) +{ + json::Json items = json::Json::array(); + auto rows = db.query("SELECT id, name, role FROM users ORDER BY id ASC"); + + while (rows->next()) + { + const auto &row = rows->row(); + items.push_back(user_to_json({row.getInt64(0), row.getString(1), row.getString(2)})); + } + return items; +} + +static std::optional find_user_by_id(vix::db::Database &db, std::int64_t id) +{ + vix::db::PooledConn conn(db.pool()); + auto stmt = conn->prepare("SELECT id, name, role FROM users WHERE id = ?"); + stmt->bind(1, id); + auto rows = stmt->query(); + + if (!rows->next()) + return std::nullopt; + + const auto &row = rows->row(); + return User{row.getInt64(0), row.getString(1), row.getString(2)}; +} + +static User create_user(vix::db::Database &db, const std::string &name, const std::string &role) +{ + db.exec("INSERT INTO users (name, role) VALUES (?, ?)", name, role); + auto rows = db.query("SELECT id, name, role FROM users ORDER BY id DESC LIMIT 1"); + + if (!rows->next()) + throw std::runtime_error("failed to load created user"); + + const auto &row = rows->row(); + + return { + row.getInt64(0), + row.getString(1), + row.getString(2) + }; +} + +static void register_user_routes(App &app, vix::db::Database &db) +{ + app.get("/api/users", [&db](Request &, Response &res){ + try { + res.json(json::kv({ + {"ok", json::Json(true)}, + {"data", list_users(db)} + })); + + }catch (const std::exception &e) { + vix::log::error("failed to list users", "details", e.what()); respond_error(res, 500, "internal_error", "Internal server error"); + } + }); + + app.get("/api/users/{id}", [&db](Request &req, Response &res){ + const auto id = parse_id(req.param("id")); + + if (!id) { + respond_error(res, 400, "invalid_id", "Invalid user id"); + return; + } + + const auto user = find_user_by_id(db, *id); + if (!user) { + respond_error(res, 404, "user_not_found", "User not found"); + return; + } + + res.json(json::kv({ + {"ok", json::Json(true)}, + {"data", user_to_json(*user)} + })); + }); + + app.post("/api/users", [&db](Request &req, Response &res){ + try{ + const auto &body = req.json(); + if (!body.is_object()) { + respond_error(res, 400, "invalid_request", "Expected JSON object body"); + return; + } + + const std::string name = body.value("name", ""); + const std::string role = body.value("role", "user"); + if (name.empty()) { + respond_error(res, 400, "validation_failed", "Field 'name' is required"); + return; + } + + const User user = create_user(db, name, role.empty() ? "user" : role); + res.status(201).json(json::kv({ + {"ok", json::Json(true)}, + {"message", json::Json("user created")}, + {"data", user_to_json(user)} + })); + + }catch (const std::exception &e) { + vix::log::error("failed to create user", "details", e.what()); respond_error(res, 500, "internal_error", "Internal server error"); + } + }); +} + +int main() +{ + vix::log::set_level(vix::log::LogLevel::Info); + try{ + auto db = vix::db::Database::sqlite("vix.db"); + initialize_schema(db); + seed_users(db); + + App app; + + app.get("/health", [](Request &, Response &res) { + res.json({"ok", true, "service", "database-api"}); + }); + + register_user_routes(app, db); + + app.run(8080); + + return 0; + }catch (const std::exception &e){ + vix::log::critical("application startup failed", "details", e.what()); + return 1; + } +} +``` + +## Test + +```bash +curl -i http://127.0.0.1:8080/health +curl -i http://127.0.0.1:8080/api/users +curl -i http://127.0.0.1:8080/api/users/1 +curl -i http://127.0.0.1:8080/api/users/999 +curl -i -X POST http://127.0.0.1:8080/api/users \ + -H "Content-Type: application/json" \ + -d '{"name":"Charlie","role":"user"}' +``` + +Restart the app — unlike the memory API, the new user should still exist. + +## Migrations + +For real projects, use migrations instead of `CREATE TABLE IF NOT EXISTS` at startup. + +```cpp +class CreateUsersTable final : public vix::db::Migration +{ +public: + std::string id() const override { return "2026-01-22-create-users"; } + + void up(vix::db::Connection &conn) override + { + conn.prepare("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, role TEXT NOT NULL)")->exec(); + } + + void down(vix::db::Connection &conn) override + { + conn.prepare("DROP TABLE IF EXISTS users")->exec(); + } +}; +``` + +## Common mistakes + +### Building SQL with string concatenation + +```cpp +// Wrong +std::string sql = "SELECT * FROM users WHERE name = '" + name + "'"; + +// Correct — use prepared statements +auto stmt = conn->prepare("SELECT * FROM users WHERE name = ?"); +stmt->bind(1, name); +``` + +### Returning raw database errors + +Log internal details, return safe client errors. + +### Not validating before insert + +Always validate input before database writes. + +## What you should remember + +The Vix DB model is explicit: +connect → prepare → bind → query → read rows → commit when needed. + +Use prepared statements for user input. +Use transactions for multi-step writes. +Routes should validate input, call database logic, and return safe JSON responses. + +## Next chapter + +[Next: Real-time WebSocket](/book/15-realtime-websocket) diff --git a/docs/book/15-realtime-websocket.md b/docs/book/15-realtime-websocket.md new file mode 100644 index 0000000..ea247c5 --- /dev/null +++ b/docs/book/15-realtime-websocket.md @@ -0,0 +1,235 @@ +# Real-time WebSocket + +In the previous chapter, you connected a Vix app to a database. +Now you will learn WebSocket. + +HTTP is request/response — the connection ends. +WebSocket stays open for real-time communication. + +```txt +client connects → connection stays open → client sends events → server sends events → many clients receive updates +``` + +## When to use WebSocket + +Use HTTP for: +CRUD APIs, +page loading, +authentication, +normal JSON APIs. + +Use WebSocket for: +live messages, +dashboards, +presence, +streaming events, +notifications. + +## Public headers + +```cpp +#include +#include +#include // for HTTP + WebSocket together +``` + +## Vix typed message model + +```json +{ "type": "chat.message", "payload": { "user": "Ada", "text": "Hello" } } +``` + +## Minimal WebSocket server + +```cpp +#include +#include +#include + +int main() +{ + vix::config::Config config{".env"}; + auto executor = std::make_shared(1u); + vix::websocket::Server ws{config, executor}; + + ws.on_open([&ws](vix::websocket::Session &session){ + ws.broadcast_json( + "system.connected", + {"message", "A client connected" + }); + }); + + ws.on_message([&ws](vix::websocket::Session &session, const std::string &payload){ + ws.broadcast_json( + "echo.raw", + {"text", payload}); + }); + + ws.on_typed_message([&ws](vix::websocket::Session &session, + const std::string &type, + const vix::json::kvs &payload){ + ws.broadcast_json(type, payload); + }); + + ws.listen_blocking(); + return 0; +} +``` + +## HTTP + WebSocket together + +```cpp +struct BasicRuntime +{ + vix::config::Config config{".env"}; + std::shared_ptr executor{ + std::make_shared(1u)}; + vix::App app{executor}; + vix::websocket::Server ws{config, executor}; +}; +``` + +### Register HTTP routes + +```cpp +static void register_http_routes(vix::App &app) +{ + app.get("/", [](vix::Request &, vix::Response &res){ + res.json({"name", "Vix HTTP + WebSocket"}); + }); + + app.get("/health", [](vix::Request &, vix::Response &res){ + res.json({ "status", "ok", "service", "http-ws" }); + }); +} +``` + +### Register WebSocket protocol + +```cpp +static void register_ws_protocol(vix::websocket::Server &ws) +{ + ws.on_typed_message( + [&ws](vix::websocket::Session &session, + const std::string &type, + const vix::json::kvs &payload) + { + if (type == "app.ping") + { + ws.broadcast_json("app.pong", {"status", "ok", "transport", "websocket"}); + return; + } + if (type == "chat.message") + { + ws.broadcast_json("chat.message", payload); + return; + } + ws.broadcast_json("app.unknown", {"type", type, "message", "Unknown event type"}); + }); +} +``` + +### Run together + +```cpp +vix::run_http_and_ws(runtime.app, runtime.ws, runtime.executor, runtime.config.getServerPort()); +``` + +## Compact alternative: `serve_http_and_ws` + +```cpp +vix::serve_http_and_ws(".env", 8080, [](auto &app, auto &ws) { + + app.get("/", [](auto &, auto &res) { + res.json({"framework", "Vix.cpp"}); + }); + + 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); + }); + +}); +``` + +## Recommended event protocol + +| Event | Direction | Purpose | +|-----------------------|------------------|-----------------------------------| +| `chat.join` | Client -> server | Joins a chat room or session. | +| `chat.leave` | Client -> server | Leaves a chat room or session. | +| `chat.message` | Both directions | Sends or receives chat content. | +| `chat.error` | Server -> client | Reports a chat-related error. | +| `app.ping` | Client -> server | Sends a health check request. | +| `app.pong` | Server -> client | Returns a health check response. | +| `system.connected` | Server -> client | Confirms the client connected. | +| `system.disconnected` | Server -> client | Confirms the client disconnected. | + +## Validate WebSocket payloads + +```cpp +if (type == "chat.message") +{ + const std::string text = payload.get_string_or("text", ""); + if (text.empty()) + { + ws.broadcast_json( + "chat.error", { + "error", "message_required", + "message", "Message text is required" + } + ); + return; + } + ws.broadcast_json("chat.message", payload); +} +``` + +## WebSocket and Nginx + +```nginx +proxy_set_header Upgrade $http_upgrade; +proxy_set_header Connection "upgrade"; +proxy_read_timeout 3600; +proxy_send_timeout 3600; +``` + +## Common mistakes + +### Using WebSocket for normal CRUD + +Use HTTP for create/read/update/delete. +Use WebSocket for live events only. + +### Broadcasting unvalidated payloads + +Validate payloads before broadcasting. + +### Forgetting Nginx upgrade headers + +Without upgrade headers, WebSocket fails through Nginx. + +### Not handling unknown event types + +Always have a fallback for unknown types returning `app.unknown`. + +## What you should remember + +HTTP is request/response. +WebSocket is a long-lived real-time connection. + +Vix WebSocket uses: +`Server`, +`Session`, +`on_open`, +`on_close`, +`on_error`, +`on_message`, +`on_typed_message`, +`broadcast_json`. + +The core idea: +use HTTP for normal API requests and WebSocket for live events that must reach clients immediately. + +## Next chapter + +[Next: Async runtime](/book/16-async-runtime) diff --git a/docs/book/16-async-runtime.md b/docs/book/16-async-runtime.md new file mode 100644 index 0000000..63f6a31 --- /dev/null +++ b/docs/book/16-async-runtime.md @@ -0,0 +1,234 @@ +# Async runtime + +In the previous chapter, you learned WebSocket. +Now you will learn the async runtime. + +```txt +event loop → coroutines → timers, signals, networking, CPU pool → completion resumes on event loop +``` + +Vix async is not a web framework. +It is a low-level C++20 asynchronous runtime core — the deterministic foundation for servers, networking, timers, workers, and real-time systems. + +## Why async exists + +C++ has coroutines as a language feature, but not a full runtime. +You still need: +event loop, +scheduler, +timers, +I/O integration, +CPU worker pool, +signal handling, +cancellation. + +Vix async provides that foundation. + +## The core idea + +User coroutine code resumes on the event loop. +Blocking or CPU-heavy work is moved elsewhere. +Completion returns to the event loop. + +```txt +event loop thread → runs user coroutine → starts timers/I/O/CPU jobs → completion → coroutine resumes +``` + +## Main components + +| Component | Purpose | +|---------------|----------------------------------------------| +| `io_context` | Owns the event loop and runtime services. | +| `scheduler` | Schedules coroutine resumption. | +| `task` | Represents a coroutine return type. | +| `timer` | Provides non-blocking timers. | +| `thread_pool` | Offloads CPU-bound work from the event loop. | +| `signal_set` | Handles operating system signals. | +| `net::*` | Provides asynchronous networking primitives. | +| `when_all` | Waits for multiple tasks to complete. | +| `when_any` | Waits for the first completed task. | + +## Minimal async example + +```cpp +#include +#include + +using vix::async::core::io_context; +using vix::async::core::task; + +static task app(io_context &ctx) +{ + vix::print("[async] hello from task"); + + co_await ctx.timers().sleep_for(std::chrono::milliseconds(100)); + + vix::print("[async] after timer"); + + ctx.stop(); + co_return; +} + +int main() +{ + io_context ctx; + auto t = app(ctx); + ctx.post(t.handle()); // schedule task + ctx.run(); // start event loop + vix::print("[async] done"); + return 0; +} +``` + +## Timers + +Use `co_await` timers instead of blocking sleep: + +```cpp +// Wrong — blocks event loop +std::this_thread::sleep_for(std::chrono::milliseconds(100)); + +// Correct — yields to event loop +co_await ctx.timers().sleep_for(std::chrono::milliseconds(100)); + +// Callback style +ctx.timers().after(std::chrono::milliseconds(150), []() { /* callback */ }); +``` + +## CPU-bound work + +CPU-heavy work belongs in the CPU pool: + +```cpp +// Wrong — blocks event loop +for (int i = 0; i < 100000000; ++i) { /* heavy work */ } + +// Correct — offload to worker thread +int result = co_await ctx.cpu_pool().submit([]() { return heavy_work(); }); +``` + +## Signals + +```cpp +#include + +auto &signals = ctx.signals(); +signals.add(SIGINT); +signals.add(SIGTERM); + +signals.on_signal([&](int signal) { ctx.stop(); }); + +int signal = co_await signals.async_wait(); +ctx.stop(); +``` + +## TCP echo server + +```cpp +static task handle_client(std::unique_ptr client) +{ + std::vector buffer(4096); + while (client && client->is_open()) + { + std::size_t n = co_await client->async_read(std::span(buffer.data(), buffer.size())); + if (n == 0) break; + co_await client->async_write(std::span(buffer.data(), n)); + } + client->close(); +} + +static task server(io_context &ctx) +{ + auto listener = vix::async::net::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(); + vix::async::core::spawn_detached(ctx, handle_client(std::move(client))); + } +} +``` + +## `when_all` and `when_any` + +```cpp +// Wait for both to complete +auto values = co_await when_all(sched, a(), b()); +std::get<0>(values); // result from a() +std::get<1>(values); // result from b() + +// Wait for first to complete +auto [index, vals] = co_await when_any(sched, a(), b()); +``` + +## Async and HTTP + +For normal HTTP apps, you do not need low-level async: + +```cpp +App app; +app.get("/", handler); +app.run(8080); +``` + +The runtime handles execution. +Understanding async helps you understand what happens underneath. + +## Common mistakes + +### Blocking the event loop + +```cpp +// Wrong +std::this_thread::sleep_for(std::chrono::seconds(1)); + +// Correct +co_await ctx.timers().sleep_for(std::chrono::seconds(1)); +``` + +### Running heavy CPU work on the event loop + +```cpp +// Wrong +auto result = heavy_work(); + +// Correct +auto result = co_await ctx.cpu_pool().submit([]() { return heavy_work(); }); +``` + +### Forgetting to schedule the task + +```cpp +// A task starts suspended — you must schedule it +ctx.post(t.handle()); +``` + +### Ignoring signal handling + +Production services must handle `SIGINT` and `SIGTERM` for clean shutdown with systemd. + +## What you should remember + +The most important flow: + +```cpp +io_context ctx; +auto t = app(ctx); +ctx.post(t.handle()); // schedule +ctx.run(); // run event loop +// co_await operations inside the coroutine +``` + +Use timers instead of blocking sleep. +Use CPU pool for heavy work. +Use signals for clean shutdown. + +The core idea: +async code stays understandable when execution is explicit, +non-blocking, +and resumed through one clear runtime. + +## Next chapter + +[Next: Cache](/book/17-cache) diff --git a/docs/book/17-cache.md b/docs/book/17-cache.md new file mode 100644 index 0000000..01d941b --- /dev/null +++ b/docs/book/17-cache.md @@ -0,0 +1,272 @@ +# Cache + +In the previous chapter, you learned the async runtime. +Now you will learn cache. + +```txt +request → cache lookup → cache hit → return cached result + → cache miss → compute or fetch → store → return response +``` + +In Vix, cache is not only about speed — it is also part of offline-first behavior. +When the network fails, a cache can safely serve previously stored data. + +## Public headers + +```cpp +#include +#include +#include +#include +#include +#include +#include +#include +``` + +## Core concepts + +| Concept | Purpose | +|------------------|----------------------------------------------| +| `Cache` | Decides whether a cached entry is usable. | +| `CacheEntry` | Stores one cached response or value. | +| `CachePolicy` | Defines TTL, freshness, and stale behavior. | +| `CacheContext` | Describes the current network condition. | +| `MemoryStore` | Stores cached entries in memory. | +| `FileStore` | Stores cached entries on disk. | +| `LruMemoryStore` | Stores bounded entries with LRU eviction. | +| `CacheKey` | Builds stable keys for cached entries. | + +## Time helper + +```cpp +static std::int64_t now_ms() +{ + using namespace std::chrono; + return duration_cast(steady_clock::now().time_since_epoch()).count(); +} +``` + +## CacheEntry + +```cpp +vix::cache::CacheEntry entry; +entry.status = 200; +entry.body = R"({"users":[1,2,3]})"; +entry.headers["Content-Type"] = "application/json"; +entry.created_at_ms = t0; +``` + +## CachePolicy + +```cpp +vix::cache::CachePolicy policy; +policy.ttl_ms = 5'000; // fresh for 5 seconds +policy.allow_stale_if_offline = true; +policy.stale_if_offline_ms = 10'000; // allow stale up to 10s when offline +policy.allow_stale_if_error = true; +policy.stale_if_error_ms = 5'000; // allow stale up to 5s on network error +``` + +## CacheContext + +```cpp +CacheContext::Online() // normal operation +CacheContext::Offline() // no network +CacheContext::NetworkError() // network request failed +``` + +## Minimal memory cache + +```cpp +auto store = std::make_shared(); +vix::cache::CachePolicy policy; +policy.ttl_ms = 5'000; +vix::cache::Cache cache(policy, store); + +const std::string key = "GET /api/users?page=1"; +const auto t0 = now_ms(); + +vix::cache::CacheEntry entry; +entry.status = 200; +entry.body = R"({"users":[1,2,3]})"; +entry.created_at_ms = t0; +cache.put(key, entry); + +auto cached = cache.get(key, t0 + 100, vix::cache::CacheContext::Online()); +if (cached) std::cout << "cache hit: " << cached->body << "\n"; +``` + +## FileStore (persistence) + +```cpp +auto store = std::make_shared(vix::cache::FileStore::Config{ + .file_path = "./cache.json", + .pretty_json = true}); +``` + +Entries persist to disk and survive process restart. + +## LruMemoryStore (bounded memory) + +```cpp +auto store = std::make_shared( + vix::cache::LruMemoryStore::Config{.max_entries = 2048}); +``` + +When capacity is reached, the least recently used entry is evicted. + +## Stale data + +```cpp +// Online — fresh data only (within TTL) +cache.get(key, t0 + 50, CacheContext::Online()); // hit if within 5000ms + +// Offline — allow stale up to stale_if_offline_ms +cache.get(key, t0 + 3000, CacheContext::Offline()); // hit if within 10000ms + +// Network error — allow stale up to stale_if_error_ms +cache.get(key, t0 + 4000, CacheContext::NetworkError()); // hit if within 5000ms + +// Too old — miss regardless of context +cache.get(key, t0 + 20'000, CacheContext::Offline()); // miss +``` + +## CacheKey builder + +```cpp +#include + +std::unordered_map req_headers; +req_headers["Accept"] = "application/json"; + +const std::string key = vix::cache::CacheKey::fromRequest( + "get", + "/api/users", + "b=2&a=1", // query is normalized + req_headers, + {"Accept"}); // vary on Accept header +``` + +Good cache keys include: +method, +path, +normalized query params, +and selected vary headers. + +## Cache in an HTTP route + +```cpp +app.get("/api/products", [&cache](Request &, Response &res){ + const auto now = now_ms(); + const std::string key = "GET /api/products"; + + auto cached = cache.get(key, now, vix::cache::CacheContext::Online()); + if (cached) + { + res.header("X-Vix-Cache", "HIT"); + res.status(cached->status).send(cached->body); + return; + } + + const std::string body = R"({"ok":true,"data":[]})"; + + vix::cache::CacheEntry entry; + entry.status = 200; + entry.body = body; + entry.headers["Content-Type"] = "application/json"; + entry.created_at_ms = now; + cache.put(key, entry); + + res.header("X-Vix-Cache", "MISS"); + res.header("Content-Type", "application/json"); + res.send(body); +}); +``` + +## Policy examples + +```cpp +// Short API cache +policy.ttl_ms = 5'000; + +// Offline-friendly +policy.ttl_ms = 30'000; +policy.allow_stale_if_offline = true; +policy.stale_if_offline_ms = 10 * 60 * 1000; + +// Network-error fallback +policy.ttl_ms = 10'000; +policy.allow_stale_if_error = true; +policy.stale_if_error_ms = 60'000; +``` + +## Good vs bad cache candidates + +#### Good use cases + +- Public `GET` responses +- Product lists +- Configuration +- Feature flags +- Read-heavy dashboards + +#### Be careful with + +- Private user data +- Payment state +- Security decisions +- Rapidly changing values + +## Common mistakes + +### Ignoring query parameters + +```cpp +// Wrong — same key for page=1 and page=2 +const std::string key = "/api/products"; + +// Correct — use CacheKey builder +const std::string key = vix::cache::CacheKey::fromRequest("get", "/api/users", "page=1", {}, {}); +``` + +### No memory limit + +```cpp +// Protect memory for long-running servers +LruMemoryStore::Config{.max_entries = 2048} +``` + +### Not invalidating after writes + +If a POST, PUT, PATCH, or DELETE changes data, old cached GET responses may become stale. +Use short TTLs or explicit invalidation. + +### Caching error responses + +A temporary 500 response should usually not be cached. + +## What you should remember + +Vix cache is built from explicit primitives: +`Cache`, +`CacheEntry`, +`CachePolicy`, +`CacheContext`, +`store`, +`CacheKey`. + +For offline-first: +`Online` prefers fresh, +`Offline` optionally allows stale, +`NetworkError` optionally allows stale. + +The core idea: +cache is not only an optimization — in Vix, +cache is part of predictable behavior when the network is slow, +unstable, +or unavailable. + +## Next chapter + +[Next: Offline-first sync](/book/18-offline-first-sync) diff --git a/docs/book/18-offline-first-sync.md b/docs/book/18-offline-first-sync.md new file mode 100644 index 0000000..353a040 --- /dev/null +++ b/docs/book/18-offline-first-sync.md @@ -0,0 +1,310 @@ +# Offline-first sync + +In the previous chapter, you learned cache. +Now you will learn offline-first sync. + +```txt +local write → WAL → outbox → sync engine → transport → done +``` + +The core idea: +**write locally first, persist the operation, sync when the network is available.** + +## Why offline-first sync exists + +Real applications do not run in perfect conditions. +Networks fail, +servers restart, +requests timeout, +devices go offline. + +A normal online-first flow fails when the network is unavailable: + +```txt +user action → network request → server response → local state updated +``` + +Offline-first sync uses a safer flow: + +```txt +user action → local write → durable log → outbox → sync later +``` + +## The mental model + +```txt +Never depend on the network to preserve user intent. +``` + +| Property | Meaning | +|-----------------|-------------------------------------------------------| +| `Durable` | The operation survives a process restart. | +| `Retryable` | Failed operations can be attempted again safely. | +| `Offline-first` | The app can continue working without network access. | + +## Vix Sync architecture + +```txt +Local Write → WAL → Outbox → SyncWorker → Transport → Done / Retry / Failed +``` + +| Component | Purpose | +|------------------|------------------------------------------------------------| +| `Operation` | Describes one durable unit of work. | +| `Wal` | Stores an append-only operation history. | +| `Outbox` | Stores operations waiting to be synchronized. | +| `RetryPolicy` | Decides when failed operations should retry. | +| `NetworkProbe` | Checks whether network access is currently available. | +| `SyncWorker` | Processes operations that are ready to run. | +| `SyncEngine` | Orchestrates workers, retries, and recovery. | +| `ISyncTransport` | Sends operations to HTTP, P2P, or custom transport targets.| + +## Public headers + +```cpp +#include +#include +``` + +## Operation + +```cpp +vix::sync::Operation op; +op.kind = "http.post"; +op.target = "/api/messages"; +op.payload = R"({"text":"hello"})"; +``` + +## Basic outbox lifecycle + +```cpp +// enqueue → claim → complete → Done +const std::string id = outbox->enqueue(op, t0); +auto ready = outbox->peek_ready(t0, 10); +const bool claimed = outbox->claim(id, t0 + 1); +const bool done = outbox->complete(id, t0 + 2); +``` + +## FileOutboxStore + +```cpp +auto store = std::make_shared( + vix::sync::outbox::FileOutboxStore::Config{ + .file_path = dir / "outbox.json", + .pretty_json = true}); + +auto outbox = std::make_shared( + vix::sync::outbox::Outbox::Config{.owner = "demo"}, + store); +``` + +## RetryPolicy + +```cpp +vix::sync::RetryPolicy retry; +retry.max_attempts = 3; +retry.base_delay_ms = 500; +retry.factor = 2.0; + +auto outbox = std::make_shared( + vix::sync::outbox::Outbox::Config{ + .owner = "demo-retry", + .retry = retry}, + store); +``` + +### Retryable vs permanent failure + +```cpp +// Temporary failure — retry later +outbox->fail(id, "temporary network error", now + 2, true); + +// Permanent failure — stop retrying +outbox->fail(id, "bad request", now + 2, false); +``` + +| Failure | Retry | Example | +|----------------------|-------|---------------------------------| +| Network timeout | Yes | Temporary connection issue. | +| Server unavailable | Yes | HTTP `503` response. | +| Offline device | Yes | No active network connection. | +| Bad request | No | Invalid request payload. | +| Validation error | No | Missing required input field. | + +## WAL + +A WAL (Write-Ahead Log) is an append-only log of durable intent. + +```cpp +vix::sync::wal::Wal wal(vix::sync::wal::Wal::Config{ + .file_path = dir / "wal.log", + .fsync_on_write = false}); + +const auto offset = wal.append(vix::sync::wal::WalRecord{ + .id = "op_1", + .type = vix::sync::wal::RecordType::PutOperation, + .ts_ms = t0, + .payload = to_bytes(payload)}); + +wal.replay(0, [](const vix::sync::wal::WalRecord &rec) + { std::cout << "id=" << rec.id << " type=" << static_cast(rec.type) << "\n"; }); +``` + +## Sync transport + +```cpp +namespace vix::sync::engine +{ + class ExampleTransport final : public ISyncTransport + { + public: + SendResult send(const vix::sync::Operation &op) override + { + std::cout << "sending: " << op.id << " -> " << op.target << "\n"; + return SendResult{.ok = true}; + } + }; +} +``` + +## NetworkProbe + +```cpp +auto probe = std::make_shared( + vix::net::NetworkProbe::Config{}, + [] { return true; }); // online + +// Offline-first control +auto network_online = std::make_shared>(false); +auto probe = std::make_shared( + vix::net::NetworkProbe::Config{}, + [network_online] { return network_online->load(); }); + +// Later when network returns +network_online->store(true); +``` + +## SyncEngine + +```cpp +vix::sync::engine::SyncEngine engine( + vix::sync::engine::SyncEngine::Config{ + .worker_count = 1, + .idle_sleep_ms = 0, + .offline_sleep_ms = 0, + .batch_limit = 10, + .inflight_timeout_ms = 10'000}, + outbox, + probe, + transport); + +const auto processed = engine.tick(now_ms()); +``` + +## Complete local-first flow + +```txt +local file write → WAL append → outbox enqueue → sync engine tick → transport sends → done +``` + +The critical rule: **local write happens before sync.** +**Network is not responsible for preserving the user action.** + +## Offline then recover pattern + +```txt +1. local write succeeds +2. WAL stores intent +3. outbox stores operation +4. network is offline → engine sends nothing (processed = 0) +5. network comes back → network_online->store(true) +6. engine sends operation → operation becomes done +``` + +## Inflight recovery + +If a worker claims an operation and then crashes: + +```txt +claim → crash → restart → engine detects stale InFlight → requeue → worker sends → Done +``` + +This is why durable outbox state matters. + +## Recommended operation kinds + +```txt +http.post, http.put, http.patch, http.delete +fs.write.sync, fs.delete.sync +db.insert.sync, db.update.sync +message.send, event.publish +``` + +## Design rules + +### Persist before sending +Store the operation first, then send the request. Never send before persisting. + +### Treat the network as optional +When the network is available, sync now. When it is unavailable, sync later. + +### Make operations idempotent +Use stable operation IDs so the server can detect and ignore duplicates. + +### Separate temporary and permanent failures +Retry temporary failures. Do not retry invalid data. + +### Keep payloads replayable +Include enough data to replay the operation later. + +## Common mistakes + +### Sending before persisting +If the process crashes after sending but before storing, recovery becomes impossible. **Persist first.** + +### Treating offline as an error +Offline is a normal state in offline-first systems. The operation should remain pending. + +### Forgetting inflight recovery +If a worker crashes after claiming, use inflight timeout recovery to requeue. + +### Using non-idempotent remote writes +Use stable operation ids and deduplication on the receiver. + +## When to use Vix Sync + +Use Vix Sync when your application needs durable operations, offline execution, and safe synchronization. + +Good use cases: + +- Offline-first applications +- Local-first file synchronization +- Reliable message delivery +- Retry-safe background jobs +- Edge applications +- Unstable network environments +- P2P synchronization + +## Production notes + +- Enable fsync for stronger durability +- Use stable operation ids +- Make remote endpoints idempotent +- Separate retryable and permanent failures +- Monitor pending and failed outbox size +- Expose sync health in `/health` + +## What you should remember + +The core flow: `local write → WAL → outbox → sync engine → transport → done` + +The WAL keeps durable history. +The outbox keeps pending work. +The sync worker processes ready operations. +The transport sends to HTTP, P2P, or custom targets. + +The core idea: **persist first, sync later, never lose user intent.** + +## Next chapter + +[Next: P2P](/book/19-p2p) diff --git a/docs/book/19-p2p.md b/docs/book/19-p2p.md new file mode 100644 index 0000000..1e797ee --- /dev/null +++ b/docs/book/19-p2p.md @@ -0,0 +1,308 @@ +# P2P + +In the previous chapter, you learned offline-first sync. Now you will learn P2P. + +P2P means peer-to-peer. Instead of every node depending on a central server, nodes can discover each other, connect, exchange messages, and synchronize data. + +```txt +node A ↔ node B +local write → WAL → outbox → P2P transport → peer → ack +``` + +## Why P2P exists + +Real systems do not always have a perfect cloud connection. P2P gives Vix a way to build systems that communicate directly. + +Use P2P when you want: local network discovery, direct node-to-node communication, peer synchronization, edge replication, offline-first data exchange, store-and-forward systems. + +## The mental model + +| Layer | Purpose | +|-------------------|------------------------------------------------------------| +| `Discovery` | Finds available peers on the network. | +| `Peer connection` | Connects the local node to another peer. | +| `Handshake` | Establishes peer identity and session metadata. | +| `Envelope` | Wraps messages with routing and protocol metadata. | +| `Framing` | Splits byte streams into complete protocol messages. | +| `Dispatch` | Decodes payloads and routes them to typed handlers. | +| `Sync messages` | Pushes WAL entries, sends acknowledgments, and pulls work. | + +## Public headers + +```cpp +#include +#include // for HTTP control routes +``` + +## Message flow + +```txt +typed message → payload bytes → envelope → frame → transport +→ peer → decode frame → decode envelope → dispatch typed message +``` + +## Envelope and framing + +```cpp +#include + +// Create and pack a Ping message +vix::p2p::msg::Ping ping; +ping.nonce = 42; +vix::p2p::Envelope outgoing = vix::p2p::pack::make_envelope(vix::p2p::MessageType::Ping, ping); + +// Frame it (length-prefix framing) +vix::p2p::framing::LengthPrefixVarint framer; +vix::p2p::Frame frame = framer.encode(outgoing.encode()); + +// Decode frame → envelope → message +vix::p2p::FrameDecodeResult decoded = framer.decode(frame.bytes); +vix::p2p::Envelope incoming = vix::p2p::Envelope::decode_or_throw(decoded.frames.front().bytes); +vix::p2p::msg::Ping roundtrip = vix::p2p::msg::Ping::decode_or_throw(incoming.payload); +``` + +TCP is a byte stream — framing ensures the receiver knows where each message begins and ends. + +## Handshake messages + +```cpp +// Node A → Hello +vix::p2p::msg::Hello hello; +hello.nonce_a = 1001; +hello.node_id = "node-a"; +hello.capabilities["proto"] = "1.0"; + +// Node B → HelloAck +vix::p2p::msg::HelloAck ack; +ack.nonce_a = 1001; +ack.nonce_b = 2002; + +// Node A → HelloFinish +vix::p2p::msg::HelloFinish finish; +finish.nonce_a = 1001; +finish.nonce_b = 2002; +finish.signature = /* 64-byte signature */; +``` + +## Discovery announcement + +```cpp +vix::p2p::msg::DiscoveryAnnounce announce; +announce.node_id = "node-a"; +announce.tcp_port = 9001; +announce.ts_ms = 1710000000000ULL; +announce.nonce = 987654321ULL; +announce.capabilities["proto"] = "1.0"; + +const std::string json = announce.to_json(); +auto parsed = vix::p2p::msg::DiscoveryAnnounce::from_json(json); +``` + +## Memory router + +```cpp +vix::p2p::MemoryRouter router; +router.upsert_route("node-b", vix::p2p::Route{"edge-1", false, 8}); +router.upsert_route("node-c", vix::p2p::Route{"relay-7", true, 4}); + +auto route = router.resolve("node-b"); // next_hop="edge-1", via_relay=false +router.remove_route("node-c"); +``` + +## WAL sync messages + +```cpp +// Push WAL records to a peer +vix::p2p::msg::WalPush push; +push.seq_begin = 10; +push.seq_end = 12; +push.wal_bytes = make_fake_wal_bytes(); + +// Acknowledge applied sequence +vix::p2p::msg::WalAck ack; +ack.last_applied_seq = 12; + +// Ask peer for pending operations +vix::p2p::msg::OutboxPull pull; +pull.target_node_id = "node-b"; +pull.max_items = 64; +``` + +## Start a real P2P node + +```cpp +vix::p2p::NodeConfig cfg; +cfg.node_id = "node-a"; +cfg.listen_port = 9001; +cfg.on_log = [](std::string_view line) { std::cout << line << "\n"; }; + +auto node = vix::p2p::make_tcp_node(cfg); +node->start(); + +const auto stats = node->stats(); +// stats.peers_total, peers_connected, handshakes_started, handshakes_completed +``` + +## P2PRuntime and connect + +```cpp +vix::p2p::P2PRuntime runtime(node); +runtime.start(); + +vix::p2p::PeerEndpoint ep; +ep.host = "127.0.0.1"; +ep.port = 9001; +ep.scheme = "tcp"; + +runtime.connect(ep); +// runtime.stats(), runtime.stop() +``` + +## UDP discovery + +```cpp +vix::p2p::DiscoveryConfig cfg; +cfg.self_node_id = "node-a"; +cfg.self_tcp_port = 9001; +cfg.discovery_port = 37020; +cfg.mode = vix::p2p::DiscoveryMode::Broadcast; +cfg.announce_interval_ms = 1000; + +auto on_peer = [](const vix::p2p::DiscoveryAnnouncement &a) +{ + std::cout << "discovered: " << a.node_id << " at " << a.host << ":" << a.port << "\n"; +}; + +auto discovery = vix::p2p::make_udp_discovery(cfg, on_peer); +discovery->start(); +auto snapshot = discovery->snapshot(); +discovery->stop(); +``` + +## P2P HTTP control surface + +```cpp +#include +#include +#include + +vix::p2p::P2PRuntime runtime(node); +runtime.start(); + +vix::p2p_http::P2PHttpOptions opt; +opt.prefix = "/p2p"; +opt.enable_ping = true; +opt.enable_status = true; +opt.enable_peers = true; + +vix::App app; +vix::p2p_http::registerRoutes(app, runtime, opt); +app.listen(8081, []() { std::cout << "HTTP API listening on 8081\n"; }); +``` + +### HTTP control routes + +| Route | Purpose | +|---------------------|--------------------------------------| +| `GET /p2p/ping` | Runs a simple P2P smoke test. | +| `GET /p2p/status` | Returns current runtime statistics. | +| `GET /p2p/peers` | Lists known peers for this node. | +| `POST /p2p/connect` | Connects this node to another peer. | +| `GET /p2p/logs` | Returns recent runtime log entries. | + +```bash +curl http://127.0.0.1:8081/p2p/ping +curl http://127.0.0.1:8081/p2p/status + +curl -X POST http://127.0.0.1:8083/p2p/connect \ + -H "content-type: application/json" \ + -d '{"host":"127.0.0.1","port":9201,"scheme":"tcp"}' +``` + +## Important vix run rule + +```bash +# Correct — --run passes args to your program +vix run p2p_manual_connect.cpp --run server +vix run p2p_manual_connect.cpp --run client 127.0.0.1 9101 + +# Wrong — -- is for compiler/linker flags +vix run p2p_manual_connect.cpp -- server +``` + +## How P2P connects to sync + +P2P can become one transport for the sync engine: + +```txt +WAL → WalPush → peer receives → peer applies → WalAck +``` + +The P2P layer moves sync data between nodes. The sync layer decides what must be durable, retried, replayed, and acknowledged. + +## P2P vs WebSocket vs HTTP + +| Criteria | WebSocket | P2P | HTTP | +|-------------|------------------------|------------------------------|-------------------------| +| Connection | Client -> server. | Node -> node. | Request -> response. | +| Best for | Browser real-time apps.| Distributed systems. | Standard APIs. | +| Discovery | Manual configuration. | UDP discovery or registry. | Manual configuration. | + +## Common mistakes + +### Sending raw bytes without framing + +TCP does not preserve message boundaries. Use framing before decoding. + +### Trusting unknown peers + +Use handshake, identity, and security checks before trusting a peer. + +### Forgetting idempotency + +A peer can receive the same sync message more than once. Use operation ids and WAL sequence numbers to deduplicate. + +### Exposing P2P HTTP control routes without auth + +Routes like `POST /p2p/connect` must be protected in production. + +## Production notes + +- Use stable node ids +- Protect control routes with authentication +- Use secure transport +- Make sync messages idempotent +- Monitor peer counts and handshake failures +- Log peer ids and operation ids + +```json +{ + "ok": true, + "p2p": { + "node_id": "node-a", + "peers_total": 3, + "peers_connected": 2, + "handshakes_completed": 2 + } +} +``` + +## What you should remember + +The basic message flow: +`typed message → envelope → frame → transport → peer → decode → dispatch` + +The runtime flow: +`node starts → peer discovered → handshake → messages exchanged` + +The sync flow: +`WAL → WalPush → peer applies → WalAck` + +P2P does not replace offline-first sync — it complements it. + +The core idea: +**Vix Sync preserves intent. Vix P2P moves that intent between nodes.** + +## Next chapter + +[Next: Production deployment](/book/20-production-deployment) diff --git a/docs/book/20-production-deployment.md b/docs/book/20-production-deployment.md new file mode 100644 index 0000000..9e73bea --- /dev/null +++ b/docs/book/20-production-deployment.md @@ -0,0 +1,388 @@ +# Production deployment + +In the previous chapter, you learned P2P. +Now you will learn how to deploy a Vix application in production. + +```txt +browser → HTTPS → Nginx → Vix app → systemd +``` + +## Why production deployment matters + +During development: `vix dev`. +In production, your application needs: +- A release build +- A stable working directory +- Environment variables +- Automatic restart +- Logs +- A reverse proxy +- HTTPS +- Health checks +- Predictable ports + +## Production architecture + +```txt +Internet → Nginx → 127.0.0.1:8080 → Vix app → systemd +``` + +Nginx handles public HTTP/HTTPS. The Vix app listens locally. systemd keeps the app alive. + +## Development vs production + +| Development | Production | +|-------------------|-------------------------| +| `vix dev` | Release binary. | +| Hot reload | Stable process. | +| Terminal logs | Systemd logs. | +| Local browser | Nginx reverse proxy. | +| Debug settings | Production environment. | +| Manual restart | Automatic restart. | + +## Prepare the server + +```bash +sudo apt update +sudo apt install -y \ + build-essential cmake ninja-build pkg-config \ + nginx certbot python3-certbot-nginx \ + libssl-dev libsqlite3-dev zlib1g-dev libbrotli-dev \ + nlohmann-json3-dev libspdlog-dev libfmt-dev +``` + +## Create a production user + +```bash +sudo useradd --system --create-home --shell /usr/sbin/nologin vix +sudo mkdir -p /home/vix/apps/myapp +sudo chown -R vix:vix /home/vix/apps +``` + +## Configure environment + +```bash +sudo -u vix nano /home/vix/apps/myapp/.env +``` + +```dotenv +SERVER_PORT=8080 +SERVER_HOST=127.0.0.1 +SERVER_TLS_ENABLED=false + +VIX_LOG_LEVEL=info +VIX_LOG_FORMAT=kv +VIX_COLOR=never + +# SQLite +DATABASE_ENGINE=sqlite +DATABASE_DEFAULT_NAME=/home/vix/apps/myapp/data/app.db + +# MySQL (if used) +DATABASE_ENGINE=mysql +DATABASE_DEFAULT_HOST=127.0.0.1 +DATABASE_DEFAULT_PORT=3306 +DATABASE_DEFAULT_USER=myapp +DATABASE_DEFAULT_PASSWORD=change-me +DATABASE_DEFAULT_NAME=myapp +``` + +> When Nginx handles HTTPS, keep `SERVER_TLS_ENABLED=false` — Nginx terminates TLS and proxies to local Vix. + +## Build a release binary + +```bash +cd /home/vix/apps/myapp + +sudo -u vix vix build --preset release +# With SQLite: +sudo -u vix vix build --preset release --with-sqlite +# With MySQL: +sudo -u vix vix build --preset release --with-mysql +``` + +## Test the binary manually + +```bash +cd /home/vix/apps/myapp +sudo -u vix ./build-release/myapp +``` + +```bash +# In another terminal +curl -i http://127.0.0.1:8080/health +``` + +Stop with `Ctrl+C` then proceed to systemd. + +## Create a systemd service + +```bash +sudo nano /etc/systemd/system/vix-myapp.service +``` + +```ini +[Unit] +Description=Vix MyApp service +After=network.target + +[Service] +Type=simple +User=vix +Group=vix +WorkingDirectory=/home/vix/apps/myapp +ExecStart=/home/vix/apps/myapp/build-release/myapp +Restart=always +RestartSec=3 +Environment=VIX_LOG_LEVEL=info +Environment=VIX_LOG_FORMAT=kv +Environment=VIX_COLOR=never +LimitNOFILE=65535 + +[Install] +WantedBy=multi-user.target +``` + +```bash +sudo systemctl daemon-reload +sudo systemctl enable vix-myapp +sudo systemctl start vix-myapp +sudo systemctl status vix-myapp +``` + +## Read logs + +```bash +journalctl -u vix-myapp -f # follow +journalctl -u vix-myapp -n 100 # last 100 lines +journalctl -u vix-myapp -b # since boot +``` + +## Nginx reverse proxy + +```bash +sudo nano /etc/nginx/sites-available/myapp +``` + +```nginx +server { + listen 80; + server_name example.com www.example.com; + + location / { + proxy_pass http://127.0.0.1:8080; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} +``` + +```bash +sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/myapp +sudo nginx -t +sudo systemctl reload nginx +curl -i http://example.com/health +``` + +## Nginx for WebSocket + +```nginx +location / { + proxy_pass http://127.0.0.1:8080; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_read_timeout 3600; + proxy_send_timeout 3600; +} +``` + +## Enable HTTPS + +```bash +sudo certbot --nginx -d example.com -d www.example.com +sudo certbot renew --dry-run +``` + +After Certbot, your config adds SSL and a redirect from HTTP to HTTPS automatically. + +## Health checks + +```cpp +app.get("/health", [](Request &, Response &res) + { + res.json({"ok", true, "service", "myapp"}); + }); +``` + +Production health with more detail: + +```json +{ + "ok": true, + "service": "myapp", + "database": "ok", + "sync": "enabled" +} +``` + +## Deployment flow + +```bash +cd /home/vix/apps/myapp +sudo -u vix git pull +sudo -u vix vix build --preset release +sudo systemctl restart vix-myapp +curl -i http://127.0.0.1:8080/health +curl -i https://example.com/health +``` + +## Useful systemd commands + +```bash +sudo systemctl start|stop|restart|status vix-myapp +sudo systemctl enable|disable vix-myapp +journalctl -u vix-myapp -f +``` + +## Useful Nginx commands + +```bash +sudo nginx -t +sudo systemctl reload nginx +sudo systemctl status nginx +ls /etc/nginx/sites-enabled +``` + +## Firewall + +```bash +sudo ufw allow OpenSSH +sudo ufw allow 80 +sudo ufw allow 443 +sudo ufw enable +``` + +## Database production notes + +**SQLite:** + +```dotenv +DATABASE_ENGINE=sqlite +DATABASE_DEFAULT_NAME=/home/vix/apps/myapp/data/app.db +``` + +**MySQL:** + +```sql +CREATE DATABASE myapp; +CREATE USER 'myapp'@'localhost' IDENTIFIED BY 'change-me'; +GRANT SELECT, INSERT, UPDATE, DELETE ON myapp.* TO 'myapp'@'localhost'; +FLUSH PRIVILEGES; +``` + +## Common production errors + +### 502 Bad Gateway + +Nginx cannot reach the Vix app. +Check: `sudo systemctl status vix-myapp` and `curl -i http://127.0.0.1:8080/health`. + +### 504 Gateway Timeout + +App accepted the connection but did not respond fast enough. +Check for slow database queries or overloaded VPS. + +### WebSocket closes immediately + +Add Nginx upgrade headers and longer timeouts. + +### App works locally but not through domain + +Check: `sudo nginx -t`, DNS records, firewall, TLS certificate. + +## Common mistakes + +### Running the app as root + +Always use a dedicated user: `User=vix`. + +### Forgetting the working directory + +```ini +WorkingDirectory=/home/vix/apps/myapp +``` + +Relative paths (`.env`, `public/`, `data/`) depend on this. + +### Using debug logs forever + +```dotenv +VIX_LOG_LEVEL=info +``` + +### Exposing P2P control routes without auth + +Routes like `POST /p2p/connect` must be protected or kept internal. + +## Deployment checklist + +- [ ] Release build works +- [ ] `.env` exists +- [ ] App listens on localhost +- [ ] Health route works locally +- [ ] systemd service starts +- [ ] systemd restarts after failure +- [ ] Logs visible with `journalctl` +- [ ] Nginx config passes `nginx -t` +- [ ] Domain points to server +- [ ] HTTPS enabled +- [ ] Certbot renewal works +- [ ] Database credentials not hardcoded +- [ ] Admin routes protected + +## Recommended production structure + +```txt +/home/vix/apps/myapp/ +├── build-release/ +│ └── myapp +├── data/ +│ └── app.db +├── public/ +├── src/ +├── .env +└── vix.json + +/etc/systemd/system/vix-myapp.service +/etc/nginx/sites-available/myapp +/etc/nginx/sites-enabled/myapp +``` + +## What you should remember + +A Vix production app is a normal native Linux service. + +```txt +browser → HTTPS → Nginx → Vix app on localhost → systemd +``` + +```bash +vix build --preset release # build +sudo systemctl start vix-myapp # run +sudo nginx -t && sudo systemctl reload nginx # expose +sudo certbot --nginx -d example.com # HTTPS +``` + +The core idea: **development uses `vix dev`, production runs a release binary.** + +## Next chapter + +[Next: Next steps](/book/21-next-steps) diff --git a/docs/book/21-next-steps.md b/docs/book/21-next-steps.md new file mode 100644 index 0000000..3c5a554 --- /dev/null +++ b/docs/book/21-next-steps.md @@ -0,0 +1,291 @@ +# Next steps + +You have reached the end of the Vix book. +You started with one simple idea: + +```txt +Run C++ code quickly. +``` + +Then you built the mental model step by step: + +```txt +CLI → Runtime → Application → Modules → Production +``` + +## What you now understand + +The main Vix workflow: + +```bash +vix run main.cpp +vix new api +vix dev +vix build +vix check +vix tests +``` + +The main application model: + +```cpp +#include +using namespace vix; + +int main() +{ + App app; + + app.get("/", [](Request &, Response &res){ + res.json({"message", "Hello from Vix"}); + }); + + app.run(8080); + return 0; +} +``` + +## The path you completed + +| Stage | What you learned | +|-------------|-------------------------------------------------------------------------| +| Start | What Vix is and why it exists. | +| CLI | Running files, creating projects, building, testing, and checking code. | +| HTTP | Building routes with `App`, `Request`, and `Response`. | +| APIs | Building JSON APIs. | +| Layers | Adding middleware, validation, errors, and logging. | +| Data | Using SQLite, MySQL, and database access. | +| Realtime | Using WebSocket and the async runtime. | +| Reliability | Using cache and offline-first synchronization. | +| Distributed | Building P2P features. | +| Production | Deploying with Nginx, systemd, TLS, logs, and health checks. | + +## What to build next + +The best next step is to build one real app from start to finish. + +**A good first real Vix app:** + +```txt +GET / +GET /health +GET /users +GET /users/{id} +POST /users +POST /auth/register +POST /auth/login +GET /auth/me +``` + +With: +- validation, +- SQLite storage, +- structured errors, +- logs, +- production deployment. + +## Recommended project + +```bash +vix new users-api +cd users-api +vix dev +``` + +Build step by step: + +1. Add `/health` +2. Add `/users` +3. Add JSON responses +4. Add validation +5. Add SQLite +6. Add structured errors +7. Add logs +8. Build release +9. Deploy behind Nginx and systemd + +## Use the Guides section + +The book teaches the story. +The guides help you solve specific problems: + +- [Build a REST API](/guides/build-rest-api) +- [Validation](/guides/validation) +- [Authentication](/guides/authentication) +- [Sessions](/guides/sessions) +- [CORS](/guides/cors) +- [Rate limiting](/guides/rate-limiting) +- [SQLite API](/guides/sqlite-api) +- [MySQL API](/guides/mysql-api) +- [WebSocket chat](/guides/websocket-chat) +- [Static files](/guides/static-files) +- [Templates](/guides/templates) +- [Production: Nginx + systemd](/guides/production-nginx-systemd) + +## Use the CLI reference + +When you need command details: + +```bash +vix run main.cpp --run --port 8080 # runtime args +vix run main.cpp -- -O2 -DNDEBUG # compiler flags +vix build --preset release +vix build --with-sqlite +``` + +Remember: `--` = compiler/linker flags, `--run` = runtime arguments to your program. + +## Learn by layers + +| Layer | What to learn | +|-------|--------------| +| 1 | `vix run main.cpp` — run C++ files | +| 2 | `App`, routes, `Request`, `Response` — HTTP APIs | +| 3 | Middleware, validation, errors, logging | +| 4 | SQLite or MySQL | +| 5 | WebSocket for realtime | +| 6 | Cache and sync for reliability | +| 7 | P2P for distributed behavior | +| 8 | Release build, systemd, Nginx, HTTPS | + +## Recommended learning order after the book + +1. Build a REST API +2. Add validation and structured errors +3. Add SQLite +4. Add authentication +5. Deploy with Nginx and systemd +6. Add WebSocket +7. Add cache +8. Add offline-first sync +9. Add P2P +10. Study internals and performance + +## Production checklist + +**App:** +- [ ] Health route exists +- [ ] Errors use consistent JSON shape +- [ ] Input is validated +- [ ] Logs are structured +- [ ] Secrets are not logged + +**Build:** +- [ ] Release build works +- [ ] Required flags enabled (`--with-sqlite`, `--with-mysql`) +- [ ] Dependencies installed + +**Runtime:** +- [ ] App runs as non-root user +- [ ] systemd restarts it +- [ ] Working directory is correct + +**Network:** +- [ ] App listens locally +- [ ] Nginx proxies public traffic +- [ ] HTTPS enabled +- [ ] WebSocket upgrade headers configured if needed + +**Data:** +- [ ] Database path is stable +- [ ] Credentials in environment +- [ ] Backups exist + +**Security:** +- [ ] Admin routes protected +- [ ] P2P control routes protected +- [ ] CORS configured correctly +- [ ] Rate limiting enabled where needed + +## What makes a good Vix application + +Keep `main()` small: + +```cpp +int main() +{ + config::Config cfg{".env"}; + App app; + configure_middlewares(app); + register_public_routes(app); + register_user_routes(app); + app.run(cfg.getServerPort()); + return 0; +} +``` + +Use predictable response shapes: + +```json +{ "ok": true, "data": {} } +{ "ok": true, "count": 2, "data": [] } +{ "ok": false, "error": "validation_failed", "message": "name is required" } +``` + +## When to use each runtime feature + +| Feature | Use when | +|--------------|----------------------------------------------------| +| `vix run` | You need to run one file quickly. | +| `vix new` | You want to start a real project. | +| HTTP | You are building APIs or web routes. | +| JSON | You need structured API responses. | +| Middleware | You need shared request behavior. | +| Validation | You are accepting user input. | +| Database | You need durable application state. | +| WebSocket | You need realtime client updates. | +| Async | You need timers, signals, or non-blocking I/O. | +| Cache | You need speed or stale data under failure. | +| Sync | You must not lose local operations. | +| P2P | You need nodes to discover, connect, or replicate. | +| Production | Your app must run as a service. | + +## A final example direction + +**Reliable Notes API:** + +```txt +POST /notes +GET /notes +GET /notes/{id} +PATCH /notes/{id} +DELETE /notes/{id} +GET /health +``` + +Grow it: +authentication → WebSocket updates → offline-first outbox → P2P sync → production deployment. + +This kind of project uses almost everything you learned. + +## What you should remember + +The full Vix path: + +```txt +one C++ file → Vix project → HTTP API → professional API layers +→ database → realtime → async runtime → cache → offline-first sync +→ P2P → production deployment +``` + +The most important command is still: + +```bash +vix run main.cpp +``` + +The most important production command is: + +```bash +vix build --preset release +``` + +The most important mental model is: + +```txt +Vix is a modern C++ runtime for building fast and reliable applications. +``` + +The final idea: **start simple, build progressively, deploy for real.** + +*End of the Vix Book. You are ready to build real applications with Vix. Choose one project and build it completely.* diff --git a/docs/cli/add.md b/docs/cli/add.md new file mode 100644 index 0000000..dbd8472 --- /dev/null +++ b/docs/cli/add.md @@ -0,0 +1,131 @@ +# vix add + +`vix add` adds a package dependency to your project. + +Use it when your project needs a package from the Vix registry. + +## Usage + +```bash +vix add [@]namespace/name[@version] +``` + +## What it does + +`vix add` resolves a package, installs it, and updates the project dependency files. It updates both `vix.json` with the declared dependency requirement and `vix.lock` with the exact resolved version. + +## Basic usage + +```bash +vix add gk/jwt +vix add gk/jwt@^1.0.0 +vix add @gk/jwt +vix add @gk/jwt@~1.2.0 +``` + +## Package name format +namespace/name → gk/jwt +@namespace/name → @gk/jwt + +## Version format + +```bash +vix add gk/jwt # latest version +vix add gk/jwt@1.0.0 # exact version +vix add gk/jwt@^1.0.0 # compatible range +vix add gk/jwt@~1.2.0 # patch-level range +vix add @gk/jwt@^1.0.0 # scoped with range +``` + +## What files are updated + +`vix add` updates two files. + +`vix.json` stores declared dependency requirements: + +```json +{ + "dependencies": { + "gk/jwt": "^1.0.0" + } +} +``` + +`vix.lock` stores exact resolved versions for reproducible installs. + +## After adding a dependency + +```bash +vix add gk/jwt +vix build +vix dev +``` + +Typical local outputs: `.vix/deps/`, `.vix/vix_deps.cmake` + +## Registry sync + +If a package is not found, refresh the local registry index: + +```bash +vix registry sync +vix add gk/jwt +``` + +## Common workflows + +```bash +# Add latest package +vix add gk/jwt + +# Add a package with compatible updates +vix add gk/jwt@^1.0.0 + +# Add a scoped package +vix add @gk/jwt + +# Refresh registry before adding +vix registry sync +vix add gk/jwt + +# Add and validate +vix add gk/jwt +vix check --tests +``` + +## Common mistakes + +### Forgetting to sync the registry + +```bash +vix registry sync +vix add gk/jwt +``` + +### Confusing `vix add` and `vix install` + +| Command | Purpose | +|---------|---------| +| `vix add` | Add or change a dependency | +| `vix install` | Install exact locked dependencies | + +### Expecting `vix add` to update all packages + +Use `vix update` to update existing dependencies. + +## Related commands + +| Command | Purpose | +|---------|---------| +| `vix install` | Install dependencies from `vix.lock` | +| `vix update` | Update dependencies | +| `vix outdated` | Check outdated dependencies | +| `vix remove` | Remove dependencies | +| `vix list` | List dependencies | +| `vix registry sync` | Refresh local registry index | + +## Next step + +Continue with dependency installation. + +[Open the vix install guide](/cli/install) diff --git a/docs/cli/build.md b/docs/cli/build.md new file mode 100644 index 0000000..827171b --- /dev/null +++ b/docs/cli/build.md @@ -0,0 +1,982 @@ +# vix build + +`vix build` configures and builds a CMake project using Vix presets. + +Use it when you want to compile a project without running the application. + +```bash +vix build +``` + +## Overview + +`vix build` gives C++ projects a clean and fast build workflow. + +It detects the current project, configures CMake, builds with Ninja, uses Vix presets, supports parallel builds, enables optional features such as SQLite or MySQL, uses compiler launchers such as ccache or sccache, can use fast linkers such as mold or lld, and writes build logs automatically. + +By default, `vix build` builds the main project target, not the full `all` target. + +This keeps normal development builds fast. + +```bash +vix build +``` + +For a full build of every target, use: + +```bash +vix build --build-target all +``` + +## Usage + +```bash +vix build [options] -- [cmake args...] +``` + +## Basic usage + +```bash +# Build the current project +vix build + +# Build with a detailed Vix summary +vix build -v + +# Build with a specific number of jobs +vix build -j 8 + +# Build from another directory +vix build --dir ./examples/blog + +# Build a release version +vix build --preset release +``` + +## What vix build does + +When you run: + +```bash +vix build +``` + +Vix performs the following steps: + +1. Detect the project directory +2. Select a build preset +3. Prepare the build directory +4. Generate CMake configuration when needed +5. Build the main project target +6. Write configure and build logs +7. Store build metadata for faster future decisions + +The normal output is intentionally compact: + +``` + build [============================] done + ✔ Built + ✔ Done in 0.1s +``` + +With verbose output: + +```bash +vix build -v +``` + +you get a clearer build summary: + +``` +Configuring project-name (dev) + ✔ Configured in 0.5s + +Compiling project-name (dev) + * launcher: ccache | linker: mold | jobs: 8 + build [============================] done + ✔ Finished dev [unoptimized + debuginfo] in 10.6s +``` + +## Build target behavior + +By default, `vix build` builds the main target of the project. + +The default target name is the project directory name. + +For example, inside: + +``` +~/vixcpp/vix +``` + +this command: + +```bash +vix build +``` + +builds: + +``` +vix +``` + +not: + +``` +all +``` + +This avoids rebuilding examples, tests, and auxiliary executables during normal development. + +### Build the main target + +```bash +vix build +``` + +### Build a specific target + +```bash +vix build --build-target vix +vix build --build-target project +vix build --build-target my_app +``` + +### Build everything + +```bash +vix build --build-target all +``` + +Use `--build-target all` before install or release workflows when CMake install rules require extra binaries to exist. + +Example: + +```bash +vix build --build-target all +sudo cmake --install build-ninja --prefix /usr/local +``` + +## Presets + +Vix provides embedded presets. + +| Preset | Generator | Build type | Build directory | +|--------|-----------|------------|-----------------| +| `dev` | Ninja | Debug | `build-dev` | +| `dev-ninja` | Ninja | Debug | `build-ninja` | +| `release` | Ninja | Release | `build-release` | + +The default preset is `dev-ninja`. + +Examples: + +```bash +# Use the default development preset +vix build + +# Use the dev preset +vix build --preset dev + +# Use the release preset +vix build --preset release +``` + +### Development build + +Use the default build for daily work: + +```bash +vix build +``` + +This uses: + +- preset: `dev-ninja` +- build type: `Debug` +- generator: `Ninja` +- build dir: `build-ninja` + +### Release build + +Use `--preset release` for optimized builds: + +```bash +vix build --preset release +``` + +This uses: + +- build type: `Release` +- build dir: `build-release` + +You can combine it with other options: + +```bash +vix build --preset release --with-sqlite +vix build --preset release --static +vix build --preset release --launcher sccache --linker mold +``` + +## Verbose output + +Use `-v` or `--verbose` to show a clean summary of the configure and build phases: + +```bash +vix build -v +``` + +This shows useful information such as: + +``` +Configuring project (dev) +Compiling project (dev) +launcher: ccache +linker: mold +jobs: 8 +``` + +It does not flood the terminal with raw CMake or Ninja logs. + +## Raw CMake and Ninja output + +Use `--cmake-verbose` when you need the raw CMake or Ninja output: + +```bash +vix build --cmake-verbose +``` + +Use this when debugging CMake itself, generator behavior, linker commands, or full build output. + +Normal `-v` is for human-readable Vix output. `--cmake-verbose` is for raw low-level output. + +## Quiet output + +Use `--quiet` to reduce output: + +```bash +vix build --quiet +``` + +This is useful in scripts when you only need the exit code. + +## Build logs + +`vix build` writes logs into the build directory. + +Common log files: + +``` +build-ninja/configure.log +build-ninja/build.log +build-dev/configure.log +build-dev/build.log +build-release/configure.log +build-release/build.log +``` + +Use these logs when you need the full CMake, Ninja, compiler, or linker output. + +Example: + +```bash +cat build-ninja/build.log +``` + +## Build progress + +During a build, Vix shows compact progress: + +``` + build [============----------------] 20/45 + › Building CXX object CMakeFiles/project.dir/src/http/RequestContext.cpp.o +``` + +At the end, Vix keeps the output readable: + +``` + build [============================] done + ✔ Built + ✔ Done in 10.1s +``` + +If the build fails, Vix hides raw compiler or linker noise and prints a focused diagnostic. + +Example: + +``` + ✖ Link failed + + message: + Referenced by: BuildCommand.cpp + + error: + undefined symbol: vix::cli::build::print_build_header_full(...) + + hint: + The symbol is declared and used, but no linked object or library provides its definition. +``` + +The raw build output is still available in: + +``` +build-ninja/build.log +``` + +## Parallel builds + +Use `-j` or `--jobs` to control parallelism: + +```bash +vix build -j 8 +vix build --jobs 16 +``` + +If no job count is provided, Vix uses the machine CPU count, clamped internally to avoid excessive values. + +## Compiler launcher + +Vix can use sccache or ccache to speed up repeated builds. + +```bash +vix build --launcher auto +vix build --launcher sccache +vix build --launcher ccache +vix build --launcher none +``` + +| Mode | Description | +|------|-------------| +| `auto` | Prefer sccache, then ccache when available | +| `sccache` | Use sccache if available | +| `ccache` | Use ccache if available | +| `none` | Disable compiler launcher | + +Example: + +```bash +vix build --launcher ccache +``` + +## Linker selection + +Vix can use faster linkers when available. + +```bash +vix build --linker auto +vix build --linker mold +vix build --linker lld +vix build --linker default +``` + +| Mode | Description | +|------|-------------| +| `auto` | Prefer mold, then lld when available | +| `mold` | Use mold | +| `lld` | Use lld | +| `default` | Use the system default linker | + +Example: + +```bash +vix build --linker mold +``` + +## SQLite support + +Use `--with-sqlite` to enable SQLite-related build options: + +```bash +vix build --with-sqlite +``` + +Release example: + +```bash +vix build --preset release --with-sqlite +``` + +This maps to CMake options such as: + +``` +VIX_ENABLE_DB=ON +VIX_DB_USE_SQLITE=ON +VIX_DB_REQUIRE_SQLITE=ON +``` + +## MySQL support + +Use `--with-mysql` to enable MySQL-related build options: + +```bash +vix build --with-mysql +``` + +Release example: + +```bash +vix build --preset release --with-mysql +``` + +This maps to CMake options such as: + +``` +VIX_ENABLE_DB=ON +VIX_DB_USE_MYSQL=ON +VIX_DB_REQUIRE_MYSQL=ON +``` + +## Static linking + +Use `--static` to request static linking: + +```bash +vix build --static +``` + +Release example: + +```bash +vix build --preset release --static +``` + +This maps to: + +``` +VIX_LINK_STATIC=ON +``` + +Static linking depends on your platform and available static libraries. + +## Clean build + +Use `--clean` to remove local build directories and reconfigure from scratch: + +```bash +vix build --clean +``` + +This removes local build directories such as: + +``` +build-dev +build-ninja +build-release +``` + +Then it configures and builds again. + +Use this when: + +- CMake cache is stale +- the build directory is broken +- toolchain settings changed +- you want a fresh rebuild + +## Cache behavior + +Vix uses build metadata and signatures to avoid unnecessary setup work. +Normal builds still let Ninja decide whether source files must be recompiled. +This is important because Ninja remains the source of truth for file-level incremental compilation. + +Use `--no-cache` to disable Vix cache shortcuts: + +```bash +vix build --no-cache +``` + +## Fast no-op builds + +Use `--fast` to let Vix check whether Ninja reports the project as up to date: + +```bash +vix build --fast +``` + +If Ninja reports no work, Vix can exit quickly. +Use this in tight loops or CI checks where a no-op build should be as fast as possible. +You can disable Ninja dry-run up-to-date detection with: + +```bash +vix build --no-up-to-date +``` + +## Status output + +Vix sets Ninja status output automatically when possible. + +Disable it with: + +```bash +vix build --no-status +``` + +Use this if you want less progress output or if your environment does not handle terminal progress well. + +## Build heartbeat + +In CI systems, long builds with no output may look stuck. +Enable heartbeat output with: + +```bash +VIX_BUILD_HEARTBEAT=1 vix build +``` + +This prints a heartbeat when the build is silent for several seconds. + +## Debug build details + +Normal verbose output hides internal details such as graph state, artifact cache paths, build state paths, and CMake variables. + +To show internal build details, use: + +```bash +VIX_LOG_LEVEL=debug vix build -v +``` + +or: + +```bash +VIX_LOG_LEVEL=trace vix build -v +``` + +This is useful when debugging Vix itself. + +## Cross-compilation + +Use `--target` to cross-compile: + +```bash +vix build --target aarch64-linux-gnu +``` + +Release cross-build: + +```bash +vix build --preset release --target aarch64-linux-gnu +``` + +With sysroot: + +```bash +vix build --target aarch64-linux-gnu --sysroot /opt/sysroots/aarch64 +``` + +Vix generates a CMake toolchain file in the build directory. +The expected compiler tools follow the target triple: + +``` +aarch64-linux-gnu-gcc +aarch64-linux-gnu-g++ +aarch64-linux-gnu-ar +aarch64-linux-gnu-ranlib +aarch64-linux-gnu-strip +``` + +List detected cross toolchains: + +```bash +vix build --targets +``` + +--- + +## Forward CMake arguments + +Use `--` to pass raw arguments to CMake: + +```bash +vix build -- -DVIX_SYNC_BUILD_TESTS=ON +``` + +Another example: + +```bash +vix build --preset release -- -DCMAKE_EXPORT_COMPILE_COMMANDS=ON +``` + +Everything after `--` is forwarded to CMake configuration. + +## Build from another directory + +Use `--dir` or `-d`: + +```bash +vix build --dir ./examples/blog +vix build -d ./examples/blog +``` + +This is useful when you are outside the project root. + +## Export the built binary + +Use `--bin` to export the built executable to the project root: + +```bash +vix build --bin +``` + +Use `--out` to export it to a specific path: + +```bash +vix build --out ./dist/my_app +``` + +`--bin` and `--out` cannot be used together. + +## Single C++ file build + +`vix build` can also build one C++ source file: + +```bash +vix build main.cpp +``` + +This builds the file and exports the produced executable. + +For running a single file directly, use: + +```bash +vix run main.cpp +``` + +## Install workflow + +Because `vix build` builds the main target by default, install workflows should build all required install targets first: + +```bash +vix build --build-target all +sudo cmake --install build-ninja --prefix /usr/local +``` + +If you only run: + +```bash +vix build +sudo cmake --install build-ninja --prefix /usr/local +``` + +CMake install may fail if an install rule expects a binary that was not built by the main target. + +## Options + +| Option | Description | +|--------|-------------| +| `--preset ` | Preset to use: `dev`, `dev-ninja`, or `release`. | +| `--target ` | Cross-compilation target triple. | +| `--sysroot ` | Sysroot for cross toolchain. | +| `--static` | Request static linking. | +| `--with-sqlite` | Enable SQLite support. | +| `--with-mysql` | Enable MySQL support. | +| `-j, --jobs ` | Number of parallel build jobs. | +| `--clean` | Remove local build directories and reconfigure from scratch. | +| `--no-cache` | Disable Vix build cache shortcuts. | +| `--fast` | Exit quickly if Ninja says the build is up to date. | +| `--linker ` | Linker mode: `auto`, `default`, `mold`, or `lld`. | +| `--launcher ` | Compiler launcher mode: `auto`, `none`, `sccache`, or `ccache`. | +| `--no-status` | Disable Ninja status progress format. | +| `--no-up-to-date` | Disable Ninja dry-run up-to-date detection. | +| `-d, --dir ` | Project directory. | +| `-q, --quiet` | Minimal output. | +| `-v, --verbose` | Show detailed Vix configure and build summary. | +| `--targets` | List detected cross toolchains on PATH. | +| `--cmake-verbose` | Show raw CMake and Ninja output. | +| `--build-target ` | Build a specific CMake target. Default is the project directory name. | +| `--bin` | Export the built executable to the project root. | +| `--out ` | Export the built executable to a specific path. | +| `-h, --help` | Show command help. | + +## Environment variables + +| Variable | Description | +|----------|-------------| +| `VIX_BUILD_HEARTBEAT=1` | Enable heartbeat when the build is silent for several seconds. | +| `VIX_LOG_LEVEL=debug` | Show internal build graph, cache, state, and CMake variable details. | +| `VIX_LOG_LEVEL=trace` | Show deeper internal build details. | +| `VIX_GRAPH_EXECUTOR=1` | Enable the experimental target-aware graph executor. | + +## Experimental graph executor + +Vix contains an internal build graph foundation. + +It can import: + +- `compile_commands.json` +- `build.ninja` +- dependency files +- object metadata + +It is used to prepare future target-aware incremental builds. +The experimental graph executor is not enabled by default. + +Enable it with: + +```bash +VIX_GRAPH_EXECUTOR=1 vix build -v +``` + +For normal projects, the stable default path remains CMake/Ninja execution. + +## Common workflows + +### Normal development build + +```bash +vix build +``` + +### Verbose development build + +```bash +vix build -v +``` + +### Release build + +```bash +vix build --preset release +``` + +### Full repository build + +```bash +vix build --build-target all +``` + +### Build one target + +```bash +vix build --build-target project +``` + +### Clean rebuild + +```bash +vix build --clean +``` + +### Fast no-op check + +```bash +vix build --fast +``` + +### Use mold + +```bash +vix build --linker mold +``` + +### Use ccache + +```bash +vix build --launcher ccache +``` + +### Release with SQLite + +```bash +vix build --preset release --with-sqlite +``` + +### Release with MySQL + +```bash +vix build --preset release --with-mysql +``` + +### Build and install + +```bash +vix build --build-target all +sudo cmake --install build-ninja --prefix /usr/local +``` + +## Common mistakes + +### Expecting vix build to run the app + +`vix build` only builds the project. It does not start the application. + +Use: + +```bash +vix run +``` + +or: + +```bash +vix dev +``` + +### Forgetting to enter the project directory + +Wrong: + +```bash +vix new api +vix build +``` + +Correct: + +```bash +vix new api +cd api +vix build +``` + +### Expecting vix build to build every target + +By default, `vix build` builds the main project target. + +Use this for all targets: + +```bash +vix build --build-target all +``` + +### Installing after only building the main target + +Wrong: + +```bash +vix build +sudo cmake --install build-ninja --prefix /usr/local +``` + +Correct: + +```bash +vix build --build-target all +sudo cmake --install build-ninja --prefix /usr/local +``` + +### Passing runtime arguments to vix build + +`vix build` does not run the program. + +Wrong: + +```bash +vix build --port 8080 +``` + +Correct: + +```bash +vix run --run --port 8080 +``` + +### Using -v when you need raw CMake logs + +`-v` shows a readable Vix summary. + +For raw CMake and Ninja logs, use: + +```bash +vix build --cmake-verbose +``` + +### Expecting --clean to remove global caches + +`--clean` removes local build directories. + +For broader cleanup, use commands such as: + +```bash +vix clean +vix reset +``` + +depending on what you want to remove. + +## Troubleshooting + +### Build says a target is missing + +Use: + +```bash +vix build --build-target all +``` + +or check available CMake targets: + +```bash +cmake --build build-ninja --target help +``` + +### Install fails because a binary is missing + +Build all install-related targets first: + +```bash +vix build --build-target all +sudo cmake --install build-ninja --prefix /usr/local +``` + +### CMake cache is stale + +Use: + +```bash +vix build --clean +``` + +### You need the raw linker command + +Use: + +```bash +vix build --cmake-verbose +``` + +or inspect: + +```bash +cat build-ninja/build.log +``` + +### You need internal Vix build details + +Use: + +```bash +VIX_LOG_LEVEL=debug vix build -v +``` + +## When to use vix build + +Use `vix build` when you want to: + +- compile a project +- verify that a project builds +- produce a release binary +- build a specific CMake target +- use a fast C++ development loop +- inspect build errors without running the app +- prepare an install or packaging workflow + +Do not use `vix build` when you want to run the app. Use `vix run` or `vix dev` instead. + +## Related commands + +| Command | Purpose | +|---------|---------| +| `vix run` | Build and run the app | +| `vix dev` | Run the app with reload | +| `vix check` | Validate build, tests, runtime, and sanitizers | +| `vix tests` | Run tests | +| `vix clean` | Remove local project cache directories | +| `vix reset` | Clean and reinstall project dependencies | + +## Next step + +Continue with validation. + +[Open the vix check guide](/cli/check) + + diff --git a/docs/cli/cache.md b/docs/cli/cache.md new file mode 100644 index 0000000..68a3b94 --- /dev/null +++ b/docs/cli/cache.md @@ -0,0 +1,130 @@ +# vix cache + +`vix cache` stores a Vix package locally so it can be reused quickly. + +Use it when you already have a package folder or `.vixpkg` artifact and want to place it in the local Vix cache. + +## Usage + +```bash +vix cache --path [options] +``` + +## What it does + +`vix cache` takes an existing Vix package and stores it locally. It verifies the package before storing by default, and supports signature verification and custom store locations. + +## Basic usage + +```bash +vix cache --path ./dist/blog@1.0.0.vixpkg +vix cache --path ./dist/blog@1.0.0 +vix cache --path ./dist/blog@1.0.0 --force +``` + +## Verification + +By default, `vix cache` verifies the package before storing. Use `--no-verify` to skip: + +```bash +vix cache --path ./dist/blog@1.0.0.vixpkg --no-verify +``` + +For safer workflows, prefer: + +```bash +vix verify --path ./dist/blog@1.0.0.vixpkg +vix cache --path ./dist/blog@1.0.0.vixpkg +``` + +## Signature and public key + +```bash +vix cache \ + --path ./dist/blog@1.0.0.vixpkg \ + --require-signature \ + --pubkey ./keys/vix-pack.pub +``` + +## Custom store location + +```bash +vix cache \ + --path ./dist/blog@1.0.0.vixpkg \ + --store ./local-store +``` + +## Options + +| Option | Description | +|--------|-------------| +| `-p, --path ` | Package folder or `.vixpkg` artifact. Required. | +| `--store ` | Custom store location. | +| `--force` | Overwrite if already cached. | +| `--no-verify` | Skip verification. Not recommended for release workflows. | +| `--verbose` | Show detailed operations. | +| `--require-signature` | Require a valid signature. | +| `--pubkey ` | Minisign public key. | +| `-h, --help` | Show command help. | + +## Common workflows + +```bash +# Pack, verify, and cache +vix pack --name blog --version 1.0.0 +vix verify --path ./dist/blog@1.0.0 +vix cache --path ./dist/blog@1.0.0 + +# Cache a .vixpkg +vix cache --path ./dist/blog@1.0.0.vixpkg + +# Cache with overwrite +vix cache --path ./dist/blog@1.0.0 --force + +# Cache with required signature +vix cache \ + --path ./dist/blog@1.0.0.vixpkg \ + --require-signature \ + --pubkey ./keys/vix-pack.pub + +# CI usage +vix build --preset release +vix check --tests +vix pack --name blog --version 1.0.0 +vix verify --path ./dist/blog@1.0.0 +vix cache --path ./dist/blog@1.0.0 +``` + +## Common mistakes + +### Forgetting `--path` + +```bash +# Wrong +vix cache + +# Correct +vix cache --path ./dist/blog@1.0.0.vixpkg +``` + +### Caching before packing + +```bash +vix pack --name blog --version 1.0.0 +vix cache --path ./dist/blog@1.0.0 +``` + +## Related commands + +| Command | Purpose | +|---------|---------| +| `vix pack` | Create a package artifact | +| `vix verify` | Verify a package artifact | +| `vix store` | Manage the local package store | +| `vix build` | Build before packing | + +## Next step + +Continue with registry management. + +[Open the vix registry guide](/cli/registry) diff --git a/docs/cli/check.md b/docs/cli/check.md new file mode 100644 index 0000000..b22f033 --- /dev/null +++ b/docs/cli/check.md @@ -0,0 +1,192 @@ +# vix check + +`vix check` validates a Vix project or a single C++ file. + +Use it when you want to verify that code builds correctly, tests pass, runtime execution works, and sanitizer checks are clean. + +## Usage + +```bash +vix check [path|file.cpp] [options] +``` + +## What it does + +`vix check` is the validation command of the Vix CLI. It can detect a Vix or CMake project, configure the project, build the selected check profile, run tests with CTest, run the built executable, compile a single `.cpp` file, run sanitizer checks, validate runtime behavior, and use isolated build directories for check profiles. + +## Basic usage + +```bash +# Check the current project +vix check + +# Check a project in another directory +vix check --dir ./examples/api + +# Check a single C++ file +vix check main.cpp +``` + +## Project mode + +In project mode, Vix can detect the project root, configure the selected preset, build the project, optionally run tests, optionally run the built executable, and optionally enable sanitizers. + +## Script mode + +Script mode is used when the input is a single `.cpp` file: + +```bash +vix check main.cpp +``` + +Vix creates a temporary CMake project around the file, compiles it, and validates the result. With sanitizers enabled, Vix also runs the binary for runtime validation: + +```bash +vix check main.cpp --san +``` + +## Run tests + +```bash +vix check --tests +vix check --tests --run +vix check --san --tests +``` + +## Run the executable + +```bash +vix check --run +vix check --run --run-timeout 20 +``` + +## Sanitizer checks + +```bash +# AddressSanitizer + UndefinedBehaviorSanitizer +vix check --san +vix check main.cpp --san + +# UndefinedBehaviorSanitizer only +vix check --ubsan +vix check main.cpp --ubsan +``` + +## Smart sanitizer mode + +By default, sanitizer mode is smart. For small projects, Vix checks the full project normally. For large or umbrella projects, Vix may switch to a reduced sanitizer configure to avoid unrelated failures. + +## Full sanitizer mode + +Use `--full` when you want the complete sanitizer configure: + +```bash +vix check --san --full +``` + +Use it when preparing releases or debugging deeper build problems. + +## Build presets + +```bash +vix check --preset dev-ninja +vix check --build-preset build-ninja +``` + +## Parallel jobs + +```bash +vix check -j 8 +``` + +## CTest options + +```bash +vix check --tests --ctest-preset test-dev +vix check --tests --ctest-arg --output-on-failure +vix check --tests \ + --ctest-arg --output-on-failure \ + --ctest-arg -R \ + --ctest-arg Auth +``` + +## Options + +| Option | Description | +|--------|-------------| +| `-d, --dir ` | Explicit project directory. | +| `--preset ` | Configure preset. Default is `dev-ninja`. | +| `--build-preset ` | Build preset override. | +| `-j, --jobs ` | Number of parallel build jobs. | +| `--tests` | Run CTest after build. | +| `--ctest-preset ` | CTest preset override. | +| `--ctest-arg ` | Extra argument forwarded to CTest. Repeatable. | +| `--run` | Run the built executable after build. | +| `--run-timeout ` | Runtime timeout in seconds. | +| `-q, --quiet` | Minimal output. | +| `--verbose` | More verbose logging. | +| `--log-level ` | Set `VIX_LOG_LEVEL` for the check session. | +| `--san` | Enable AddressSanitizer and UndefinedBehaviorSanitizer. | +| `--ubsan` | Enable UndefinedBehaviorSanitizer only. | +| `--full` | Force the complete sanitizer check. | +| `-h, --help` | Show command help. | + +## Build directories + +Project checks use isolated build directories per profile: `build-ninja`, `build-ninja-san`, `build-ninja-ubsan`. + +## Common workflows + +```bash +# Basic project validation +vix check + +# Build and run tests +vix check --tests + +# Build and run the executable +vix check --run + +# Full validation +vix check --tests --run + +# Sanitizer validation +vix check --san + +# Sanitizers with tests +vix check --san --tests + +# Sanitizers with tests and runtime timeout +vix check --san --tests --run-timeout 20 + +# Strict full sanitizer check +vix check --san --full + +# Single file validation +vix check main.cpp + +# Single file sanitizer validation +vix check main.cpp --san +``` + +## Common mistakes + +### Using `--full` for every local check + +For daily work, prefer `vix check --san`. Use `--full` only when you intentionally want a complete project-level sanitizer configure. + +## Related commands + +| Command | Purpose | +|---------|---------| +| `vix build` | Configure and compile the project | +| `vix run` | Build and run the app | +| `vix dev` | Run the app with reload | +| `vix tests` | Run project tests | +| `vix fmt` | Format source files | + +## Next step + +Continue with tests. + +[Open the vix tests guide](/cli/tests) diff --git a/docs/cli/clean.md b/docs/cli/clean.md new file mode 100644 index 0000000..9668be3 --- /dev/null +++ b/docs/cli/clean.md @@ -0,0 +1,105 @@ +# vix clean + +`vix clean` removes local project cache directories. + +Use it when you want to clean generated project state without deleting the global Vix cache. + +## Usage + +```bash +vix clean +``` + +## What it does + +`vix clean` removes local build and cache directories from the current project — specifically `.vix/` and `build/`. + +## What it does not remove + +`vix clean` does not remove the global Vix directory (`~/.vix/`). Global packages, registry data, and global cache data are kept. + +## Basic usage + +```bash +cd api +vix clean +``` + +## Common workflow + +```bash +vix clean +vix install +vix build +``` + +For this complete workflow in one command, use `vix reset`. + +## Difference between `vix clean` and `vix reset` + +| Command | What it does | +|---------|-------------| +| `vix clean` | Removes `.vix/` and `build/` | +| `vix reset` | Runs `vix clean`, then `vix install` | + +## Local files affected + +Common affected paths: `.vix/`, `build/`, `build-ninja/`, `build-release/`, `.vix/deps/`, `.vix/vix_deps.cmake`. + +## Common workflows + +```bash +# Clean after dependency issues +vix clean +vix install +vix build + +# Clean before a fresh dev run +vix clean +vix install +vix dev + +# Clean before validation +vix clean +vix install +vix check --tests +``` + +## Common mistakes + +### Expecting `vix clean` to remove global cache + +`vix clean` does not remove `~/.vix/`. For global store issues, use `vix store`, `vix registry`, or `vix info`. + +### Forgetting to reinstall dependencies + +After cleaning, run `vix install` before building. + +### Running outside the project directory + +```bash +# Wrong +cd .. +vix clean + +# Correct +cd api +vix clean +``` + +## Related commands + +| Command | Purpose | +|---------|---------| +| `vix reset` | Clean and reinstall project dependencies | +| `vix install` | Install dependencies from `vix.lock` | +| `vix build` | Configure and build project | +| `vix check` | Validate project | +| `vix info` | Show Vix paths and cache locations | +| `vix store` | Manage the local package store | + +## Next step + +Continue with project reset. + +[Open the vix reset guide](/cli/reset) diff --git a/docs/cli/commands.md b/docs/cli/commands.md new file mode 100644 index 0000000..add2bf0 --- /dev/null +++ b/docs/cli/commands.md @@ -0,0 +1,295 @@ +# CLI Commands + +This page is a complete overview of the Vix CLI command surface. + +Use it when you want to quickly find the purpose of a command, its category, and the next detailed page to read. + +## Command overview + +```bash +vix [options] +``` + +Running `vix` without a command starts the interactive REPL: + +```bash +vix +``` + +Show global help: + +```bash +vix --help +``` + +Show the installed version: + +```bash +vix --version +``` + +## Core workflow + +Most Vix projects start with this flow: + +```bash +vix new api +cd api +vix install +vix dev +``` + +For an existing project: + +```bash +vix install +vix dev +``` + +For a single C++ file: + +```bash +vix run main.cpp +``` + +## Project commands + +| Command | Purpose | Guide | +|---------|---------|-------| +| `vix` | Start the interactive REPL | [REPL](/cli/repl) | +| `vix repl` | Start the REPL explicitly | [REPL](/cli/repl) | +| `vix new ` | Create a new Vix project | [vix new](/cli/new) | +| `vix make` | Generate C++ scaffolding | [vix make](/cli/make) | +| `vix dev` | Run an app in development mode with reload | [vix dev](/cli/dev) | +| `vix run` | Build and run a project, file, or manifest | [vix run](/cli/run) | +| `vix build` | Configure and build a CMake project | [vix build](/cli/build) | +| `vix check` | Validate build, tests, runtime, and sanitizers | [vix check](/cli/check) | +| `vix tests` | Run project tests | [vix tests](/cli/tests) | +| `vix fmt` | Format C++ source files | [vix fmt](/cli/fmt) | +| `vix clean` | Remove local project cache directories | [vix clean](/cli/clean) | +| `vix reset` | Clean and reinstall the project | [vix reset](/cli/reset) | +| `vix task` | Run reusable project tasks | [vix task](/cli/task) | + +## Dependency commands + +| Command | Purpose | Guide | +|---------|---------|-------| +| `vix add ` | Add a package to the project | [vix add](/cli/add) | +| `vix install` | Install dependencies from `vix.lock` | [vix install](/cli/install) | +| `vix update` | Update dependencies | [vix update](/cli/update) | +| `vix outdated` | Check outdated dependencies | [vix outdated](/cli/outdated) | +| `vix remove ` | Remove a dependency | [vix remove](/cli/remove) | +| `vix list` | List project or global packages | [vix list](/cli/list) | + +### Dependency aliases + +| Alias | Equivalent command | +|-------|--------------------| +| `vix up` | `vix update` | +| `vix i` | `vix install` | +| `vix deps` | `vix install` | + +## Packaging commands + +| Command | Purpose | Guide | +|---------|---------|-------| +| `vix pack` | Build a distributable package | [vix pack](/cli/pack) | +| `vix verify` | Verify package integrity | [vix verify](/cli/verify) | +| `vix cache` | Store a package locally | [vix cache](/cli/cache) | + +## Advanced commands + +| Command | Purpose | Guide | +|---------|---------|-------| +| `vix registry` | Manage the local registry index | [vix registry](/cli/registry) | +| `vix store` | Manage the local package store | [vix store](/cli/store) | +| `vix orm` | Run database migration tooling | [vix orm](/cli/orm) | +| `vix p2p` | Run a peer-to-peer node | [vix p2p](/cli/p2p) | + +## System commands + +| Command | Purpose | Guide | +|---------|---------|-------| +| `vix info` | Show Vix paths, caches, and local state | [vix info](/cli/info) | +| `vix doctor` | Check the local environment | [vix doctor](/cli/doctor) | +| `vix upgrade` | Upgrade Vix or a global package | [vix upgrade](/cli/upgrade) | +| `vix uninstall` | Remove Vix or a global package | [vix uninstall](/cli/uninstall) | +| `vix completion` | Generate shell completion scripts | [vix completion](/cli/completion) | +| `vix version` | Show the installed version | — | + +## Help commands + +| Command | Purpose | +|---------|---------| +| `vix help` | Show general help | +| `vix help ` | Show help for a specific command | +| `vix --help` | Show help for a specific command | +| `vix -h` | Show general help | +| `vix --help` | Show general help | + +Examples: + +```bash +vix help run +vix run --help +vix new --help +``` + +## Global options + +| Option | Purpose | +|--------|---------| +| `-h, --help` | Show help | +| `-v, --version` | Show version | +| `--verbose` | Enable debug logs | +| `-q, --quiet` | Show only warnings and errors | +| `--log-level ` | Set log level | + +Supported log levels: `trace`, `debug`, `info`, `warn`, `error`, `critical`, `off` + +## Project lifecycle map + +| Stage | Command | +|-------|---------| +| Create project | `vix new` | +| Install dependencies | `vix install` | +| Develop | `vix dev` | +| Run manually | `vix run` | +| Validate | `vix check` | +| Test | `vix tests` | +| Format | `vix fmt` | +| Build | `vix build` | +| Package | `vix pack` | +| Verify artifact | `vix verify` | +| Cache artifact | `vix cache` | + +## New project workflow + +```bash +vix new api +cd api +vix install +vix dev +``` + +Validate before committing: + +```bash +vix fmt --check +vix check --tests +``` + +Build for release: + +```bash +vix build --preset release +vix pack --name api --version 1.0.0 +vix verify --path ./dist/api@1.0.0 +``` + +## Existing project workflow + +```bash +git clone https://github.com/example/api.git +cd api +vix install +vix dev +vix check --tests +``` + +## Single-file workflow + +```bash +vix run main.cpp +vix run main.cpp --run --port 8080 +vix run main.cpp -- -O2 -DNDEBUG +vix run main.cpp --san +vix check main.cpp --san +``` + +## Choosing the right command + +| Goal | Use | +|------|-----| +| Explore interactively | `vix` | +| Create a project | `vix new` | +| Generate a class or file | `vix make` | +| Run during development | `vix dev` | +| Run manually | `vix run` | +| Compile only | `vix build` | +| Validate deeply | `vix check` | +| Run tests only | `vix tests` | +| Format source files | `vix fmt` | +| Add dependencies | `vix add` | +| Install dependencies | `vix install` | +| Update dependencies | `vix update` | +| Package project | `vix pack` | +| Verify package | `vix verify` | +| Inspect environment | `vix info` | +| Diagnose setup | `vix doctor` | + +## Common mistakes + +### Passing runtime arguments after `--` in script mode + +Wrong: + +```bash +vix run main.cpp -- --port 8080 +``` + +Correct: + +```bash +vix run main.cpp --run --port 8080 +``` + +### Running project commands outside the project directory + +Wrong: + +```bash +vix new api +vix dev +``` + +Correct: + +```bash +vix new api +cd api +vix dev +``` + +### Forgetting to install dependencies + +For a fresh or cloned project, run `vix install` before `vix dev`. + +## Recommended daily workflow + +```bash +# Development +vix dev + +# In another terminal +vix tests --watch + +# Before committing +vix fmt --check +vix check --tests + +# Before release +vix build --preset release +vix tests --preset release +vix check --san --full +vix pack --name api --version 1.0.0 +vix verify --path ./dist/api@1.0.0 +``` + +## Version + +```bash +vix --version +``` + +Example output: diff --git a/docs/cli/completion.md b/docs/cli/completion.md new file mode 100644 index 0000000..c9f3425 --- /dev/null +++ b/docs/cli/completion.md @@ -0,0 +1,121 @@ +# vix completion + +`vix completion` generates a shell completion script. + +Use it when you want your shell to autocomplete Vix commands, subcommands, and options. + +## Usage + +```bash +vix completion [bash] +``` + +## What it does + +`vix completion` prints a shell completion script. You redirect that output into a file, then source the file from your shell. + +## Basic usage + +```bash +vix completion bash > ~/.vix-completion.bash +source ~/.vix-completion.bash +``` + +## Recommended persistent Bash setup + +```bash +vix completion bash > ~/.vix-completion.bash +echo 'source ~/.vix-completion.bash' >> ~/.bashrc +source ~/.bashrc +``` + +After that, open a new terminal and try `vix `. + +## One-time usage + +```bash +source <(vix completion bash) +``` + +This does not persist after closing the terminal. + +## Update completions after upgrading Vix + +```bash +vix upgrade +vix completion bash > ~/.vix-completion.bash +source ~/.bashrc +``` + +## Shell support + +The documented completion target is `bash`: + +```bash +vix completion bash +``` + +## Common workflows + +```bash +# Generate Bash completion +vix completion bash + +# Save completion file +vix completion bash > ~/.vix-completion.bash + +# Load completion in current shell +source ~/.vix-completion.bash + +# Install persistent Bash completion +vix completion bash > ~/.vix-completion.bash +echo 'source ~/.vix-completion.bash' >> ~/.bashrc +source ~/.bashrc + +# Regenerate after upgrade +vix upgrade +vix completion bash > ~/.vix-completion.bash +source ~/.bashrc +``` + +## Common mistakes + +### Forgetting to source the file + +```bash +# Only writes the script +vix completion bash > ~/.vix-completion.bash + +# You still need to load it +source ~/.vix-completion.bash +``` + +### Expecting completion to persist without editing shell config + +Add to `~/.bashrc`: + +```bash +echo 'source ~/.vix-completion.bash' >> ~/.bashrc +``` + +### Forgetting to regenerate after upgrade + +```bash +vix completion bash > ~/.vix-completion.bash +``` + +## Related commands + +| Command | Purpose | +|---------|---------| +| `vix --help` | Show global CLI help | +| `vix help ` | Show command help | +| `vix upgrade` | Upgrade Vix | +| `vix doctor` | Check environment health | +| `vix info` | Inspect Vix paths and local state | + +## Next step + +Return to the command overview. + +[Open all CLI commands](/cli/commands) diff --git a/docs/cli/dev.md b/docs/cli/dev.md new file mode 100644 index 0000000..49e319c --- /dev/null +++ b/docs/cli/dev.md @@ -0,0 +1,709 @@ +# vix dev + +`vix dev` starts a Vix application in development mode. + +It configures, builds, runs, watches, rebuilds, and restarts your app automatically while you edit code. + +Use it during active development. + +```bash +vix dev +``` + +## Overview + +`vix dev` is the development entrypoint for Vix projects and single-file C++ apps. + +It is designed for a fast feedback loop: + +1. edit file +2. save +3. Vix detects the change +4. Vix rebuilds the target +5. Vix restarts the app + +In project mode, `vix dev` uses the project target by default, not the full `all` target. + +This keeps development reloads focused and fast. + +## Usage + +```bash +vix dev [name] [options] [-- app-args...] +``` + +## Basic usage + +```bash +# Start the current project in dev mode +vix dev + +# Start a named app or target +vix dev api + +# Start a single C++ file in dev mode +vix dev server.cpp + +# Pass runtime arguments to the application +vix dev server.cpp -- --port 8080 +``` + +## What vix dev does + +When you run: + +```bash +vix dev +``` + +Vix performs these steps: + +1. Detect the current project +2. Configure the project if needed +3. Build the main target +4. Start the application +5. Watch relevant files +6. Rebuild when files change +7. Restart the application automatically + +Normal output is intentionally compact: + +``` +Dev api (dev) + ✔ Configured +Compiling api (dev) + build [============================] done + ✔ Started pid=12345 +``` + +When the application starts, the app may print its own output: + +``` +2:36:00 AM ◆ Vix.cpp READY v2.5.3 (1 ms) dev + + › HTTP: http://localhost:8080/ + i Threads: 8/8 + i Mode: dev (watch/reload) + i Status: ready + i Hint: Ctrl+C to stop the server +``` + +## Development profile + +The `(dev)` label means Vix is running in development profile. + +| Label | Description | +|-------|-------------| +| `(dev)` | development build, debug-friendly, fast feedback | +| `(release)` | optimized build, production-oriented | + +For `vix dev`, the public profile is always development-oriented. + +Example: + +``` +Dev api (dev) +Compiling api (dev) +``` + +## Project mode + +Inside a Vix project: + +```bash +vix dev +``` + +Vix uses the project directory name as the default target. + +For example, `~/tmp/api` uses target `api`. So `vix dev` starts the `api` target. + +You can also pass a target or app name explicitly: + +```bash +vix dev api +``` + +## Script mode + +You can run a single `.cpp` file in development mode: + +```bash +vix dev server.cpp +``` + +This is useful for quick prototypes, small servers, experiments, or demos without creating a full project. + +Example: + +```bash +vix dev server.cpp --force-server +``` + +## Runtime arguments + +Arguments after `--` are passed to your application. + +```bash +vix dev server.cpp -- --port 8080 +``` + +For project mode: + +```bash +vix dev -- --port 8080 +``` + +The `--` separator matters because Vix needs to distinguish its own options from your app arguments. + +### Important argument rule + +In script mode, `vix dev server.cpp -- --port 8080` means `--port 8080` goes to your application. + +Without `--`, Vix may treat the argument as a Vix option. + +Wrong: + +```bash +vix dev server.cpp --port 8080 +``` + +Correct: + +```bash +vix dev server.cpp -- --port 8080 +``` + +## Watch and reload + +Watch mode is enabled by default in `vix dev`. + +These are equivalent: + +```bash +vix dev +vix dev --watch +vix dev --reload +``` + +When a watched file changes, Vix clears the terminal, rebuilds, and restarts the application. + +Example reload output: + +``` +Dev api (dev) + changed: /home/user/api/src/main.cpp +Compiling api (dev) + build [============================] done + ✔ Started pid=12389 +``` + +## File watching behavior + +`vix dev` watches files that can affect the build or runtime. + +Watched source extensions: `.cpp`, `.cc`, `.cxx`, `.c`, `.hpp`, `.hh`, `.hxx`, `.h`, `.ipp`, `.inl`, `.cmake` + +Watched project files: `CMakeLists.txt`, `CMakePresets.json`, `vix.json`, `vix.toml`, `vix.lock` + +Ignored paths: `.git`, `.vix`, `build`, `build-dev`, `build-ninja`, `build-release`, `node_modules`, `.cache`, `.idea`, `.vscode`, `docs`, `doc`, `dist`, `out`, `coverage` + +Ignored files include common non-build files such as `README.md`, `CHANGELOG.md`, `LICENSE`, `.gitignore`. + +## Rebuild behavior + +`vix dev` uses a file index to detect changes. +It compares mtime, file size, and change kind. This avoids unnecessary rebuilds and makes reload detection faster. + +### Source or header change + +When a `.cpp`, `.hpp`, or `.h` file changes, Vix performs a normal rebuild and restarts the app. + +``` +Dev api (dev) + changed: /home/user/api/src/main.cpp +Compiling api (dev) + build [============================] done + ✔ Started pid=12389 +``` + +### CMake or Vix config change + +When a configuration file changes (`CMakeLists.txt`, `CMakePresets.json`, `vix.json`, `vix.lock`), Vix performs a reconfigure, rebuild, and restart. +This is needed because the project structure, dependencies, options, or targets may have changed. + +## Build progress + +`vix dev` uses the same build progress style as `vix build`. + +When Ninja reports progress, Vix renders it as: + +``` +Compiling api (dev) + build [====------------------------] 4/31 + › Building CXX object CMakeFiles/api.dir/src/main.cpp.o +``` + +When the build is complete: + +``` + build [============================] done +``` + +The progress is based on real Ninja build progress, not a fake spinner. + +### Why progress may not appear instantly + +Sometimes `Compiling api (dev)` appears before the progress line because Vix is waiting for Ninja to emit the first real progress line. This is normal. +Vix does not show fake progress in `vix dev`. + +## Configure behavior + +If the build directory does not exist yet, Vix configures the project first: + +``` +Dev api (dev) + ✔ Configured +Compiling api (dev) + build [============================] done + ✔ Started pid=12345 +``` + +If the project is already configured, Vix skips the configure step. + +## Build directory + +Project dev mode uses `build-ninja`. + +Example: + +``` +/home/user/api/build-ninja +``` + +This matches the normal development build path used by `vix build`. + +## Target behavior + +By default, `vix dev` builds the main project target. For a project named `api`, Vix builds `api`, not `all`. This avoids rebuilding examples, tests, and auxiliary targets during development reloads. + +## Force server mode + +Use `--force-server` when the target is a long-running app such as an HTTP server or WebSocket server. + +```bash +vix dev server.cpp --force-server +``` + +## Force script mode + +Use `--force-script` when the target is a short-lived CLI tool. + +```bash +vix dev tool.cpp --force-script +``` + +## Parallel build jobs + +Use `-j` or `--jobs` to control parallel build jobs. + +```bash +vix dev -j 8 +vix dev --jobs 16 +``` + +If no job count is provided, Vix uses a sensible default based on the machine. + +## SQLite support + +Use `--with-sqlite` when your app needs SQLite-related Vix database support. + +```bash +vix dev --with-sqlite +vix dev server.cpp --with-sqlite +``` + +## MySQL support + +Use `--with-mysql` when your app needs MySQL-related Vix database support. + +```bash +vix dev --with-mysql +vix dev server.cpp --with-mysql +``` + +When enabled, Vix can also expose runtime environment variables such as: + +``` +VIX_DB_ENGINE=mysql +VIX_ENABLE_DB=1 +VIX_DB_USE_MYSQL=1 +``` + +or: + +``` +VIX_DB_ENGINE=sqlite +VIX_ENABLE_DB=1 +VIX_DB_USE_SQLITE=1 +``` + +## Sanitizers + +`vix dev` supports sanitizer-related options inherited from `vix run`. + +```bash +vix dev server.cpp --san +vix dev server.cpp --ubsan +vix dev server.cpp --tsan +``` + +Use these when debugging memory issues, undefined behavior, or threading problems. + +## Logging + +Use `--log-level` to control Vix logging. + +```bash +vix dev --log-level debug +``` + +Supported levels: `trace`, `debug`, `info`, `warn`, `error`, `critical` + +Use `--verbose` for debug-style output: + +```bash +vix dev --verbose +``` + +Use `--quiet` for minimal output: + +```bash +vix dev --quiet +``` + +## Verbose mode + +Normal mode keeps output small: + +``` +Dev api (dev) + ✔ Configured +Compiling api (dev) + build [============================] done + ✔ Started pid=12345 +``` + +Verbose mode shows more detail: + +```bash +vix dev -v +``` + +``` +Dev api (dev) + watching: /home/user/api + target : api + build : /home/user/api/build-ninja + press Ctrl+C to stop + +Configuring api (dev) + ✔ Configured + +Compiling api (dev) + build [============================] done + ✔ Started pid=12345 +``` + +## Quiet mode + +```bash +vix dev --quiet +vix dev -q +``` + +## Clean rebuild + +Use `vix build --clean` when you want to clean the build directory before running dev mode again. + +```bash +vix build --clean +vix dev +``` + +For most normal development, you do not need to clean manually. + +## Recommended workflow + +### New project + +```bash +vix new api +cd api +vix install +vix dev +``` + +Then edit your files. When you save: + +``` +Dev api (dev) + changed: /home/user/api/src/main.cpp +Compiling api (dev) + build [============================] done + ✔ Started pid=12389 +``` + +### Existing project + +```bash +cd api +vix install +vix dev +``` + +### Single-file server + +```bash +vix dev server.cpp --force-server +vix dev server.cpp --force-server -- --port 8080 +``` + +### Single-file CLI + +```bash +vix dev tool.cpp --force-script +vix dev tool.cpp --force-script -- input.txt +``` + +## Difference between vix dev, vix run, and vix build + +| Command | Purpose | Runs app | Watches files | Restarts app | +|---------|---------|----------|---------------|--------------| +| `vix build` | Configure and compile | no | no | no | +| `vix run` | Build and run once | yes | no by default | no by default | +| `vix run --watch` | Build, run, and watch | yes | yes | yes | +| `vix dev` | Development mode | yes | yes | yes | + +`vix dev` is the best command while editing an application. `vix run` is better when you want to run manually. +`vix build` is better when you only want to compile. + +## Relationship with vix run --watch + +Conceptually, `vix dev` is similar to `vix run --watch`, but `vix dev` is optimized for the development experience. +It provides cleaner dev output, target-aware rebuilds, automatic reload behavior, file indexing, and development-focused terminal rendering. + +## Options + +| Option | Description | +|--------|-------------| +| `--force-server` | Force classification as a long-running development server. | +| `--force-script` | Force classification as a short-lived script or CLI tool. | +| `--watch` | Enable watch mode. Enabled by default in `vix dev`. | +| `--reload` | Alias-style reload flag. Enabled by default in `vix dev`. | +| `-j, --jobs ` | Number of parallel build jobs. | +| `--with-sqlite` | Enable SQLite-related support. | +| `--with-mysql` | Enable MySQL-related support. | +| `--san` | Enable AddressSanitizer and UBSan where supported. | +| `--ubsan` | Enable UBSan-only mode where supported. | +| `--tsan` | Enable ThreadSanitizer where supported. | +| `--cwd ` | Run the application from a specific working directory. | +| `--env ` | Pass an environment variable to the app. | +| `--args ` | Pass repeatable runtime arguments. | +| `--log-level ` | Set Vix log verbosity. | +| `--verbose, -v` | Show more development details. | +| `--quiet, -q` | Reduce output to warnings and errors. | +| `-h, --help` | Show command help. | + +## Environment variables + +| Variable | Description | +|----------|-------------| +| `VIX_LOG_LEVEL=debug` | Show debug-level Vix logs. | +| `VIX_LOG_LEVEL=trace` | Show trace-level Vix logs. | +| `VIX_MODE=dev` | Set automatically for the running app. | +| `VIX_STDOUT_MODE=line` | Set automatically to make runtime output easier to stream. | +| `VIX_DB_ENGINE=mysql` | Set when MySQL mode is enabled. | +| `VIX_DB_ENGINE=sqlite` | Set when SQLite mode is enabled. | + +## Common workflows + +### Start a new app + +```bash +vix new api +cd api +vix install +vix dev +``` + +### Run an existing app + +```bash +cd api +vix install +vix dev +``` + +### Run a server file directly + +```bash +vix dev server.cpp --force-server +``` + +### Run a CLI tool file directly + +```bash +vix dev tool.cpp --force-script +``` + +### Pass app arguments + +```bash +vix dev server.cpp -- --port 8080 +``` + +### Use more build jobs + +```bash +vix dev -j 8 +``` + +### Enable verbose dev output + +```bash +vix dev -v +``` + +### Enable debug logs + +```bash +VIX_LOG_LEVEL=debug vix dev +``` + +## Common mistakes + +### Running outside the project directory + +Wrong: + +```bash +vix new api +vix dev +``` + +Correct: + +```bash +vix new api +cd api +vix dev +``` + +### Forgetting `--` before app arguments + +Wrong: + +```bash +vix dev server.cpp --port 8080 +``` + +Correct: + +```bash +vix dev server.cpp -- --port 8080 +``` + +### Expecting vix dev to build every target + +`vix dev` builds the main project target. It does not build every example, test, or auxiliary target by default. + +```bash +vix build --build-target all +``` + +### Using VIX_LOG_LEVEL=release + +Wrong: + +```bash +VIX_LOG_LEVEL=release vix dev +``` + +Correct: + +```bash +vix build --preset release +``` + +`VIX_LOG_LEVEL` controls logging, not the build profile. + +### Editing ignored files and expecting reload + +Files such as `README.md`, `docs`, build output, `.git`, and `.vix` are ignored by dev mode. +Editing them does not trigger a reload. + +## Troubleshooting + +### Nothing happens after saving + +Check that the file is watched. Watched files include source, header, CMake, and Vix config files. +Files such as docs, README, generated output, and build directories are ignored. + +### The app does not restart + +Make sure the build succeeds. If the build fails, Vix waits for the next file change. +Fix the error, save again, and Vix will rebuild automatically. + +### The executable cannot be found + +Vix expects the project executable target to match the project target name. +For example, project directory `api` expects target `api`. +Make sure your `CMakeLists.txt` defines an executable target with that name. + +### Progress appears late + +Vix only shows the progress bar when Ninja reports real progress. +If the build is already up to date or Ninja has not emitted progress yet, you may see `Compiling api (dev)` before the progress line. This is normal. + +### Too much output + +```bash +vix dev --quiet +``` + +### Need more details + +```bash +vix dev -v +VIX_LOG_LEVEL=debug vix dev +``` + +## Best practices + +Use `vix dev` while coding. +Use `vix build` before committing or packaging. +Use `vix build --build-target all` before install or full repository validation. +Use `vix check` when you want deeper validation. + +--- + +## Related commands + +| Command | Purpose | +|---------|---------| +| `vix run` | Build and run manually | +| `vix run --watch` | Build, run, and reload manually | +| `vix build` | Configure and compile | +| `vix check` | Validate build, tests, runtime, and sanitizers | +| `vix tests` | Run tests | +| `vix replay` | Inspect and replay previous Vix executions | + +## Next step + +Continue with project builds. + +[Open the vix build guide](/cli/build) + + + diff --git a/docs/cli/doctor.md b/docs/cli/doctor.md new file mode 100644 index 0000000..c31f387 --- /dev/null +++ b/docs/cli/doctor.md @@ -0,0 +1,98 @@ +# vix doctor + +`vix doctor` checks the local environment for running and upgrading Vix. + +Use it when you want to diagnose setup problems, inspect missing tools, or verify whether your Vix installation can work correctly. + +## Usage + +```bash +vix doctor [options] +``` + +## What it does + +`vix doctor` inspects the local environment. It can check whether Vix is installed correctly, inspect required development tools, check environment readiness, check local upgrade information, optionally check the latest release online, and print a JSON summary for scripts or CI. + +## Basic usage + +```bash +vix doctor +vix doctor --online +vix doctor --json +vix doctor --json --online +vix doctor --online --repo vixcpp/vix +``` + +## Options + +| Option | Description | +|--------|-------------| +| `--json` | Print a JSON summary at the end. | +| `--online` | Also check the latest release on GitHub. | +| `--repo ` | Repository to check when using `--online`. Default is `vixcpp/vix`. | +| `-h, --help` | Show command help. | + +## When to run it + +Run `vix doctor` when: `vix build` fails unexpectedly, `vix run` cannot find tools, `vix install` behaves strangely, your environment changed, you installed Vix on a new machine, or you want to verify setup before using Vix seriously. + +## Suggested debugging flow + +```bash +vix doctor +vix info +vix registry sync # if package resolution fails +vix reset # if dependency state is broken +vix doctor --online # if Vix may be outdated +``` + +## Difference between `vix doctor` and `vix info` + +| Command | Purpose | +|---------|---------| +| `vix doctor` | Check environment health | +| `vix info` | Show paths, caches, and local state | + +## Common mistakes + +### Expecting doctor to fix problems automatically + +`vix doctor` diagnoses problems — it does not repair them. After diagnosis, use the appropriate command (`vix registry sync`, `vix reset`, `vix upgrade`). + +### Using `--repo` without `--online` + +```bash +vix doctor --online --repo vixcpp/vix +``` + +## Recommended troubleshooting order + +```bash +vix doctor +vix info +vix registry sync +vix reset +vix check --tests + +# If issue may be caused by an old version +vix doctor --online +vix upgrade --check +``` + +## Related commands + +| Command | Purpose | +|---------|---------| +| `vix info` | Show paths, caches, and local state | +| `vix upgrade --check` | Check upgrade target and download info | +| `vix upgrade` | Upgrade Vix | +| `vix registry sync` | Refresh registry metadata | +| `vix reset` | Clean and reinstall project dependencies | +| `vix check` | Validate project health | + +## Next step + +Continue with upgrades. + +[Open the vix upgrade guide](/cli/upgrade) diff --git a/docs/cli/fmt.md b/docs/cli/fmt.md new file mode 100644 index 0000000..3147b10 --- /dev/null +++ b/docs/cli/fmt.md @@ -0,0 +1,153 @@ +# vix fmt + +`vix fmt` formats C++ source files using `clang-format`. + +Use it when you want consistent formatting across your project. + +## Usage + +```bash +vix fmt [options] [files...] +``` + +## What it does + +`vix fmt` scans files or directories and applies `clang-format`. It can format one file, multiple files, or directories, and check formatting without changing files. + +## Basic usage + +```bash +# Format the default project folders +vix fmt + +# Format specific folders +vix fmt src include + +# Format one file +vix fmt main.cpp + +# Check whether a file is formatted +vix fmt main.cpp --check + +# Ignore a path +vix fmt src --ignore=build --ignore=vendor +``` + +## Default behavior + +If no files or directories are provided, `vix fmt` scans `src/` and `include/`. A normal project can usually be formatted with: + +```bash +vix fmt +``` + +## Check mode + +Use `--check` when you only want to verify formatting: + +```bash +vix fmt --check +vix fmt src include --check +vix fmt main.cpp --check +``` + +This is useful before committing or in CI. + +## Ignore paths + +```bash +vix fmt src include --ignore=build +vix fmt src include --ignore=build --ignore=vendor +``` + +Common ignored folders: `build`, `vendor`, `third_party`, `.vix`, `dist` + +## Project formatting file + +If a `.clang-format` file exists, `clang-format` uses it automatically. Then `vix fmt` uses the formatting rules from `.clang-format`. + +## Options + +| Option | Description | +|--------|-------------| +| `--check` | Check if files are formatted without modifying them. | +| `--ignore ` | Ignore a file or path pattern. Repeatable. | +| `-q, --quiet` | Suppress non-essential output. | +| `-h, --help` | Show command help. | + +## Common workflows + +```bash +# Format the current project +vix fmt + +# Check formatting before commit +vix fmt --check + +# Format source and headers +vix fmt src include + +# Format one file +vix fmt src/main.cpp + +# Format while ignoring build outputs +vix fmt src include --ignore=build --ignore=.vix +``` + +## CI usage + +In CI, use check mode: + +```bash +vix fmt --check +vix check --tests +``` + +## Common mistakes + +### Forgetting `--check` in CI + +This modifies files: + +```bash +vix fmt +``` + +In CI, prefer: + +```bash +vix fmt --check +``` + +### Formatting generated or vendor files + +```bash +vix fmt src include --ignore=vendor --ignore=build --ignore=.vix +``` + +## Recommended project task + +```json +{ + "tasks": { + "fmt": "vix fmt", + "fmt:check": "vix fmt --check" + } +} +``` + +## Related commands + +| Command | Purpose | +|---------|---------| +| `vix make` | Generate C++ files | +| `vix check` | Validate build, tests, runtime, and sanitizers | +| `vix tests` | Run tests | +| `vix task` | Run reusable project tasks | +| `vix build` | Compile the project | + +## Next step + +Continue with project cleanup. + +[Open the vix clean guide](/cli/clean) diff --git a/docs/cli/index.md b/docs/cli/index.md new file mode 100644 index 0000000..3208203 --- /dev/null +++ b/docs/cli/index.md @@ -0,0 +1,278 @@ +# Vix CLI + +The Vix CLI is the command-line interface for Vix.cpp. + +It gives C++ a modern runtime-like development experience. With one binary, you can create projects, run C++ files, build applications, manage dependencies, validate code, package artifacts, and inspect your local environment. + +## What the CLI gives you + +The `vix` command is designed to make C++ development faster and more direct. + +It provides: + +- an interactive REPL +- project creation +- C++ file generation +- script-like `.cpp` execution +- CMake-based project builds +- development mode with reload +- checks, tests, and sanitizers +- dependency management +- packaging and verification +- registry and local store tools +- environment diagnostics + +## Start in seconds + +Create a new application: + +```bash +vix new api +cd api +vix install +vix dev +``` + +This creates a ready-to-run Vix project, installs dependencies, and starts the app in development mode. + +## Default interactive mode + +Running `vix` without a command starts the interactive REPL: + +```bash +vix +``` + +The REPL lets you evaluate expressions, create variables, work with JSON, inspect the environment, and run Vix commands from an interactive shell. + +You can also start it explicitly: + +```bash +vix repl +``` + +## Core workflow + +For a new project: + +```bash +vix new api +cd api +vix install +vix dev +``` + +For an existing project: + +```bash +vix install +vix dev +``` + +For a single C++ file: + +```bash +vix run main.cpp +``` + +For validation: + +```bash +vix check +vix tests +``` + +## Command groups + +The CLI is organized around a few major workflows. + +### Project commands + +| Command | Purpose | +|---------|---------| +| `vix new` | Create a new Vix project | +| `vix make` | Generate C++ files quickly | +| `vix dev` | Run an app in development mode with reload | +| `vix run` | Build and run a project, file, or manifest | +| `vix build` | Configure and build a CMake project | +| `vix check` | Validate a project or single C++ file | +| `vix tests` | Run project tests | +| `vix fmt` | Format C++ source files | +| `vix clean` | Remove local project cache directories | +| `vix reset` | Clean and reinstall the project | +| `vix task` | Run reusable project tasks | + +### Dependency commands + +| Command | Purpose | +|---------|---------| +| `vix add` | Add a package to the project | +| `vix install` | Install dependencies from `vix.lock` | +| `vix update` | Update dependencies | +| `vix outdated` | Check outdated dependencies | +| `vix remove` | Remove a dependency | +| `vix list` | List project or global dependencies | + +Aliases: + +| Alias | Equivalent command | +|-------|--------------------| +| `vix up` | `vix update` | +| `vix i` | `vix install` | +| `vix deps` | `vix install` | + +### Packaging commands + +| Command | Purpose | +|---------|---------| +| `vix pack` | Package a project into a distributable artifact | +| `vix verify` | Verify a package artifact | +| `vix cache` | Store a verified package locally | + +### Advanced commands + +| Command | Purpose | +|---------|---------| +| `vix registry` | Manage the local registry index | +| `vix store` | Manage the local package store | +| `vix orm` | Run database migration tooling | +| `vix p2p` | Run a peer-to-peer node | + +### System commands + +| Command | Purpose | +|---------|---------| +| `vix info` | Show Vix paths, caches, and local state | +| `vix doctor` | Check the local environment | +| `vix upgrade` | Upgrade Vix or a global package | +| `vix uninstall` | Remove Vix or a global package | +| `vix completion` | Generate shell completion scripts | +| `vix version` | Show the installed Vix version | + +## Global options + +Most commands support global logging and help options. + +```bash +vix --help +vix --version +vix --verbose +vix --quiet +vix --log-level debug +``` + +| Option | Purpose | +|--------|---------| +| `-h, --help` | Show help | +| `-v, --version` | Show version | +| `--verbose` | Enable debug logs | +| `-q, --quiet` | Show only warnings and errors | +| `--log-level ` | Set log level | + +Supported log levels: `trace`, `debug`, `info`, `warn`, `error`, `critical`, `off` + +## Typical usage + +### Create and run a new app + +```bash +vix new api +cd api +vix install +vix dev +``` + +### Run a single C++ file + +```bash +vix run main.cpp +``` + +### Pass runtime arguments to a C++ file + +```bash +vix run main.cpp --run --port 8080 +``` + +> Use `--run` for runtime arguments. Do not pass runtime arguments after `--`, because everything after `--` is forwarded to the compiler or linker in script mode. + +### Build a project + +```bash +vix build +``` + +### Build with SQLite support + +```bash +vix build --with-sqlite +``` + +### Validate with sanitizers + +```bash +vix check --san +``` + +For a single file: + +```bash +vix check main.cpp --san +``` + +### Run tests + +```bash +vix tests +``` + +### Format code + +```bash +vix fmt +``` + +### Package a project + +```bash +vix pack --name api --version 1.0.0 +``` + +### Verify a package + +```bash +vix verify --path ./dist/api@1.0.0 +``` + +## When to use each command + +- Use `vix` when you want an interactive shell. +- Use `vix new` when starting a new project. +- Use `vix make` when you want to generate C++ files such as classes, structs, enums, functions, tests, or config files. +- Use `vix dev` when you are actively developing and want automatic rebuilds. +- Use `vix run` when you want to build and run a project, a single `.cpp` file, or a `.vix` manifest. +- Use `vix build` when you only want to configure and compile. +- Use `vix check` when you want validation, tests, runtime checks, or sanitizer checks. +- Use `vix tests` when you only want to run tests. +- Use `vix fmt` before committing source code. +- Use `vix task` when your project defines reusable workflows in `vix.json`. +- Use `vix pack`, `vix verify`, and `vix cache` when preparing artifacts for sharing or deployment. +- Use `vix doctor` when something does not work and you want to inspect your environment. + +## CLI design philosophy + +The Vix CLI exists to make C++ feel direct without hiding what C++ is. + +It does not replace CMake, compilers, linkers, or native tooling. Instead, it gives them a cleaner workflow: + +1. write code +2. run it +3. check it +4. package it +5. ship it + +Vix keeps C++ native, but makes the development loop faster and easier to understand. + +## Next step + +Continue with the [REPL guide](#). diff --git a/docs/cli/info.md b/docs/cli/info.md new file mode 100644 index 0000000..1833efa --- /dev/null +++ b/docs/cli/info.md @@ -0,0 +1,83 @@ +# vix info + +`vix info` shows Vix environment information, paths, caches, and local state. + +Use it when you want to inspect where Vix stores data and understand the current local setup. + +## Usage + +```bash +vix info +``` + +## What it does + +`vix info` prints useful information about your Vix installation and local environment, including: Vix version, root path, registry index path and state, store cache path and state, global packages manifest, build artifact cache path, disk usage, and package counts. + +## Basic usage + +```bash +vix info +``` + +## What it helps debug + +Use `vix info` when debugging: dependency installation issues, registry sync issues, package store state, global package state, build artifact cache usage, disk usage problems, or unexpected cache behavior. + +## Relationship with other commands + +`vix info` is read-only. It does not modify your project or global Vix state. Use it before cleanup commands when you want to understand what exists. + +```bash +vix info +vix store path +vix store gc --dry-run +``` + +## Common workflows + +```bash +vix info +vix info && vix store gc --dry-run +vix install -g gk/jwt && vix info && vix list -g +vix registry sync && vix info +vix reset && vix info +``` + +## Common mistakes + +### Expecting `vix info` to fix problems + +`vix info` only prints information. Use `vix doctor` for environment checks, `vix clean` for project cleanup, `vix reset` for dependency reinstall. + +### Expecting `vix info` to update the registry + +```bash +vix registry sync # to update registry metadata +``` + +### Expecting `vix info` to remove cache data + +```bash +vix store gc # to clean the store +vix store gc --dry-run # preview first +``` + +## Related commands + +| Command | Purpose | +|---------|---------| +| `vix doctor` | Check environment health | +| `vix registry path` | Print registry index path | +| `vix registry sync` | Refresh registry index | +| `vix store path` | Print local store path | +| `vix store gc` | Clean unused store data | +| `vix list -g` | List global packages | +| `vix clean` | Clean project-local generated state | +| `vix reset` | Clean and reinstall project dependencies | + +## Next step + +Continue with environment diagnostics. + +[Open the vix doctor guide](/cli/doctor) diff --git a/docs/cli/install.md b/docs/cli/install.md new file mode 100644 index 0000000..7882b0c --- /dev/null +++ b/docs/cli/install.md @@ -0,0 +1,146 @@ +# vix install + +`vix install` installs project dependencies from `vix.lock`. + +It can also install one package globally with `-g`. + +## Usage + +```bash +vix install +vix install -g [@]namespace/name[@version] +``` + +## What it does + +| Mode | Command | Purpose | +|------|---------|---------| +| Project mode | `vix install` | Install exact project dependencies from `vix.lock` | +| Global mode | `vix install -g ` | Install one package globally | + +## Project install + +```bash +vix install +``` + +In project mode, Vix reads exact resolved dependencies from `vix.lock`, reuses cached packages when available, and generates CMake integration files. + +Typical project outputs: `.vix/deps/`, `.vix/vix_deps.cmake` + +## Global install + +```bash +vix install -g gk/jwt +vix install -g gk/jwt@^1.0.0 +vix install -g @gk/jwt +vix install -g @gk/jwt@~1.2.0 +``` + +Typical global outputs: `~/.vix/global/packages/`, `~/.vix/global/installed.json` + +## Project workflow + +```bash +# New project +vix new api +cd api +vix install +vix dev + +# Existing project +git clone https://github.com/example/api.git +cd api +vix install +vix dev +``` + +## Difference between `vix add` and `vix install` + +| Command | Purpose | +|---------|---------| +| `vix add ` | Add a new dependency and update `vix.json` + `vix.lock` | +| `vix install` | Install dependencies already pinned in `vix.lock` | + +## Difference between `vix update` and `vix install` + +| Command | Purpose | +|---------|---------| +| `vix update` | Resolve newer versions and rewrite `vix.lock` | +| `vix install` | Install the exact versions already in `vix.lock` | + +## Registry sync + +If install fails because a package is not found: + +```bash +vix registry sync +vix install +``` + +## Options + +| Option | Description | +|--------|-------------| +| `-g, --global` | Install one package globally. | +| `-h, --help` | Show command help. | + +## Common workflows + +```bash +# Install project dependencies +vix install + +# Install after clone +git clone https://github.com/example/api.git +cd api +vix install + +# Install then run dev mode +vix install +vix dev + +# Install a global package +vix install -g gk/jwt +vix install -g gk/jwt@^1.0.0 +``` + +## CI usage + +```bash +vix install +vix check --tests + +# Release builds +vix install +vix build --preset release +vix tests --preset release +``` + +## Common mistakes + +### Expecting `vix install` to update dependencies + +`vix install` installs locked versions. Use `vix outdated` to check and `vix update` to update. + +### Editing `vix.json` and expecting install to resolve ranges + +Project install is strict. If you manually edit `vix.json`, use `vix update` or `vix add` to update `vix.lock` first. + +## Related commands + +| Command | Purpose | +|---------|---------| +| `vix add` | Add a new dependency | +| `vix update` | Update dependency versions | +| `vix outdated` | Check outdated dependencies | +| `vix remove` | Remove a dependency | +| `vix list` | List dependencies | +| `vix reset` | Clean and reinstall project dependencies | +| `vix registry sync` | Refresh registry index | + +## Next step + +Continue with dependency updates. + +[Open the vix update guide](/cli/update) diff --git a/docs/cli/list.md b/docs/cli/list.md new file mode 100644 index 0000000..a12add7 --- /dev/null +++ b/docs/cli/list.md @@ -0,0 +1,124 @@ +# vix list + +`vix list` lists project dependencies or globally installed packages. + +Use it when you want to inspect what packages are currently installed for a project or globally in your Vix environment. + +## Usage + +```bash +vix list +vix list -g +``` + +## What it does + +| Mode | Command | Purpose | +|------|---------|---------| +| Project mode | `vix list` | List project dependencies from `vix.lock` | +| Global mode | `vix list -g` | List globally installed packages | + +## Project dependencies + +```bash +cd api +vix list +``` + +Reads from `vix.lock`. + +## Global packages + +```bash +vix list -g +``` + +Reads from `~/.vix/global/installed.json`. + +## Difference between project and global list + +| Command | Reads from | Scope | +|---------|-----------|-------| +| `vix list` | `vix.lock` | Current project | +| `vix list -g` | `~/.vix/global/installed.json` | Global Vix environment | + +## Options + +| Option | Description | +|--------|-------------| +| `-g, --global` | List globally installed packages. | +| `-h, --help` | Show command help. | + +## Common workflows + +```bash +# List project dependencies +vix list + +# List global packages +vix list -g + +# Add and list +vix add gk/jwt +vix list + +# Update and list +vix update --install +vix list + +# Remove and list +vix remove gk/jwt +vix list +``` + +## CI usage + +```bash +vix install +vix list +vix check --tests +``` + +## Common mistakes + +### Running project list outside a project + +```bash +# Wrong +cd .. +vix list + +# Correct +cd api +vix list +``` + +### Expecting `vix list` to show outdated packages + +Use `vix outdated` for that. + +### Confusing project and global packages + +```bash +vix list # project packages +vix list -g # global packages +``` + +## Related commands + +| Command | Purpose | +|---------|---------| +| `vix add` | Add a dependency | +| `vix install` | Install dependencies | +| `vix update` | Update dependencies | +| `vix outdated` | Check outdated dependencies | +| `vix remove` | Remove a dependency | +| `vix install -g` | Install a global package | +| `vix upgrade -g` | Upgrade a global package | +| `vix uninstall -g` | Remove a global package | + +## Next step + +Continue with packaging. + +[Open the vix pack guide](/cli/pack) diff --git a/docs/cli/make.md b/docs/cli/make.md new file mode 100644 index 0000000..3690716 --- /dev/null +++ b/docs/cli/make.md @@ -0,0 +1,274 @@ +# vix make + +`vix make` generates C++ files quickly from the folder you are working in. + +Use it when you want to create common C++ scaffolding such as classes, structs, enums, functions, concepts, exceptions, tests, lambdas, or config files. + +## Usage + +```bash +vix make [options] +vix make: [name] [options] +``` + +## What it does + +`vix make` is a file generator. It writes files into the current directory by default, or into a custom folder when `--in` is provided. + +It is useful for quickly creating: classes, structs, enums, free functions, lambdas, concepts, exceptions, GoogleTest skeletons, and JSON runtime config files. + +## Basic usage + +```bash +vix make class User +vix make struct Claims +vix make enum Status +vix make function parse_token +vix make test AuthService +vix make config app +``` + +## Interactive generator form + +You can also use the `vix make:` form: + +```bash +vix make:class +vix make:class User +``` + +If the name is missing, Vix starts an interactive prompt. + +## Supported kinds + +| Kind | What it generates | +|------|-------------------| +| `class` | A C++ class, usually `.hpp` and `.cpp` | +| `struct` | A plain data struct, usually `.hpp` | +| `enum` | An enum class with helpers | +| `function` | A free function, usually `.hpp` and `.cpp` | +| `lambda` | A modern generic lambda snippet | +| `concept` | A C++20 concept | +| `exception` | A `std::exception`-derived type | +| `test` | A GoogleTest skeleton | +| `module` | Redirects to the modules workflow | +| `config` | A JSON runtime configuration file | + +## Generate a class + +```bash +vix make class User +vix make class User --in src/domain +vix make class User --namespace app::domain +vix make class User --header-only +vix make class User --dry-run +vix make class User --print +``` + +## Generate a struct + +```bash +vix make struct Claims +vix make struct Claims --namespace auth +vix make struct Claims --in src/auth +``` + +## Generate an enum + +```bash +vix make enum Status +vix make enum Status --in src/domain +vix make enum Status --namespace app::domain +``` + +## Generate a free function + +```bash +vix make function parse_token +vix make function parse_token --in src/auth +vix make function parse_token --header-only +``` + +## Generate a lambda + +```bash +vix make lambda visit_all +vix make lambda visit_all --print +``` + +## Generate a concept + +```bash +vix make concept EqualityComparable +``` + +This generates a C++20 concept file, useful for expressing constraints in generic code. + +## Generate an exception + +```bash +vix make exception InvalidToken +vix make exception InvalidToken --in src/auth +``` + +## Generate a test + +```bash +vix make test AuthService +``` + +This generates a GoogleTest skeleton. + +## Generate a config file + +```bash +vix make config app +vix make config app --websocket --database +vix make config app --server --logging --waf +vix make config app --no-websocket +``` + +### Config sections + +| Option | Meaning | +|--------|---------| +| `--server` / `--no-server` | Enable/disable server section | +| `--logging` / `--no-logging` | Enable/disable logging section | +| `--waf` / `--no-waf` | Enable/disable WAF section | +| `--websocket` / `--no-websocket` | Enable/disable WebSocket section | +| `--database` / `--no-database` | Enable/disable database section | + +## Output directory + +By default, files are generated in the current directory. + +```bash +vix make enum Status --in src/domain +vix make class User --dir ./apps/api +``` + +`--dir` is the project root. `--in` is the folder where generated files are written. + +## Namespace + +```bash +vix make class User --namespace app::domain +vix make struct Claims --namespace auth +``` + +## Header-only generation + +```bash +vix make class User --header-only +``` + +## Preview and dry run + +```bash +vix make lambda visit_all --print +vix make class User --dry-run +``` + +## Overwrite files + +```bash +vix make class User --force +``` + +Use `--force` carefully — it replaces existing files. + +## Options + +| Option | Description | +|--------|-------------| +| `-d, --dir ` | Project root. Default is the current directory. | +| `--in ` | Folder where files should be generated. | +| `--namespace ` | Override namespace. | +| `--header-only` | Generate only header files when supported. | +| `--print` | Print preview or snippet without writing files. | +| `--dry-run` | Show what would be generated without writing files. | +| `--force` | Overwrite existing files. | +| `--server` / `--no-server` | Enable/disable server section for config generation. | +| `--logging` / `--no-logging` | Enable/disable logging section for config generation. | +| `--waf` / `--no-waf` | Enable/disable WAF section for config generation. | +| `--websocket` / `--no-websocket` | Enable/disable WebSocket section for config generation. | +| `--database` / `--no-database` | Enable/disable database section for config generation. | +| `-h, --help` | Show command help. | + +## Common workflows + +```bash +# Generate a domain class +vix make class User --in src/domain --namespace app::domain + +# Generate an auth struct +vix make struct Claims --in src/auth --namespace auth + +# Generate an enum for status values +vix make enum Status --in src/domain + +# Generate a parser function +vix make function parse_token --in src/auth + +# Generate a custom exception +vix make exception InvalidToken --in src/auth + +# Generate a test skeleton +vix make test AuthService + +# Generate app configuration +vix make config app --websocket --database +``` + +## Common mistakes + +### Using `vix make` instead of `vix new` + +`vix make` generates files. It does not create a full project layout. Use `vix new` when starting a new app. + +### Confusing `--dir` and `--in` + +`--dir` tells Vix where the project root is. `--in` tells Vix where to write files. + +```bash +vix make class User --dir ./apps/api --in src/domain +# project root: ./apps/api +# output folder: ./apps/api/src/domain +``` + +### Missing a name + +```bash +# May start interactive prompt +vix make class + +# Direct generation +vix make class User + +# Or use interactive form intentionally +vix make:class +``` + +## When to use `vix make` + +Use `vix make` when your project already exists and you want to generate C++ files quickly, create test skeletons, generate a runtime config file, or preview generated code before writing it. + +Use `vix new` when you need a new project. + +## Related commands + +| Command | Purpose | +|---------|---------| +| `vix new` | Create a new project | +| `vix dev` | Run the project in development mode | +| `vix run` | Build and run | +| `vix build` | Build project | +| `vix check` | Validate project | +| `vix tests` | Run tests | +| `vix fmt` | Format generated files | + +## Next step + +Continue with formatting. + +[Open the vix fmt guide](/cli/fmt) diff --git a/docs/cli/new.md b/docs/cli/new.md new file mode 100644 index 0000000..b1f2811 --- /dev/null +++ b/docs/cli/new.md @@ -0,0 +1,268 @@ +# vix new + +`vix new` creates a new Vix project. + +Use it when you want to start a new application or initialize a Vix project in an existing folder. + +## Usage + +```bash +vix new [options] +``` + +## Examples + +Create a new project named `api`: + +```bash +vix new api +``` + +Create a project in the current directory: + +```bash +vix new . +``` + +Create a header-only library: + +```bash +vix new tree --lib +``` + +Create a project inside another directory: + +```bash +vix new blog -d ./projects +``` + +Overwrite an existing directory: + +```bash +vix new api --force +``` + +## What it creates + +For an application, `vix new` generates a ready-to-run Vix project with: + +- a CMake project +- source structure +- config files +- a `vix.json` manifest +- default project tasks +- an empty dependency list + +For a library, it generates package metadata and an empty dependency list. + +## Basic workflow + +```bash +vix new api +cd api +vix install +vix dev +``` + +## Application project + +By default, `vix new` creates an application: + +```bash +vix new api +``` + +This is equivalent to: + +```bash +vix new api --app +``` + +Use this when you want to build an executable application, API server, backend service, tool, or demo. + +## Library project + +Use `--lib` when you want to create a header-only library package: + +```bash +vix new tree --lib +``` + +This is useful when you want to create reusable C++ code that can later be packaged and shared. + +## Initialize the current directory + +Use `.` to create the project in the current folder: + +```bash +vix new . +``` + +This is useful when you already created the repository manually. + +Example: + +```bash +mkdir api +cd api +git init +vix new . +``` + +## Create inside another directory + +Use `-d` or `--dir` to choose the base directory where the project should be created: + +```bash +vix new blog -d ./projects +``` + +This creates `./projects/blog`. + +## Overwrite an existing directory + +By default, Vix avoids overwriting existing project files. + +Use `--force` only when you intentionally want to overwrite an existing directory: + +```bash +vix new api --force +``` + +Use this carefully, because existing files may be replaced. + +## Options + +| Option | Description | +|--------|-------------| +| `--app` | Generate an application project. This is the default. | +| `--lib` | Generate a header-only library project. | +| `-d, --dir ` | Base directory for project creation. | +| `--force` | Overwrite an existing directory. | +| `-h, --help` | Show command help. | + +## Environment variables + +| Variable | Description | +|----------|-------------| +| `VIX_NONINTERACTIVE=1` | Disable interactive prompts. | +| `CI=1` | Disable interactive prompts in CI environments. | + +Use non-interactive mode in scripts and CI pipelines: + +```bash +VIX_NONINTERACTIVE=1 vix new api +``` + +## Generated manifest + +A new project includes a `vix.json` manifest. The manifest describes the project, its type, its dependencies, and its reusable tasks. + +A typical application manifest may include: + +```json +{ + "name": "api", + "version": "0.1.0", + "type": "app", + "dependencies": {}, + "tasks": { + "dev": "vix dev", + "run": "vix run", + "build": "vix build", + "check": "vix check --tests", + "fmt": "vix fmt" + } +} +``` + +The exact generated content may evolve with Vix versions, but the role stays the same: `vix.json` is the project manifest. + +## After project creation + +After creating a project, the usual next commands are: + +```bash +cd api +vix install +vix dev +``` + +- Use `vix install` to install dependencies from the lockfile. +- Use `vix dev` to run the application in development mode. +- Use `vix run` when you want to build and run manually. +- Use `vix build` when you only want to compile. +- Use `vix check` when you want to validate the project. + +## Common mistakes + +### Running commands outside the project + +Wrong: + +```bash +vix new api +vix dev +``` + +Correct: + +```bash +vix new api +cd api +vix dev +``` + +After creating a project, enter the generated directory before running project commands. + +### Using `--force` too early + +Avoid this unless you really want to overwrite files: + +```bash +vix new api --force +``` + +Prefer creating a clean folder first. + +### Creating a library when you need an app + +This creates a library: + +```bash +vix new api --lib +``` + +For a backend service or executable app, use the default: + +```bash +vix new api +``` + +## When to use `vix new` + +Use `vix new` when: + +- starting a new Vix application +- creating a reusable Vix library package +- initializing an existing empty repository +- preparing a project for dependency management +- creating a project that should work with `vix dev`, `vix run`, `vix build`, and `vix check` + +## Related commands + +| Command | Purpose | +|---------|---------| +| `vix install` | Install project dependencies | +| `vix dev` | Run the app in development mode | +| `vix run` | Build and run the project | +| `vix build` | Build the project | +| `vix check` | Validate the project | +| `vix task` | Run generated or custom project tasks | +| `vix add` | Add dependencies | + +## Next step + +Continue with file generation. + +[Open the vix make guide](/cli/make) diff --git a/docs/cli/orm.md b/docs/cli/orm.md new file mode 100644 index 0000000..cc16af8 --- /dev/null +++ b/docs/cli/orm.md @@ -0,0 +1,204 @@ +# vix orm + +`vix orm` provides database migration and schema management tooling. + +Use it when you want to apply migrations, rollback migrations, inspect migration status, or generate migration files from schema changes. + +## Usage + +```bash +vix orm migrate [options] +vix orm rollback --steps [options] +vix orm status [options] +vix orm makemigrations --new [options] +``` + +## Subcommands + +| Subcommand | Purpose | +|------------|---------| +| `migrate` | Apply pending migrations | +| `rollback` | Roll back applied migrations | +| `status` | Show migration status | +| `makemigrations` | Generate migration files from schema changes | + +## Basic usage + +```bash +vix orm migrate --db blog_db --dir ./migrations +vix orm status --db blog_db +vix orm rollback --steps 1 --db blog_db +vix orm makemigrations \ + --new ./schema.new.json \ + --snapshot ./schema.json \ + --dir ./migrations \ + --name create_users \ + --dialect mysql +``` + +## Migrate + +```bash +vix orm migrate --db blog_db --dir ./migrations +``` + +## Rollback + +```bash +vix orm rollback --steps 1 --db blog_db --dir ./migrations +vix orm rollback --steps 2 --db blog_db +``` + +`--steps` is required. + +## Status + +```bash +vix orm status --db blog_db +``` + +## Makemigrations + +```bash +vix orm makemigrations --new ./schema.new.json + +vix orm makemigrations \ + --new ./schema.new.json \ + --snapshot ./schema.json \ + --dir ./migrations \ + --name create_users \ + --dialect mysql +``` + +Supported dialects: `mysql` (default), `sqlite` + +## Common options + +| Option | Description | +|--------|-------------| +| `--db ` | Database name. Overrides `VIX_ORM_DB`. | +| `--dir ` | Migrations directory. Overrides `VIX_ORM_DIR`. | +| `--host ` | MySQL URI. Default is `tcp://127.0.0.1:3306`. | +| `--user ` | Database user. Default is `root`. | +| `--pass ` | Database password. | +| `--project-dir ` | Force project root detection. | +| `--tool ` | Override migrator executable path. | +| `-h, --help` | Show command help. | + +## Makemigrations options + +| Option | Description | +|--------|-------------| +| `--new ` | New schema JSON file. Required. | +| `--snapshot ` | Previous snapshot. Default is `schema.json`. | +| `--name