[
  {
    "path": ".cargo/config.toml",
    "content": "[alias]\n\nxtask = \"run --package xtask --\"\n\n# https://github.com/rust-lang/rust/issues/141626\n# (can be removed once link.exe is fixed)\n[target.x86_64-pc-windows-msvc]\nlinker = \"rust-lld\"\n"
  },
  {
    "path": ".clippy.toml",
    "content": "cognitive-complexity-threshold = 40\n"
  },
  {
    "path": ".gitattributes",
    "content": "* linguist-vendored\n*.rs linguist-vendored=false\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "github: loco-rs "
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug-report.md",
    "content": "---\nname: Bug Report\nabout: Report behavior that deviates from specification or expectation\ntitle: \"\"\nlabels: assessment\nassignees: \"\"\n---\n\n**Description**\n\nProvide a clear and concise explanation of the bug, including references to any conflicting documentation or an elucidation of why the current functionality doesn't align with your expectations.\n\n**To Reproduce**\n\nPlease outline the steps to replicate the bug, and if possible, provide a comprehensive code example consisting of both `main.rs` and `Cargo.toml`` files. A complete and functional code snippet would be highly appreciated.\n\n**Expected Behavior**\n\nA clear and concise description of what you expected to happen.\n\n**Environment:**\n\n**Additional Context**\n\nInclude additional context about the issue in this section. For instance, share insights into the bug's discovery process or any hypotInclude additional context about the issue in this section. For instance, share insights into the bug's discovery process or any hypotheses you may have regarding the root cause or specific aspects where Loco may be malfunctioning.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: true\ncontact_links:\n  - name: Question\n    url: https://github.com/loco-rs/loco/discussions\n    about: Please ask questions or raise indefinite concerns on Discussions\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature-request.md",
    "content": "---\nname: Feature Request\nabout: Suggest a new feature\ntitle: \"\"\nlabels: enhancement\nassignees: \"\"\n---\n\n## Feature Request\n\n**Is your feature request related to a problem? Please describe.**\n\nProvide a succinct and clear description of the problem. For example, I encounter an issue when...\n\n**Describe the solution you'd like**\n\nArticulate a clear and concise depiction of your desired outcome.\n\n**Describe alternatives you've considered**\n\nOffer a clear and concise description of any alternative solutions or features you have contemplated.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/suggestion.md",
    "content": "---\nname: Suggestion\nabout: Suggest a change or improvement to existing functionality\n\ntitle: \"\"\nlabels: assessment\nassignees: \"\"\n---\n\n**Description**\n\nPlease provide a well-defined and succinct explanation of the issue. Include references to any conflicting documentation or clarify why the current functionality deviates from your expectations.\n\n**To Reproduce**\n\nPlease outline the steps to replicate the bug, and if possible, provide a comprehensive code example consisting of both `main.rs` and `Cargo.toml`` files. A complete and functional code snippet would be highly appreciated.\n\n**Expected Behavior**\n\nProvide a straightforward and brief explanation of your anticipated outcome.\n\n**Environment:**\n\n**Additional Context**\n\nInclude additional details about the issue in this section. For instance, share insights into how you discovered the bug or any hypotheses you may have regarding what might be causing the problem with Loco.\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: \"cargo\"\n    directory: \"/\"\n    schedule:\n      interval: \"daily\"\n    open-pull-requests-limit: 0\n    \n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: \"daily\"\n"
  },
  {
    "path": ".github/workflows/docs.yml",
    "content": "name: \"[docs]\"\n\non:\n  push:\n    branches:\n      - master\n  pull_request:\n\nenv:\n  RUST_TOOLCHAIN: stable\n  TOOLCHAIN_PROFILE: minimal\n\njobs:\n  check:\n    runs-on: ubuntu-latest\n\n    permissions:\n      contents: read\n\n    steps:\n      - name: Checkout the code\n        uses: actions/checkout@v6\n      - uses: dtolnay/rust-toolchain@stable\n        with:\n          toolchain: ${{ env.RUST_TOOLCHAIN }}\n      - run: cargo install snipdoc --features exec\n      - run: snipdoc check\n        continue-on-error: true\n        env:\n          SNIPDOC_SKIP_EXEC_COMMANDS: true\n"
  },
  {
    "path": ".github/workflows/loco-gen-ci.yml",
    "content": "name: \"[loco-gen:ci]\"\n\non:\n  push:\n    branches:\n      - master\n    paths:\n      - \"loco-gen/**\"\n  pull_request:\n    paths:\n      - \"loco-gen/**\"\n\nenv:\n  RUST_TOOLCHAIN: stable\n  TOOLCHAIN_PROFILE: minimal\n\ndefaults:\n  run:\n    working-directory: ./loco-gen\n\njobs:\n  style:\n    runs-on: ubuntu-latest\n\n    permissions:\n      contents: read\n\n    steps:\n      - name: Checkout the code\n        uses: actions/checkout@v6\n      - uses: dtolnay/rust-toolchain@stable\n        with:\n          toolchain: ${{ env.RUST_TOOLCHAIN }}\n          components: rustfmt\n      - name: Setup Rust cache\n        uses: Swatinem/rust-cache@v2\n      - name: Run cargo fmt\n        run: cargo fmt --all -- --check\n      - name: Run cargo clippy\n        run: cargo clippy --all-features -- -D warnings -W clippy::pedantic -W clippy::nursery -W rust-2018-idioms\n\n  test:\n    needs: [style]\n    runs-on: ubuntu-latest\n\n    permissions:\n      contents: read\n\n    services:\n      postgres:\n        image: postgres\n        env:\n          POSTGRES_DB: postgres_test\n          POSTGRES_USER: postgres\n          POSTGRES_PASSWORD: postgres\n        ports:\n          - \"5432:5432\"\n        # Set health checks to wait until postgres has started\n        options: --health-cmd pg_isready\n          --health-interval 10s\n          --health-timeout 5s\n          --health-retries 5\n\n    steps:\n      - name: Checkout the code\n        uses: actions/checkout@v6\n      - uses: dtolnay/rust-toolchain@stable\n        with:\n          toolchain: ${{ env.RUST_TOOLCHAIN }}\n      - name: Setup Rust cache\n        uses: Swatinem/rust-cache@v2\n\n      - name: Install seaorm cli\n        run: cargo install sea-orm-cli\n\n      - run: |\n          cargo install --path ../loco-new\n\n      - name: Run cargo test\n        run: cargo test --all-features\n        env:\n          LOCO_DEV_MODE_PATH: ${{ github.workspace }}\n          DATABASE_URL: postgres://postgres:postgres@localhost:5432/postgres_test\n"
  },
  {
    "path": ".github/workflows/loco-gen-deploy.yml",
    "content": "name: \"[loco-gen-deploy]\"\non:\n  schedule:\n    - cron: \"0 0 * * *\"\n\nenv:\n  RUST_TOOLCHAIN: stable\n  TOOLCHAIN_PROFILE: minimal\n\njobs:\n  g-deploy-docker:\n    # This workflow creates a new Loco application and builds a Docker image\n    # We only want this to run on the main repository (loco-rs/loco) and not on forks because:\n    # 1. It consumes GitHub Actions minutes unnecessarily on forks\n    # 2. The Docker build and deployment is specific to the main repository\n    # 3. Forks typically don't need to run this automated deployment process\n    if: github.repository == 'loco-rs/loco'\n    runs-on: ubuntu-latest\n\n    permissions:\n      contents: read\n\n    steps:\n      - name: Checkout the code\n        uses: actions/checkout@v6\n      - uses: dtolnay/rust-toolchain@stable\n        with:\n          toolchain: ${{ env.RUST_TOOLCHAIN }}\n      - name: Setup Rust cache\n        uses: Swatinem/rust-cache@v2\n\n      - name: Install seaorm cli\n        run: cargo install sea-orm-cli\n\n      - name: install 'loco new'\n        run: |\n          cargo install loco\n\n      - name: create myapp\n        run: |\n          loco new -n myapp --db sqlite --bg async --assets serverside -a\n\n      - name:\n        run: cargo loco generate deployment docker && docker build -t demo .\n        working-directory: ./myapp\n\n"
  },
  {
    "path": ".github/workflows/loco-new.yml",
    "content": "name: \"[loco-new:ci]\"\n\non:\n  push:\n    branches:\n      - master\n    paths:\n      - \"loco-new/**\"\n      - \"loco-gen/**\"\n  pull_request:\n    paths:\n      - \"loco-new/**\"\n      - \"loco-gen/**\"\n\nenv:\n  RUST_TOOLCHAIN: stable\n  TOOLCHAIN_PROFILE: minimal\n\njobs:\n  style:\n    runs-on: ubuntu-latest\n\n    permissions:\n      contents: read\n\n    steps:\n      - name: Checkout the code\n        uses: actions/checkout@v6\n      - uses: dtolnay/rust-toolchain@stable\n        with:\n          toolchain: ${{ env.RUST_TOOLCHAIN }}\n          components: rustfmt\n      - name: Setup Rust cache\n        uses: Swatinem/rust-cache@v2\n      - run: cargo fmt --all -- --check\n        working-directory: ./loco-new\n      - name: Run cargo clippy\n        run: cargo clippy --all-features -- -D warnings -W clippy::pedantic -W clippy::nursery -W rust-2018-idioms\n        working-directory: ./loco-new\n\n  test:\n    # needs: [style]\n    runs-on: ${{ matrix.os }}\n    strategy:\n      matrix:\n        os: [ubuntu-latest, windows-latest]\n\n    permissions:\n      contents: read\n\n    steps:\n      - name: Checkout the code\n        uses: actions/checkout@v6\n\n      - uses: dtolnay/rust-toolchain@stable\n        with:\n          toolchain: ${{ env.RUST_TOOLCHAIN }}\n\n      - name: Setup Rust cache\n        uses: Swatinem/rust-cache@v2\n\n      - name: Configure sccache\n        run: |\n          echo \"RUSTC_WRAPPER=sccache\" >> $GITHUB_ENV\n          echo \"SCCACHE_GHA_ENABLED=true\" >> $GITHUB_ENV\n\n      - name: Run sccache-cache\n        uses: mozilla-actions/sccache-action@v0.0.9\n\n      - name: Install seaorm cli\n        run: cargo install sea-orm-cli\n\n      - name: Free Disk Space\n        if: ${{ matrix.os == 'ubuntu-latest' }}\n        uses: jlumbroso/free-disk-space@main\n        with:\n          tool-cache: false\n\n      - name: Run cargo test\n        run: cargo test --all-features -- --test-threads 1\n        working-directory: ./loco-new\n        env:\n          LOCO_DEV_MODE_PATH: ${{ github.workspace }}\n          # NOTE NOTE NOTE: this is for optimizing build and may result in strange behavior\n          CARGO_TARGET_DIR: /tmp/shared-target\n"
  },
  {
    "path": ".github/workflows/loco-rs-ci-sanity.yml",
    "content": "# To optimize CI runtime:\n# A simpler \"sanity check\" workflow is introduced. \n# This workflow only runs if changes in the PR do NOT include \n# the `loco-gen` or `loco-new` paths.\n# (When changes are made to `loco-gen` or `loco-new`, \n# we run comprehensive tests to validate every generator command \n# and template option.)\n\n# Purpose of the sanity check:\n# It performs basic validation by comparing the local changes \n# against the templates. \n# If any breaking changes are detected in the templates, \n# the sanity check will fail, signaling an issue.\n\nname: \"[loco_rs:sanity]\"\n\non:\n  push:\n    branches:\n      - master\n  pull_request:\n\nenv:\n  RUST_TOOLCHAIN: stable\n  TOOLCHAIN_PROFILE: minimal\n\njobs:\n  sanity:\n    runs-on: ubuntu-latest\n\n    permissions:\n      contents: read\n\n    steps:\n      - name: Checkout the code\n        uses: actions/checkout@v6\n      - uses: dtolnay/rust-toolchain@stable\n        with:\n          toolchain: ${{ env.RUST_TOOLCHAIN }}\n      - name: Setup Rust cache\n        uses: Swatinem/rust-cache@v2\n\n      - name: Install seaorm cli\n        run: cargo install sea-orm-cli\n        \n      - run: cargo install --path loco-new\n      \n      - run: |\n          loco new -n myappdb --db sqlite --bg async --assets serverside -a\n          cd myappdb\n          cargo check\n          cargo build --release\n        env:\n          LOCO_DEV_MODE_PATH: ${{ github.workspace }}\n          \n      - run: |\n          loco new -n myapp --db none --bg async --assets none -a\n          cd myapp\n          cargo check\n          cargo build --release\n        env:\n          LOCO_DEV_MODE_PATH: ${{ github.workspace }}\n\n      "
  },
  {
    "path": ".github/workflows/loco-rs-ci.yml",
    "content": "name: \"[loco_rs:ci]\"\n\non:\n  push:\n    branches:\n      - master\n  pull_request:\n\nenv:\n  RUST_TOOLCHAIN: stable\n  TOOLCHAIN_PROFILE: minimal\n\njobs:\n  style:\n    runs-on: ubuntu-latest\n\n    permissions:\n      contents: read\n\n    steps:\n      - name: Checkout the code\n        uses: actions/checkout@v6\n      - uses: dtolnay/rust-toolchain@stable\n        with:\n          toolchain: ${{ env.RUST_TOOLCHAIN }}\n          components: rustfmt\n      - name: Setup Rust cache\n        uses: Swatinem/rust-cache@v2\n      - name: Run cargo fmt\n        run: cargo fmt --all -- --check\n      - name: Run cargo clippy\n        run: cargo clippy --all-features -- -D warnings -W clippy::pedantic -W clippy::nursery -W rust-2018-idioms\n\n  check:\n    needs: [style]\n    runs-on: ubuntu-latest\n\n    permissions:\n      contents: read\n\n    steps:\n      - name: Checkout the code\n        uses: actions/checkout@v6\n      - uses: dtolnay/rust-toolchain@stable\n        with:\n          toolchain: ${{ env.RUST_TOOLCHAIN }}\n      - name: Setup Rust cache\n        uses: Swatinem/rust-cache@v2\n      - uses: taiki-e/install-action@v2\n        with:\n          tool: cargo-hack\n      - run: cargo hack check --each-feature\n\n  build:\n    needs: [check, style]\n    runs-on: ubuntu-latest\n\n    permissions:\n      contents: read\n\n    steps:\n      - name: Checkout the code\n        uses: actions/checkout@v6\n      - uses: dtolnay/rust-toolchain@stable\n        with:\n          toolchain: ${{ env.RUST_TOOLCHAIN }}\n      - name: Setup Rust cache\n        uses: Swatinem/rust-cache@v2\n\n      - name: Run cargo build\n        run: cargo build --release\n  test:\n    needs: [check, style]\n    runs-on: ubuntu-latest\n\n    permissions:\n      contents: read\n\n    steps:\n      - name: Checkout the code\n        uses: actions/checkout@v6\n      - uses: dtolnay/rust-toolchain@stable\n        with:\n          toolchain: ${{ env.RUST_TOOLCHAIN }}\n      - name: Setup Rust cache\n        uses: Swatinem/rust-cache@v2\n\n      - name: Run cargo test\n        run: cargo test --all-features --workspace --exclude loco-gen --exclude loco\n"
  },
  {
    "path": ".gitignore",
    "content": "# local dev\ntodo.txt\ntodo.md\nexamples/demo2\nexamples/myapp\n*.sqlite\n*.sqlite-wal\n*.sqlite-shm\n\n*.sqlite3\n*.sqlite3-wal\n*.sqlite3-shm\n\n# IDE config files\n.idea\n.vscode\n\n**/config/local.yaml\n**/config/*.local.yaml\n\n# Local Netlify folder\n.netlify\n\n\n### macOS ###\n# General\n.DS_Store\n.AppleDouble\n.LSOverride\n\n# Icon must end with two \\r\nIcon\n\n# Thumbnails\n._*\n\n# Files that might appear in the root of a volume\n.DocumentRevisions-V100\n.fseventsd\n.Spotlight-V100\n.TemporaryItems\n.Trashes\n.VolumeIcon.icns\n.com.apple.timemachine.donotpresent\n\n# Directories potentially created on remote AFP share\n.AppleDB\n.AppleDesktop\nNetwork Trash Folder\nTemporary Items\n.apdisk\n\n### Rust ###\n# Generated by Cargo\n# will have compiled files and executables\ntarget/\nCargo.lock\n\n# These are backup files generated by rustfmt\n**/*.rs.bk\n\n## Frontend\n# Various node lock files; so people don't accidentally commit them while\n# updating the template\nstarters/saas/frontend/package-lock.json\nstarters/saas/frontend/yarn.lock\nstarters/saas/frontend/pnpm-lock.yaml\n\n\nexample"
  },
  {
    "path": ".rustfmt.toml",
    "content": "max_width = 100\nuse_small_heuristics = \"Default\"\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\n\n\n##  Unreleased\n- Fix `cargo fmt` error in `loco-new` ([#1669](https://github.com/loco-rs/loco/pull/1669))\n- Fix UUID pattern in form field generation ([#1665](https://github.com/loco-rs/loco/pull/1665))\n- Add tests for auth extractor ([#1671](https://github.com/loco-rs/loco/pull/1671))\n- Fix Clippy warnings for Rust 1.92 ([#1705](https://github.com/loco-rs/loco/pull/1705))\n- Add email headers support to mailer ([#1700](https://github.com/loco-rs/loco/pull/1700))\n- Wrap `TeraView` in `Arc` to reduce runtime memory usage ([#1703](https://github.com/loco-rs/loco/pull/1703))\n- Allow overriding a secure header ([#1659](https://github.com/loco-rs/loco/pull/1659))\n- Add “create user” task ([#1670](https://github.com/loco-rs/loco/pull/1670))\n- Add `UuidUniqWithDefault` and `UuidWithDefault` types ([#1642](https://github.com/loco-rs/loco/pull/1642))\n- Refactor users model to reuse `find_by_api_key` in `Authenticable` ([#1706](https://github.com/loco-rs/loco/pull/1706))\n- Split error detail generic parameters ([#1709](https://github.com/loco-rs/loco/pull/1709))\n- Update `loco-new` for new Rhai version ([#1704](https://github.com/loco-rs/loco/pull/1704))\n\n### Breaking Changes\nIn file `src/initializers/view_engine.rs`, modify the code lines in `after_routes`:\n\nBefore\n\n```rust\nasync fn after_routes(&self, router: AxumRouter, _ctx: &AppContext) -> Result<AxumRouter> {\n                :\n    engines::TeraView::build()?.post_process(move |tera| {\n                :\n```\n\nAfter (use `build_with_post_process` instead of `post_process`)\n\n```rust\nasync fn after_routes(&self, router: AxumRouter, _ctx: &AppContext) -> Result<AxumRouter> {\n                :\n    engines::TeraView::build_with_post_process(move |tera| {\n                :\n}\n```\n\n\n\n## v0.16.4 \n- Feat: decouple JWT authentication from database dependency. [https://github.com/loco-rs/loco/pull/1546](https://github.com/loco-rs/loco/pull/1546)\n- Fix: add sqlx dependency to with-db feature. [https://github.com/loco-rs/loco/pull/1557](https://github.com/loco-rs/loco/pull/1557)\n- Remove the deprecated `--link` generate command and fix the table name creation. [https://github.com/loco-rs/loco/pull/1556](https://github.com/loco-rs/loco/pull/1556)\n- Support underscore for migration join table. [https://github.com/loco-rs/loco/pull/1562](https://github.com/loco-rs/loco/pull/1562)\n- Fix: resolve deployment CLI argument parsing issue. [https://github.com/loco-rs/loco/pull/1566](https://github.com/loco-rs/loco/pull/1566)\n- Add database enum support (Postgres only). [https://github.com/loco-rs/loco/pull/1593](https://github.com/loco-rs/loco/pull/1568)\n- Remove duplicated #[async_trait::async_trait]. [https://github.com/loco-rs/loco/pull/1593](https://github.com/loco-rs/loco/pull/1572)\n- Clippy fixes for Rust 1.89. [https://github.com/loco-rs/loco/pull/1593](https://github.com/loco-rs/loco/pull/1593)\n- Improvement: do not hot-reload unless files have changed. [https://github.com/loco-rs/loco/pull/1552](https://github.com/loco-rs/loco/pull/1552)\n- Feat: add --without-tz flag for controlling timestamp generation. [https://github.com/loco-rs/loco/pull/1592](https://github.com/loco-rs/loco/pull/1592)\n- Support extra fields when generating the join table migration. [https://github.com/loco-rs/loco/pull/1595](https://github.com/loco-rs/loco/pull/1595)\n- Convert validator to trait-based API (add ValidatorTrait, keep derive adapter, update docs). [https://github.com/loco-rs/loco/pull/1597](https://github.com/loco-rs/loco/pull/1597)\n- Rename dockerfile to Dockerfile. [https://github.com/loco-rs/loco/pull/1574](https://github.com/loco-rs/loco/pull/1574)\n- Enable edit CORS expose headers. [https://github.com/loco-rs/loco/pull/1599](https://github.com/loco-rs/loco/pull/1599)\n- Adding new imports about multipart. [https://github.com/loco-rs/loco/pull/1600](https://github.com/loco-rs/loco/pull/1600)\n- Adding readiness default endpoint. [https://github.com/loco-rs/loco/pull/1563](https://github.com/loco-rs/loco/pull/1563)\n- Add Route methods to make collecting and nesting easier. [https://github.com/loco-rs/loco/pull/1608](https://github.com/loco-rs/loco/pull/1608)\n- Add streaming support for both download and upload. [https://github.com/loco-rs/loco/pull/1610](https://github.com/loco-rs/loco/pull/1610)\n- Fix Clippy for Rust 1.90. [https://github.com/loco-rs/loco/pull/1630](https://github.com/loco-rs/loco/pull/1630)\n- Loco CLI: Update rhai version. [https://github.com/loco-rs/loco/pull/1631](https://github.com/loco-rs/loco/pull/1631)\n\n\n## v0.16.3\n- Support nullable foreign keys with `references?` syntax. [https://github.com/loco-rs/loco/pull/1544](https://github.com/loco-rs/loco/pull/1544)\n- **HOTFIX**: **Breaking changes** Fixed a critical issue introduced in version `v0.16.2` that caused `cargo build --release` to fail after merging #1540. [https://github.com/loco-rs/loco/pull/1551](https://github.com/loco-rs/loco/pull/1551)\n- Add an API to re-send verification mail. [https://github.com/loco-rs/loco/pull/1456](https://github.com/loco-rs/loco/pull/1456)\n- Adding to ci cargo build --release. [https://github.com/loco-rs/loco/pull/1553](https://github.com/loco-rs/loco/pull/1553)\n\n### Breaking Changes\n\nIn file `src/initializers/view_engine.rs`, modify the method `after_routes`:\n\nBefore\n\n```rust\nasync fn after_routes(&self, router: AxumRouter, _ctx: &AppContext) -> Result<AxumRouter> {\n\t#[allow(unused_mut)]\n\tlet mut tera_engine = engines::TeraView::build()?;\n\tif std::path::Path::new(I18N_DIR).exists() {\n\t\tlet arc = ArcLoader::builder(&I18N_DIR, unic_langid::langid!(\"en-US\"))\n\t\t\t.shared_resources(Some(&[I18N_SHARED.into()]))\n\t\t\t.customize(|bundle| bundle.set_use_isolating(false))\n\t\t\t.build()\n\t\t\t.map_err(|e| Error::string(&e.to_string()))?;\n\t\t#[cfg(debug_assertions)]\n\t\ttera_engine\n\t\t\t.tera\n\t\t\t.lock()\n\t\t\t.expect(\"lock\")\n\t\t\t.register_function(\"t\", FluentLoader::new(arc));\n\n\t\t#[cfg(not(debug_assertions))]\n\t\ttera_engine\n\t\t\t.tera\n\t\t\t.register_function(\"t\", FluentLoader::new(arc));\n\t\tinfo!(\"locales loaded\");\n\t}\n\n\tOk(router.layer(Extension(ViewEngine::from(tera_engine))))\n}\n```\n\nAfter (use `post_process` to add i18n initialization code)\n\n```rust\nasync fn after_routes(&self, router: AxumRouter, _ctx: &AppContext) -> Result<AxumRouter> {\n\tlet tera_engine = if std::path::Path::new(I18N_DIR).exists() {\n\t\tlet arc = std::sync::Arc::new(\n\t\t\tArcLoader::builder(&I18N_DIR, unic_langid::langid!(\"en-US\"))\n\t\t\t\t.shared_resources(Some(&[I18N_SHARED.into()]))\n\t\t\t\t.customize(|bundle| bundle.set_use_isolating(false))\n\t\t\t\t.build()\n\t\t\t\t.map_err(|e| Error::string(&e.to_string()))?,\n\t\t);\n\t\tinfo!(\"locales loaded\");\n\n\t\tengines::TeraView::build()?.post_process(move |tera| {\n\t\t\ttera.register_function(\"t\", FluentLoader::new(arc.clone()));\n\t\t\tOk(())\n\t\t})?\n\t} else {\n\t\tengines::TeraView::build()?\n\t};\n\n\tOk(router.layer(Extension(ViewEngine::from(tera_engine))))\n}\n```\n## v0.16.2\n- Update auth import in the Authentication document. [https://github.com/loco-rs/loco/pull/1531](https://github.com/loco-rs/loco/pull/1531)\n- Adding cache control header to the static asset middleware. [https://github.com/loco-rs/loco/pull/1535](https://github.com/loco-rs/loco/pull/1535)\n- Fix borrow checker when sending config to handle_job_command when feature with-db is off. [https://github.com/loco-rs/loco/pull/1536](https://github.com/loco-rs/loco/pull/1536)\n- feat: add initializer health checks to doctor command. [https://github.com/loco-rs/loco/pull/1537](https://github.com/loco-rs/loco/pull/1537)\n- Update shuttle template to 0.56. [https://github.com/loco-rs/loco/pull/1518](https://github.com/loco-rs/loco/pull/1518)\n- Encapsulate post-processing into Tera engine creation. [https://github.com/loco-rs/loco/pull/1540](https://github.com/loco-rs/loco/pull/1540)\n- Adding QueryValidateWithMessage. [https://github.com/loco-rs/loco/pull/1521](https://github.com/loco-rs/loco/pull/1521)\n- Add S3 driver with credentials and endpoint support. [https://github.com/loco-rs/loco/pull/1539](https://github.com/loco-rs/loco/pull/1539)\n\n## v0.16.1\n- fix clippy result_large_err. [https://github.com/loco-rs/loco/pull/1496](https://github.com/loco-rs/loco/pull/1496)\n- chore: remove async-std. [https://github.com/loco-rs/loco/pull/1492](https://github.com/loco-rs/loco/pull/1492)\n- fix: Bump shuttle version to 0.55.0. [https://github.com/loco-rs/loco/pull/1488](https://github.com/loco-rs/loco/pull/1488)\n- Change the Docker building image to 1.87. [https://github.com/loco-rs/loco/pull/1475](https://github.com/loco-rs/loco/pull/1475)\n- Fix Clippy warnings for Rust 1.88 stable. [https://github.com/loco-rs/loco/pull/1519](https://github.com/loco-rs/loco/pull/1519) \n- Remove Migrator from boot_test_* doc comments. [https://github.com/loco-rs/loco/pull/1512](https://github.com/loco-rs/loco/pull/1512) \n- fix: use rust-lld linker on Windows. [https://github.com/loco-rs/loco/pull/1508](https://github.com/loco-rs/loco/pull/1508) \n- Fix precompressed in static assets. [https://github.com/loco-rs/loco/pull/1524](https://github.com/loco-rs/loco/pull/1524) \n- Support multiple JWT locations. [https://github.com/loco-rs/loco/pull/1497](https://github.com/loco-rs/loco/pull/1497) \n\n## v0.16.0\n\n**Note:** For detailed upgrade steps for breaking changes, see the [upgrade guide](https://loco.rs/docs/extras/upgrades/#upgrade-from-0-15-x-to-0-16-x).\n\n- chore: improve readability and performance by using map_err in Model. [https://github.com/loco-rs/loco/pull/1311](https://github.com/loco-rs/loco/pull/1311)\n- Allow testing the controller by passing a cookie. [https://github.com/loco-rs/loco/pull/1326](https://github.com/loco-rs/loco/pull/1326)\n- Support BigInt in the scaffold Array. [https://github.com/loco-rs/loco/pull/1304](https://github.com/loco-rs/loco/pull/1304)\n- Add `escape` Tera function to the scaffold list template. [https://github.com/loco-rs/loco/pull/1337](https://github.com/loco-rs/loco/pull/1337)\n- Return a specific error when logging in with a non-existent email. [https://github.com/loco-rs/loco/pull/1336](https://github.com/loco-rs/loco/pull/1336)\n- Return a specific error when trying to verify with an invalid token. [https://github.com/loco-rs/loco/pull/1340](https://github.com/loco-rs/loco/pull/1340)\n- Clippy 1.86. [https://github.com/loco-rs/loco/pull/1353](https://github.com/loco-rs/loco/pull/1353)\n- Fix the DB creation. [https://github.com/loco-rs/loco/pull/1352](https://github.com/loco-rs/loco/pull/1352)\n- YAML responses. [https://github.com/loco-rs/loco/pull/1360](https://github.com/loco-rs/loco/pull/1360)\n- Swap to the validators' built-in email validation. [https://github.com/loco-rs/loco/pull/1359](https://github.com/loco-rs/loco/pull/1359)\n- Cancellation tokens for the Postgres and SQLite background workers. [https://github.com/loco-rs/loco/pull/1365](https://github.com/loco-rs/loco/pull/1365)\n- docs: testing auth routes. [https://github.com/loco-rs/loco/pull/1303](https://github.com/loco-rs/loco/pull/1303)\n- Add comprehensive tests for the task module. [https://github.com/loco-rs/loco/pull/1386](https://github.com/loco-rs/loco/pull/1386)\n- Add comprehensive test coverage for the data module. [https://github.com/loco-rs/loco/pull/1387](https://github.com/loco-rs/loco/pull/1387)\n- Add validator extractors test suite. [https://github.com/loco-rs/loco/pull/1388](https://github.com/loco-rs/loco/pull/1388)\n- **Breaking changes** Replace sidekiq job management: existing Redis jobs incompatible. [https://github.com/loco-rs/loco/pull/1384](https://github.com/loco-rs/loco/pull/1384)\n- **Breaking changes** Add generic type support to the Cache API: cache method calls need type parameters. [https://github.com/loco-rs/loco/pull/1385](https://github.com/loco-rs/loco/pull/1385)\n- Adding cache redis driver + configuration instead of enabling from code. [https://github.com/loco-rs/loco/pull/1389](https://github.com/loco-rs/loco/pull/1389)\n- Ability to configure pragma for SQLite. [https://github.com/loco-rs/loco/pull/1346](https://github.com/loco-rs/loco/pull/1346)\n- **Breaking changes** swap to validators builtin email validation: custom email validator syntax changed. [https://github.com/loco-rs/loco/pull/1359](https://github.com/loco-rs/loco/pull/1359)\n- Optimize worker tag filtering string handling. [https://github.com/loco-rs/loco/pull/1396](https://github.com/loco-rs/loco/pull/1396)\n- Add test coverage for db.rs. [https://github.com/loco-rs/loco/pull/1400](https://github.com/loco-rs/loco/pull/1400)\n- Allow storage of arbitrary custom objects in AppContext. [https://github.com/loco-rs/loco/pull/1404](https://github.com/loco-rs/loco/pull/1404)\n- Improve deployment generator CLI. [https://github.com/loco-rs/loco/pull/1413](https://github.com/loco-rs/loco/pull/1413)\n- Move auth and validate to the extractor folder. [https://github.com/loco-rs/loco/pull/1414](https://github.com/loco-rs/loco/pull/1414)\n- Hot reload on extended Tera templates. [https://github.com/loco-rs/loco/pull/1416](https://github.com/loco-rs/loco/pull/1416)\n- **Breaking changes** Update the `init_logger` to use `AppContext` instead of config: function signature changed. [https://github.com/loco-rs/loco/pull/1418](https://github.com/loco-rs/loco/pull/1418)\n- Support embedded assets. [https://github.com/loco-rs/loco/pull/1427](https://github.com/loco-rs/loco/pull/1427)\n- **Removed dependencies:**\n  - [`hyper`](https://github.com/loco-rs/loco/pull/1430)\n  - [`thousands`](https://github.com/loco-rs/loco/pull/1431)\n  - [`cfg-if`](https://github.com/loco-rs/loco/pull/1432)\n  - [`reqwest`](https://github.com/loco-rs/loco/pull/1434)\n  - [`serde_variant`](https://github.com/loco-rs/loco/pull/1493)\n\n* **Dependency updates:**\n  - Bumped [`tokio`] to `1.45` and [`tokio-util`] to `0.7` ([#1435](https://github.com/loco-rs/loco/pull/1435))\n  - Bumped [`colored`] to `3.0` ([#1437](https://github.com/loco-rs/loco/pull/1437))\n  - Bumped [`rand`] to `0.9` ([#1439](https://github.com/loco-rs/loco/pull/1439))\n  - Bumped [`duct`] to `1.0` ([#1438](https://github.com/loco-rs/loco/pull/1438))\n  - Bumped [`redis`] to `0.31`, [`bb8`] to `0.9`, and [`bb8-redis`] to `0.23` ([commit `7e7be`](https://github.com/loco-rs/loco/commit/7e7bebe15f74c377c93d979aab41c52eb871d667))\n  - Updated Loco template crates ([#1440](https://github.com/loco-rs/loco/pull/1440))\n\n- Support custom flags from `sea-orm entity`. [https://github.com/loco-rs/loco/pull/1442](https://github.com/loco-rs/loco/pull/1442)\n- Better `loco new` cleanup folders. [https://github.com/loco-rs/loco/pull/1429](https://github.com/loco-rs/loco/pull/1429)\n- Remove legacy mailer derive macro code. [https://github.com/loco-rs/loco/pull/1472](https://github.com/loco-rs/loco/pull/1472)\n- Make extract_token and get_jwt_from_config fn public. [https://github.com/loco-rs/loco/pull/1495](https://github.com/loco-rs/loco/pull/1495)\n\n## v0.15.0\n\n- Added total_items to pagination view & response. [https://github.com/loco-rs/loco/pull/1197](https://github.com/loco-rs/loco/pull/1197)\n- Flatten (de)serialization of custom user claims. [https://github.com/loco-rs/loco/pull/1159](https://github.com/loco-rs/loco/pull/1159)\n- Updated validator to 0.20. [https://github.com/loco-rs/loco/pull/1199](https://github.com/loco-rs/loco/pull/1199)\n- Scaffold v2. [https://github.com/loco-rs/loco/pull/1209](https://github.com/loco-rs/loco/pull/1209)\n- Fix generator Docker deployment to support both server-side and client-side rendering. [https://github.com/loco-rs/loco/pull/1227](https://github.com/loco-rs/loco/pull/1227)\n- Docs: num_workers worker configuration. [https://github.com/loco-rs/loco/pull/1242](https://github.com/loco-rs/loco/pull/1242)\n- Smoother model validations. [https://github.com/loco-rs/loco/pull/1233](https://github.com/loco-rs/loco/pull/1233)\n- Docs: num_workers worker configuration. [https://github.com/loco-rs/loco/pull/1242](https://github.com/loco-rs/loco/pull/1242)\n- Ignore SQLite WAL and SHM files and update Cargo watch crate docs. [https://github.com/loco-rs/loco/pull/1254](https://github.com/loco-rs/loco/pull/1254)\n- Remove fs-err crate. [https://github.com/loco-rs/loco/pull/1253](https://github.com/loco-rs/loco/pull/1253)\n- Allows to run scheduler as part of cargo loco start. [https://github.com/loco-rs/loco/pull/1247](https://github.com/loco-rs/loco/pull/1247)\n- Added prefix and route nesting to AppRoutes. [https://github.com/loco-rs/loco/pull/1241](https://github.com/loco-rs/loco/pull/1241)\n- Replace hyper crate with axum. [https://github.com/loco-rs/loco/pull/1258](https://github.com/loco-rs/loco/pull/1258)\n- Remove mime crate. [https://github.com/loco-rs/loco/pull/1256](https://github.com/loco-rs/loco/pull/1256)\n- Support async tests. [https://github.com/loco-rs/loco/pull/1237](https://github.com/loco-rs/loco/pull/1237)\n- Change job queue status from cli. [https://github.com/loco-rs/loco/pull/1228](https://github.com/loco-rs/loco/pull/1228)\n- Handle panics in queue worker. [https://github.com/loco-rs/loco/pull/1274](https://github.com/loco-rs/loco/pull/1274)\n- Schema with defaults. [https://github.com/loco-rs/loco/pull/1273](https://github.com/loco-rs/loco/pull/1273)\n- Add data subsystem. [https://github.com/loco-rs/loco/pull/1267](https://github.com/loco-rs/loco/pull/1267)\n- Add \"endpoint\" arg to azure storage builder.[https://github.com/loco-rs/loco/pull/1317](https://github.com/loco-rs/loco/pull/1317)\n- Improve readability and performance by using map_err in Model. [https://github.com/loco-rs/loco/pull/1311](https://github.com/loco-rs/loco/pull/1311)\n\n### Breaking Changes\n\nIn module `loco_rs::auth::jwt` in struct `JWT`, the impl method `generate_token` signature has changed.\nMigration:\n\nBefore\n\n```rust\njwt.generate_token(&expiration, pid.clone(), None);\n```\n\nAfter\n\n```rust\njwt.generate_token(expiration, pid.clone(), Map::new());\n//                 ^ no \"&\"                 ^ serde_json::map (doesn't allocate in constructor)\n```\n\n## v0.14.1\n\n- Fix: bump shuttle to 0.51.0. [https://github.com/loco-rs/loco/pull/1169](https://github.com/loco-rs/loco/pull/1169)\n- Return 422 status code for JSON rejection errors. [https://github.com/loco-rs/loco/pull/1173](https://github.com/loco-rs/loco/pull/1173)\n- Address clippy warnings for Rust stable 1.84. [https://github.com/loco-rs/loco/pull/1168](https://github.com/loco-rs/loco/pull/1168)\n- Bump shuttle to 0.51.0. [https://github.com/loco-rs/loco/pull/1169](https://github.com/loco-rs/loco/pull/1169)\n- Return 422 status code for JSON rejection errors. [https://github.com/loco-rs/loco/pull/1173](https://github.com/loco-rs/loco/pull/1173)\n- Return json validation details response. [https://github.com/loco-rs/loco/pull/1174](https://github.com/loco-rs/loco/pull/1174)\n- Fix example command after generating schedule. [https://github.com/loco-rs/loco/pull/1176](https://github.com/loco-rs/loco/pull/1176)\n- Fixed independent features. [https://github.com/loco-rs/loco/pull/1177](https://github.com/loco-rs/loco/pull/1177)\n- Custom response header for redirect. [https://github.com/loco-rs/loco/pull/1186](https://github.com/loco-rs/loco/pull/1186)\n- Added run_on_start feature to scheduler. [https://github.com/loco-rs/loco/pull/1184](https://github.com/loco-rs/loco/pull/1184)\n- feat: public jwt extractor from non-mutable reference to parts. [https://github.com/loco-rs/loco/pull/1190](https://github.com/loco-rs/loco/pull/1190)\n\n## v0.14\n\n- feat: smart migration generator. you can now generate migration based on naming them for creating a table, adding columns, references, join tables and more. [https://github.com/loco-rs/loco/pull/1086](https://github.com/loco-rs/loco/pull/1086)\n- feat: `cargo loco routes` will now pretty-print routes\n- fix: guard jwt error behind feature flag. [https://github.com/loco-rs/loco/pull/1032](https://github.com/loco-rs/loco/pull/1032)\n- fix: logger file_appender not using the seperated format setting. [https://github.com/loco-rs/loco/pull/1036](https://github.com/loco-rs/loco/pull/1036)\n- seed cli command. [https://github.com/loco-rs/loco/pull/1046](https://github.com/loco-rs/loco/pull/1046)\n- Updated validator to 0.19. [https://github.com/loco-rs/loco/pull/993](https://github.com/loco-rs/loco/pull/993)\n  ### Breaking Changes\n  Bump validator to 0.19 in your local `Cargo.toml`\n- Testing helpers: simplified function calls + adding html selector. [https://github.com/loco-rs/loco/pull/1047](https://github.com/loco-rs/loco/pull/1047)\n\n  ### Breaking Changes\n\n  #### Updated Import Paths\n\n  The testing module import path has been updated. To adapt your code, update imports from:\n\n  ```rust\n  use loco_rs::testing;\n  ```\n\n  to:\n\n  ```rust\n  use testing::prelude::*;\n  ```\n\n  #### Simplified Function Calls\n\n  Function calls within the testing module no longer require the testing:: prefix. Update your code accordingly. For example:\n\n  Before:\n\n  ```rust\n  let boot = testing::boot_test::<App>().await.unwrap();\n  ```\n\n  After:\n\n  ```rust\n  let boot = boot_test::<App>().await.unwrap();\n  ```\n\n- implement commands to manage background jobs. [https://github.com/loco-rs/loco/pull/1071](https://github.com/loco-rs/loco/pull/1071)\n- magic link. [https://github.com/loco-rs/loco/pull/1085](https://github.com/loco-rs/loco/pull/1085)\n- infer migration. [https://github.com/loco-rs/loco/pull/1086](https://github.com/loco-rs/loco/pull/1086)\n- Remove unnecessary calls to 'register_tasks' functions in scheduler. [https://github.com/loco-rs/loco/pull/1100](https://github.com/loco-rs/loco/pull/1100)\n- implement commands to manage background jobs. [https://github.com/loco-rs/loco/pull/1071](https://github.com/loco-rs/loco/pull/1071)\n- expose hello_name for SMTP client config. [https://github.com/loco-rs/loco/pull/1057](https://github.com/loco-rs/loco/pull/1057)\n- use reqwest with rustls rather than openssl. [https://github.com/loco-rs/loco/pull/1058](https://github.com/loco-rs/loco/pull/1058)\n- more flexible config, take more values from ENV. [https://github.com/loco-rs/loco/pull/1058](https://github.com/loco-rs/loco/pull/1058)\n- refactor: Use opendal to replace object_store. [https://github.com/loco-rs/loco/pull/897](https://github.com/loco-rs/loco/pull/897)\n- allow override loco template. [https://github.com/loco-rs/loco/pull/1102](https://github.com/loco-rs/loco/pull/1102)\n- support custom config folder. [https://github.com/loco-rs/loco/pull/1081](https://github.com/loco-rs/loco/pull/1081)\n- feat: upgrade to Axum 8. [https://github.com/loco-rs/loco/pull/1130](https://github.com/loco-rs/loco/pull/1130)\n- create load config hook. [https://github.com/loco-rs/loco/pull/1143](https://github.com/loco-rs/loco/pull/1143)\n- initial impl new migration dsl. [https://github.com/loco-rs/loco/pull/1125](https://github.com/loco-rs/loco/pull/1125)\n- allow disable limit_payload middleware. [https://github.com/loco-rs/loco/pull/1113](https://github.com/loco-rs/loco/pull/1113)\n\n## v0.13.2\n\n- static fallback now returns 200 and not 404 [https://github.com/loco-rs/loco/pull/991](https://github.com/loco-rs/loco/pull/991)\n- cache system now has expiry [https://github.com/loco-rs/loco/pull/1006](https://github.com/loco-rs/loco/pull/1006)\n- fixed: http interface binding [https://github.com/loco-rs/loco/pull/1007](https://github.com/loco-rs/loco/pull/1007)\n- JWT claims now editable and public [https://github.com/loco-rs/loco/issues/988](https://github.com/loco-rs/loco/issues/988)\n- CORS now not enabled in dev mode to avoid friction [https://github.com/loco-rs/loco/pull/1009](https://github.com/loco-rs/loco/pull/1009)\n- fixed: task code generation now injects in all cases [https://github.com/loco-rs/loco/pull/1012](https://github.com/loco-rs/loco/pull/1012)\n\n**BREAKING**\nIn your `app.rs` add the following injection comment at the bottom:\n\n```rust\nfn register_tasks(tasks: &mut Tasks) {\n    tasks.register(tasks::user_report::UserReport);\n    tasks.register(tasks::seed::SeedData);\n    tasks.register(tasks::foo::Foo);\n    // tasks-inject (do not remove)\n}\n```\n\n- fix: seeding now sets autoincrement fields in the relevant DBs [https://github.com/loco-rs/loco/pull/1014](https://github.com/loco-rs/loco/pull/1014)\n- fix: avoid generating entities from queue tables when the queue backend is database based [https://github.com/loco-rs/loco/issues/1013](https://github.com/loco-rs/loco/issues/1013)\n- removed: channels moved to an initializer [https://github.com/loco-rs/loco/issues/892](https://github.com/loco-rs/loco/issues/892)\n  **BREAKING**\n  See how this looks like in [https://github.com/loco-rs/chat-rooms](https://github.com/loco-rs/chat-rooms)\n\n## v0.13.0\n\n- Added SQLite background job support [https://github.com/loco-rs/loco/pull/969](https://github.com/loco-rs/loco/pull/969)\n- Added automatic updating of `updated_at` on change [https://github.com/loco-rs/loco/pull/962](https://github.com/loco-rs/loco/pull/962)\n- fixed codegen injection point in migrations [https://github.com/loco-rs/loco/pull/952](https://github.com/loco-rs/loco/pull/952)\n\n**NOTE: update your migration listing module like so:**\n\n```rust\n// migrations/src/lib.rs\n  vec![\n      Box::new(m20220101_000001_users::Migration),\n      Box::new(m20231103_114510_notes::Migration),\n      Box::new(m20240416_071825_roles::Migration),\n      Box::new(m20240416_082115_users_roles::Migration),\n      // inject-above (do not remove this comment)\n  ]\n```\n\nAdd the comment just before the closing array (`inject-above`)\n\n- Added ability to name references in [https://github.com/loco-rs/loco/pull/955](https://github.com/loco-rs/loco/pull/955):\n\n```sh\n$ generate scaffold posts title:string! content:string! written_by:references:users approved_by:references:users\n```\n\n- Added hot-reload like experience to Tera templates [https://github.com/loco-rs/loco/issues/977](https://github.com/loco-rs/loco/issues/977), in debug builds only.\n\n**NOTE: update your initializers `after_routes` like so:**\n\n```rust\n// src/initializers/view_engine.rs\nasync fn after_routes(&self, router: AxumRouter, _ctx: &AppContext) -> Result<AxumRouter> {\n    #[allow(unused_mut)]\n    let mut tera_engine = engines::TeraView::build()?;\n    if std::path::Path::new(I18N_DIR).exists() {\n        let arc = ArcLoader::builder(&I18N_DIR, unic_langid::langid!(\"en-US\"))\n            .shared_resources(Some(&[I18N_SHARED.into()]))\n            .customize(|bundle| bundle.set_use_isolating(false))\n            .build()\n            .map_err(|e| Error::string(&e.to_string()))?;\n        #[cfg(debug_assertions)]\n        tera_engine\n            .tera\n            .lock()\n            .expect(\"lock\")\n            .register_function(\"t\", FluentLoader::new(arc));\n\n        #[cfg(not(debug_assertions))]\n        tera_engine\n            .tera\n            .register_function(\"t\", FluentLoader::new(arc));\n        info!(\"locales loaded\");\n    }\n\n    Ok(router.layer(Extension(ViewEngine::from(tera_engine))))\n}\n```\n\n- `loco doctor` now checks for app-specific minimum dependency versions. This should help in upgrades. `doctor` also supports \"production only\" checks which you can run in production with `loco doctor --production`. This, for example, will check your connections but will not check dependencies. [https://github.com/loco-rs/loco/pull/931](https://github.com/loco-rs/loco/pull/931)\n- Use a single loco-rs dep for a whole project. [https://github.com/loco-rs/loco/pull/927](https://github.com/loco-rs/loco/pull/927)\n- chore: fix generated testcase. [https://github.com/loco-rs/loco/pull/939](https://github.com/loco-rs/loco/pull/939)\n- chore: Correct cargo test message. [https://github.com/loco-rs/loco/pull/938](https://github.com/loco-rs/loco/pull/938)\n- Add relevant meta tags for better defaults. [https://github.com/loco-rs/loco/pull/943](https://github.com/loco-rs/loco/pull/943)\n- Update cli message with correct command. [https://github.com/loco-rs/loco/pull/942](https://github.com/loco-rs/loco/pull/942)\n- remove lazy_static. [https://github.com/loco-rs/loco/pull/941](https://github.com/loco-rs/loco/pull/941)\n- change update HTTP verb semantics to put+patch. [https://github.com/loco-rs/loco/pull/919](https://github.com/loco-rs/loco/pull/919)\n- Fixed HTML scaffold error. [https://github.com/loco-rs/loco/pull/960](https://github.com/loco-rs/loco/pull/960)\n- Scaffolded HTML update method should be POST. [https://github.com/loco-rs/loco/pull/963](https://github.com/loco-rs/loco/pull/963)\n\n## v0.12.0\n\nThis release have been primarily about cleanups and simplification.\n\nPlease update:\n\n- `loco-rs`\n- `loco-cli`\n\nChanges:\n\n- **generators (BREAKING)**: all prefixes in starters (e.g. `/api`) are now _local to each controller_, and generators will be prefix-aware (`--api` generator will add an `/api` prefix to controllers) [https://github.com/loco-rs/loco/pull/818](https://github.com/loco-rs/loco/pull/818)\n\nTo migrate, please move prefixes from `app.rs` to each controller you use in `controllers/`, for example in `notes` controller:\n\n```rust\nRoutes::new()\n    .prefix(\"api/notes\")\n    .add(\"/\", get(list))\n```\n\n- **starters**: removed `.devcontainer` which can now be found in [loco-devcontainer](https://github.com/loco-rs/loco-devcontainer)\n- **starters**: removed example `notes` scaffold (model, controllers, etc), and unified `user` and `auth` into a single file: `auth.rs`\n- **generators**: `scaffold` generator will now generate a CRUD with `PUT` and `PATCH` semantics for updating an entity [https://github.com/loco-rs/loco/issues/896](https://github.com/loco-rs/loco/issues/896)\n- **cleanup**: `loco-extras` was moved out of the repo, but we've incorporated `MultiDB` and `ExtraDB` from `extras` into `loco-rs` [https://github.com/loco-rs/loco/pull/917](https://github.com/loco-rs/loco/pull/917)\n\n- `cargo loco doctor` now checks for minimal required SeaORM CLI version\n- **BREAKING** Improved migration generator. If you have an existing migration project, add the following comment indicator to the top of the `vec` statement and right below the opening bracked like so in `migration/src/lib.rs`:\n\n```rust\n    fn migrations() -> Vec<Box<dyn MigrationTrait>> {\n        vec![\n            // inject-below (do not remove this comment)\n```\n\n## v0.11.0\n\n- Upgrade **SeaORM to v1.1.0**\n- Added OpenAPI example\n- Improve health route [https://github.com/loco-rs/loco/pull/851](https://github.com/loco-rs/loco/pull/851)\n- Add good pragmas to Sqlite [https://github.com/loco-rs/loco/pull/848](https://github.com/loco-rs/loco/pull/848)\n- Upgrade to rsbuild 1.0. [https://github.com/loco-rs/loco/pull/792](https://github.com/loco-rs/loco/pull/792)\n- Implements fmt::Debug to pub structs. [https://github.com/loco-rs/loco/pull/812](https://github.com/loco-rs/loco/pull/812)\n- Add num_workers config for sidekiq queue. [https://github.com/loco-rs/loco/pull/823](https://github.com/loco-rs/loco/pull/823)\n- Fix some comments in the starters and example code. [https://github.com/loco-rs/loco/pull/824](https://github.com/loco-rs/loco/pull/824)\n- Fix Y2038 bug for JWT on 32 bit platforms. [https://github.com/loco-rs/loco/pull/825](https://github.com/loco-rs/loco/pull/825)\n- Make App URL in Boot Banner Clickable. [https://github.com/loco-rs/loco/pull/826](https://github.com/loco-rs/loco/pull/826)\n- Add `--no-banner` flag to allow disabling the banner display. [https://github.com/loco-rs/loco/pull/839](https://github.com/loco-rs/loco/pull/839)\n- add on_shutdown hook. [https://github.com/loco-rs/loco/pull/842](https://github.com/loco-rs/loco/pull/842)\n\n## v0.10.1\n\n- `Format(respond_to): Format` extractor in controller can now be replaced with `respond_to: RespondTo` extractor for less typing.\n- When supplying data to views, you can now use `data!` instead of `serde_json::json!` for shorthand.\n- Refactor middlewares. [https://github.com/loco-rs/loco/pull/785](https://github.com/loco-rs/loco/pull/785). Middleware selection, configuration, and tweaking is MUCH more powerful and convenient now. You can keep the `middleware:` section empty or remove it now, see more in [the middleware docs](https://loco.rs/docs/the-app/controller/#middleware)\n- **NEW (BREAKING)** background worker subsystem is now queue agnostic. Providing for both Redis and Postgres with a change of configuration. This means you can now use a full-Postgres stack to remove Redis as a dependency if you wish. Here are steps to migrate your codebase:\n\n```rust\n// in your app.rs, change the worker registration code:\n\n// BEFORE\nfn connect_workers<'a>(p: &'a mut Processor, ctx: &'a AppContext) {\n    p.register(DownloadWorker::build(ctx));\n}\n\n// AFTER\nasync fn connect_workers(ctx: &AppContext, queue: &Queue) -> Result<()>{\n    queue.register(DownloadWorker::build(ctx)).await?;\n    Ok(())\n}\n\n// in your app.rs, replace the `worker` module references.\n// REMOVE\nworker::{AppWorker, Processor},\n// REPLACE WITH\nbgworker::{BackgroundWorker, Queue},\n\n// in your workers change the signature, and add the `build` function\n\n// BEFORE\nimpl worker::Worker<DownloadWorkerArgs> for DownloadWorker {\n    async fn perform(&self, args: DownloadWorkerArgs) -> worker::Result<()> {\n\n// AFTER\n#[async_trait]\nimpl BackgroundWorker<DownloadWorkerArgs> for DownloadWorker {\n    fn build(ctx: &AppContext) -> Self {\n        Self { ctx: ctx.clone() }\n    }\n    async fn perform(&self, args: DownloadWorkerArgs) -> Result<()> {\n\n// Finally, remove the `AppWorker` trait implementation completely.\n\n// REMOVE\nimpl worker::AppWorker<DownloadWorkerArgs> for DownloadWorker {\n    fn build(ctx: &AppContext) -> Self {\n        Self { ctx: ctx.clone() }\n    }\n}\n```\n\nFinally, update your `development.yaml` and `test.yaml` with a `kind`:\n\n```yaml\nqueue:\n  kind: Redis # add this to the existing `queue` section\n```\n\n- **UPGRADED (BREAKING)**: `validator` crate was upgraded which require some small tweaks to work with the new API:\n\n```rust\n// BEFORE:\n#[validate(custom = \"validation::is_valid_email\")]\npub email: String,\n\n// AFTER:\n#[validate(custom (function = \"validation::is_valid_email\"))]\npub email: String,\n```\n\nThen update your `Cargo.toml` to take version `0.18`:\n\n```toml\n# update\nvalidator = { version = \"0.18\" }\n```\n\n- **UPGRADED (BREAKING)**: `axum-test` crate was upgraded\n  Update your `Cargo.toml` to version `16`:\n\n```toml\n# update\naxum-test = { version = \"16\" }\n```\n\n## v0.9.0\n\n- Add fallback behavior. [https://github.com/loco-rs/loco/pull/732](https://github.com/loco-rs/loco/pull/732)\n- Add Scheduler Feature for Running Cron Jobs. [https://github.com/loco-rs/loco/pull/735](https://github.com/loco-rs/loco/pull/735)\n- Add `--html`, `--htmx` and `--api` flags to scaffold CLI command. [https://github.com/loco-rs/loco/pull/749](https://github.com/loco-rs/loco/pull/749)\n- Add base template for scaffold generation. [https://github.com/loco-rs/loco/pull/752](https://github.com/loco-rs/loco/pull/752)\n- Connect Redis only when the worker is BackgroundQueue. [https://github.com/loco-rs/loco/pull/755](https://github.com/loco-rs/loco/pull/755)\n- Add loco doctor --config. [https://github.com/loco-rs/loco/pull/736](https://github.com/loco-rs/loco/pull/736)\n- Rename demo: blo -> demo_app. [https://github.com/loco-rs/loco/pull/741](https://github.com/loco-rs/loco/pull/741)\n\n## v0.8.1\n\n- fix: introduce secondary binary for compile-and-run on Windows. [https://github.com/loco-rs/loco/pull/727](https://github.com/loco-rs/loco/pull/727)\n\n## v0.8.0\n\n- Added: loco-cli (`loco new`) now receives options from CLI and/or interactively asks for configuration options such as which asset pipeline, background worker type, or database provider to use.\n- Fix: custom queue names now merge with default queues.\n- Added `remote_ip` middleware for resolving client remote IP when under a proxy or loadbalancer, similar to the Rails `remote_ip` middleware.\n- Added `secure_headers` middleware for setting secure headers by default, similar to how [https://github.com/github/secure_headers](https://github.com/github/secure_headers) works. This is now ON by default to promote security-by-default.\n- Added: `money`, `blob` types to entitie generator.\n\n## 0.7.0\n\n- Moving to _timezone aware timestamps_. From now on migrations will generate **timestamps with time zone** by default. Moving to TZ aware timestamps in combination with newly revamped timestamp code generation in SeaORM v1.0.0 finally allows for _seamlessly_ moving between using `sqlite` and `postgres` with minimal or no entities code changes (resolved [this long standing issue](https://github.com/loco-rs/loco/issues/518#issuecomment-2051708319)). TZ aware timestamps also aligns us with how Rails works today (initially Rails had a no-tz timestamps, and today the default is to use timestamps). If not specified the TZ is the server TZ, which is usually UTC, therefore semantically this is almost like a no-tz timestamp.\n\n**A few highlights:**\n\nGenerated entities will now always use `DateTimeWithTimeZone` for the default timestamp fields:\n\n```\n...\nGenerating users.rs\n    > Column `created_at`: DateTimeWithTimeZone, not_null\n    > Column `updated_at`: DateTimeWithTimeZone, not_null\n...\n```\n\nFor better cross database provider compatibility, from now on prefer the `tstz` type instead of just `ts` when using generators (i.e. `cargo loco generate model movie released:tstz`)\n\n- remove eyer lib. [https://github.com/loco-rs/loco/pull/650](https://github.com/loco-rs/loco/pull/650)\n\n  ### Breaking Changes:\n\n  1.  Update the Main Function in src/bin/main\n\n      Replace the return type of the main function:\n\n      **Before:**\n\n      ```rust\n      async fn main() -> eyre::Result<()>\n      ```\n\n      **After:**\n\n      ```rust\n      async fn main() -> loco_rs::Result<()>\n      ```\n\n  2.  Modify examples/playground.rs\n      You need to apply two changes here:\n\n          a. Update the Function Signature\n          **Before:**\n\n          ```rust\n          async fn main() -> eyre::Result<()>\n          ```\n\n          **After:**\n\n          ```rust\n          async fn main() -> loco_rs::Result<()>\n          ```\n\n          b. Adjust the Context Handling\n          **Before:**\n\n          ```rust\n          let _ctx = playground::<App>().await.context(\"playground\")?;\n          ```\n\n          **After:**\n\n          ```rust\n          let _ctx = playground::<App>().await?;\n          ```\n\n      Note,\n      If you are using eyre in your project, you can continue to do so. We have only removed this crate from our base code dependencies.\n\n- Bump rstest crate to 0.21.0. [https://github.com/loco-rs/loco/pull/650](https://github.com/loco-rs/loco/pull/650)\n- Bump serial_test crate to 3.1.1. [https://github.com/loco-rs/loco/pull/651](https://github.com/loco-rs/loco/pull/651)\n- Bumo object store to create to 0.10.2. [https://github.com/loco-rs/loco/pull/654](https://github.com/loco-rs/loco/pull/654)\n- Bump axum crate to 0.7.5. [https://github.com/loco-rs/loco/pull/652](https://github.com/loco-rs/loco/pull/652)\n- Add Hooks::before_routes to give user control over initial axum::Router construction. [https://github.com/loco-rs/loco/pull/646](https://github.com/loco-rs/loco/pull/646)\n- Support logger file appender. [https://github.com/loco-rs/loco/pull/636](https://github.com/loco-rs/loco/pull/636)\n- Response from the template. [https://github.com/loco-rs/loco/pull/682](https://github.com/loco-rs/loco/pull/682)\n- Add get_or_insert function to cache layer. [https://github.com/loco-rs/loco/pull/637](https://github.com/loco-rs/loco/pull/637)\n- Bump ORM create to 1.0.0. [https://github.com/loco-rs/loco/pull/684](https://github.com/loco-rs/loco/pull/684)\n\n## 0.6.2\n\n- Use Rust-based tooling for SaaS starter frontend. [https://github.com/loco-rs/loco/pull/625](https://github.com/loco-rs/loco/pull/625)\n- Default binding to localhost to avoid firewall dialogues during development on macOS. [https://github.com/loco-rs/loco/pull/627](https://github.com/loco-rs/loco/pull/627)\n- upgrade sea-orm to 1.0.0 RC 7. [https://github.com/loco-rs/loco/pull/627](https://github.com/loco-rs/loco/pull/639)\n- Add a down migration command. [https://github.com/loco-rs/loco/pull/414](https://github.com/loco-rs/loco/pull/414)\n- replace create_postgres_database function table_name to db_name. [https://github.com/loco-rs/loco/pull/647](https://github.com/loco-rs/loco/pull/647)\n\n## 0.6.1\n\n- Upgrade htmx generator to htmx2. [https://github.com/loco-rs/loco/pull/629](https://github.com/loco-rs/loco/pull/629)\n\n## 0.6.0 https://github.com/loco-rs/loco/pull/610\n\n- Bump socketioxide to v0.13.1. [https://github.com/loco-rs/loco/pull/594](https://github.com/loco-rs/loco/pull/594)\n- Add CC and BCC fields to the mailers. [https://github.com/loco-rs/loco/pull/599](https://github.com/loco-rs/loco/pull/599)\n- Delete reset tokens after use. [https://github.com/loco-rs/loco/pull/602](https://github.com/loco-rs/loco/pull/602)\n- Generator html support delete entity. [https://github.com/loco-rs/loco/pull/604](https://github.com/loco-rs/loco/pull/604)\n- **Breaking changes** move task args from BTreeMap to struct. [https://github.com/loco-rs/loco/pull/609](https://github.com/loco-rs/loco/pull/609)\n  - Change task signature from `async fn run(&self, app_context: &AppContext, vars: &BTreeMap<String, String>)` to `async fn run(&self, _app_context: &AppContext, _vars: &task::Vars) -> Result<()>`\n  - **Breaking changes** change default port to 5150. [https://github.com/loco-rs/loco/pull/611](https://github.com/loco-rs/loco/pull/611)\n- Update shuttle version in deployment generation. [https://github.com/loco-rs/loco/pull/616](https://github.com/loco-rs/loco/pull/616)\n\n## v0.5.0 https://github.com/loco-rs/loco/pull/593\n\n- refactor auth middleware for supporting bearer, cookie and query. [https://github.com/loco-rs/loco/pull/560](https://github.com/loco-rs/loco/pull/560)\n- SeaORM upgraded: `rc1` -> `rc4`. [https://github.com/loco-rs/loco/pull/585](https://github.com/loco-rs/loco/pull/585)\n- Adding Cache to app content. [https://github.com/loco-rs/loco/pull/570](https://github.com/loco-rs/loco/pull/570)\n- Apply a layer to a specific handler using `layer` method. [https://github.com/loco-rs/loco/pull/554](https://github.com/loco-rs/loco/pull/554)\n- Add the debug macro to the templates to improve the errors. [https://github.com/loco-rs/loco/pull/547](https://github.com/loco-rs/loco/pull/547)\n- Opentelemetry initializer. [https://github.com/loco-rs/loco/pull/531](https://github.com/loco-rs/loco/pull/531)\n- Refactor auth middleware for supporting bearer, cookie and query [https://github.com/loco-rs/loco/pull/560](https://github.com/loco-rs/loco/pull/560)\n- Add redirect response [https://github.com/loco-rs/loco/pull/563](https://github.com/loco-rs/loco/pull/563)\n- **Breaking changes** Adding a custom claims `Option<serde_json::Value>` to the `UserClaims` struct (type changed). [https://github.com/loco-rs/loco/pull/578](https://github.com/loco-rs/loco/pull/578)\n- **Breaking changes** Refactored DSL and Pagination: namespace changes. [https://github.com/loco-rs/loco/pull/566](https://github.com/loco-rs/loco/pull/566)\n  - Replaced `model::query::dsl::` with `model::query`.\n  - Replaced `model::query::exec::paginate` with `model::query::paginate`.\n  - Updated the `PaginatedResponse` struct. Refer to its usage example [here](https://github.com/loco-rs/loco/blob/master/examples/demo/src/views/notes.rs#L29).\n- **Breaking changes** When introducing the Cache system which is much more flexible than having just Redis, we now call the 'redis' member simply a 'queue' which indicates it should be used only for the internal queue and not as a general purpose cache. In the application configuration setting `redis`, change to `queue`. [https://github.com/loco-rs/loco/pull/590](https://github.com/loco-rs/loco/pull/590)\n\n```yaml\n# before:\nredis:\n# after:\nqueue:\n```\n\n- **Breaking changes** We have made a few parts of the context pluggable, such as the `storage` and new `cache` subsystems, this is why we decided to let you configure the context entirely before starting up your app. As a result, if you have a storage building hook code it should move to `after_context`, see example [here](https://github.com/loco-rs/loco/pull/570/files#diff-5534e8826fb82e5c7f2587d270a51b48009341e79889d1504e6b63b2f0b652bdR83). [https://github.com/loco-rs/loco/pull/570](https://github.com/loco-rs/loco/pull/570)\n\n## v0.4.0\n\n- Refactored model validation for better developer experience. Added a few traits and structs to `loco::prelude` for a smoother import story. Introducing `Validatable`:\n\n```rust\nimpl Validatable for super::_entities::users::ActiveModel {\n    fn validator(&self) -> Box<dyn Validate> {\n        Box::new(Validator {\n            name: self.name.as_ref().to_owned(),\n            email: self.email.as_ref().to_owned(),\n        })\n    }\n}\n\n// now you can call `user.validate()` freely\n```\n\n- Refactored type field mapping to be centralized. Now model, scaffold share the same field mapping, so no more gaps like [https://github.com/loco-rs/loco/issues/513](https://github.com/loco-rs/loco/issues/513) (e.g. when calling `loco generate model title:string` the ability to map `string` into something useful in the code generation side)\n  **NOTE** the `_integer` class of types are now just `_int`, e.g. `big_int`, so that it correlate with the `int` field name in a better way\n\n- Adding to to quiery dsl `is_in` and `is_not_in`. [https://github.com/loco-rs/loco/pull/507](https://github.com/loco-rs/loco/pull/507)\n- Added: in your configuration you can now use an `initializers:` section for initializer specific settings\n\n  ```yaml\n  # Initializers Configuration\n  initializers:\n  # oauth2:\n  #   authorization_code: # Authorization code grant type\n  #     - client_identifier: google # Identifier for the OAuth2 provider. Replace 'google' with your provider's name if different, must be unique within the oauth2 config.\n  #       ... other fields\n  ```\n\n- Docs: fix schema data types mapping. [https://github.com/loco-rs/loco/pull/506](https://github.com/loco-rs/loco/pull/506)\n- Let Result accept other errors. [https://github.com/loco-rs/loco/pull/505](https://github.com/loco-rs/loco/pull/505)\n- Allow trailing slashes in URIs by adding the NormalizePathLayer. [https://github.com/loco-rs/loco/pull/481](https://github.com/loco-rs/loco/pull/481)\n- **BREAKING** Move from `Result<impl IntoResponse>` to `Result<Response>`. This enables much greater flexibility building APIs, where with `Result<Response>` you mix and match response types based on custom logic (returning JSON and HTML/String in the same route).\n- **Added**: mime responders similar to `respond_to` in Rails:\n\n1. Use the `Format` extractor\n2. Match on `respond_to`\n3. Create different content for different response formats\n\nThe following route will always return JSON, unless explicitly asked for HTML with a\n`Content-Type: text/html` (or `Accept: `) header:\n\n```rust\npub async fn get_one(\n    Format(respond_to): Format,\n    Path(id): Path<i32>,\n    State(ctx): State<AppContext>,\n) -> Result<Response> {\n    let item = load_item(&ctx, id).await?;\n    match respond_to {\n        RespondTo::Html => format::html(&format!(\"<html><body>{:?}</body></html>\", item.title)),\n        _ => format::json(item),\n    }\n}\n```\n\n## 0.3.2\n\n- Redisgin pagination. [https://github.com/loco-rs/loco/pull/463](https://github.com/loco-rs/loco/pull/463)\n- Wrap seaorm query and condition for common use cases. [https://github.com/loco-rs/loco/pull/463](https://github.com/loco-rs/loco/pull/463)\n- Adding to loco-extras initializer for extra or multiple db. [https://github.com/loco-rs/loco/pull/471](https://github.com/loco-rs/loco/pull/471)\n- Scaffold now supporting different templates such as API,HTML or htmx, this future is in beta.[https://github.com/loco-rs/loco/pull/474](https://github.com/loco-rs/loco/pull/474)\n- Fix generatore fields types + adding tests. [https://github.com/loco-rs/loco/pull/459](https://github.com/loco-rs/loco/pull/459)\n- Fix channel cors. [https://github.com/loco-rs/loco/pull/430](https://github.com/loco-rs/loco/pull/430)\n- Improve auth controller compatibility with frontend [https://github.com/loco-rs/loco/pull/472](https://github.com/loco-rs/loco/pull/472)\n\n## 0.3.1\n\n- **Breaking changes** Upgrade sea-orm to v1.0.0-rc.1. [https://github.com/loco-rs/loco/pull/420](https://github.com/loco-rs/loco/pull/420)\n  Needs to update `sea-orm` crate to use `v1.0.0-rc.1` version.\n- Implemented file upload support with versatile strategies. [https://github.com/loco-rs/loco/pull/423](https://github.com/loco-rs/loco/pull/423)\n- Create a `loco_extra` crate to share common basic implementations. [https://github.com/loco-rs/loco/pull/425](https://github.com/loco-rs/loco/pull/425)\n- Update shuttle deployment template to 0.38. [https://github.com/loco-rs/loco/pull/422](https://github.com/loco-rs/loco/pull/422)\n- Enhancement: Move the Serve to Hook flow with the ability to override default serve settings. [https://github.com/loco-rs/loco/pull/418](https://github.com/loco-rs/loco/pull/418)\n- Avoid cloning sea_query::ColumnDef. [https://github.com/loco-rs/loco/pull/415](https://github.com/loco-rs/loco/pull/415)\n- Allow required UUID type in a scaffold. [https://github.com/loco-rs/loco/pull/408](https://github.com/loco-rs/loco/pull/408)\n- Cover `SqlxMySqlPoolConnection` in db.rs. [https://github.com/loco-rs/loco/pull/411](https://github.com/loco-rs/loco/pull/411)\n- Update worker docs and change default worker mode. [https://github.com/loco-rs/loco/pull/412](https://github.com/loco-rs/loco/pull/412)\n- Added server-side view generation through a new `ViewEngine` infrastructure and `Tera` server-side templates: [https://github.com/loco-rs/loco/pull/389](https://github.com/loco-rs/loco/pull/389)\n- Added `generate model --migration-only` [https://github.com/loco-rs/loco/issues/400](https://github.com/loco-rs/loco/issues/400)\n- Add JSON to scaffold gen. [https://github.com/loco-rs/loco/pull/396](https://github.com/loco-rs/loco/pull/396)\n- Add --binding(-b) and --port(-b) to `cargo loco start`.[https://github.com/loco-rs/loco/pull/402](https://github.com/loco-rs/loco/pull/402)\n\n## 0.2.3\n\n- Add: support for [pre-compressed assets](https://github.com/loco-rs/loco/pull/370/files).\n- Added: Support socket channels, see working example [here](https://github.com/loco-rs/chat-rooms). [https://github.com/loco-rs/loco/pull/380](https://github.com/loco-rs/loco/pull/380)\n- refactor: optimize checking permissions on Postgres. [9416c](https://github.com/loco-rs/loco/commit/9416c5db85a27e3d30471374effec3fe88bf80a2)\n- Added: E2E db. [https://github.com/loco-rs/loco/pull/371](https://github.com/loco-rs/loco/pull/371)\n\n## v0.2.2\n\n- fix: public fields in mailer-op. [e51b7e](https://github.com/loco-rs/loco/commit/e51b7e64e7667c519451ac8a8bea574b2c5d4403)\n- fix: handle missing db permissions. [e51b7e](https://github.com/loco-rs/loco/commit/e51b7e64e7667c519451ac8a8bea574b2c5d4403)\n\n## v0.2.1\n\n- enable compression for CompressionLayer, not etag. [https://github.com/loco-rs/loco/pull/356](https://github.com/loco-rs/loco/pull/356)\n- Fix nullable JSONB column schema definition. [https://github.com/loco-rs/loco/pull/357](https://github.com/loco-rs/loco/pull/357)\n\n## v0.2.0\n\n- Add: Loco now has Initializers ([see the docs](https://loco.rs/docs/the-app/initializers/)). Initializers help you integrate infra into your app in a seamless way, as well as share pieces of setup code between your projects\n- Add: an `init_logger` hook in `src/app.rs` for those who want to take ownership of their logging and tracing stack.\n- Add: Return a JSON schema when payload json could not serialize to a struct. [https://github.com/loco-rs/loco/pull/343](https://github.com/loco-rs/loco/pull/343)\n- Init logger in cli.rs. [https://github.com/loco-rs/loco/pull/338](https://github.com/loco-rs/loco/pull/338)\n- Add: return JSON schema in panic HTTP layer. [https://github.com/loco-rs/loco/pull/336](https://github.com/loco-rs/loco/pull/336)\n- Add: JSON field support in model generation. [https://github.com/loco-rs/loco/pull/327](https://github.com/loco-rs/loco/pull/327) [https://github.com/loco-rs/loco/pull/332](https://github.com/loco-rs/loco/pull/332)\n- Add: float support in model generation. [https://github.com/loco-rs/loco/pull/317](https://github.com/loco-rs/loco/pull/317)\n- Fix: conflicting idx definition on M:M migration. [https://github.com/loco-rs/loco/issues/311](https://github.com/loco-rs/loco/issues/311)\n- Add: **Breaking changes** Supply `AppContext` to `routes` Hook. Migration steps in `src/app.rs`:\n\n```rust\n// src/app.rs: add app context to routes function\nimpl Hooks for App {\n  ...\n  fn routes(_ctx: &AppContext) -> AppRoutes;\n  ...\n}\n```\n\n- Add: **Breaking changes** change parameter type from `&str` to `&Environment` in `src/app.rs`\n\n```rust\n// src/app.rs: change parameter type for `environment` from `&str` to `&Environment`\nimpl Hooks for App {\n    ...\n    async fn boot(mode: StartMode, environment: &Environment) -> Result<BootResult> {\n        create_app::<Self>(mode, environment).await\n    }\n    ...\n```\n\n- Added: setting cookies:\n\n```rust\nformat::render()\n    .cookies(&[\n        cookie::Cookie::new(\"foo\", \"bar\"),\n        cookie::Cookie::new(\"baz\", \"qux\"),\n    ])?\n    .etag(\"foobar\")?\n    .json(notes)\n```\n\n## v0.1.9\n\n- Adding [pagination](https://loco.rs/docs/the-app/pagination/) on Models. [https://github.com/loco-rs/loco/pull/238](https://github.com/loco-rs/loco/pull/238)\n- Adding compression middleware. [https://github.com/loco-rs/loco/pull/205](https://github.com/loco-rs/loco/pull/205)\n  Added support for [compression middleware](https://docs.rs/tower-http/0.5.0/tower_http/compression/index.html).\n  usage:\n\n```yaml\nmiddlewares:\n  compression:\n    enable: true\n```\n\n- Create a new Database from the CLI. [https://github.com/loco-rs/loco/pull/223](https://github.com/loco-rs/loco/pull/223)\n- Validate if seaorm CLI is installed before running `cargo loco db entities` and show a better error to the user. [https://github.com/loco-rs/loco/pull/212](https://github.com/loco-rs/loco/pull/212)\n- Adding to `saas and `rest-api` starters a redis and DB in GitHub action workflow to allow users work with github action out of the box. [https://github.com/loco-rs/loco/pull/215](https://github.com/loco-rs/loco/pull/215)\n- Adding the app name and the environment to the DB name when creating a new starter. [https://github.com/loco-rs/loco/pull/216](https://github.com/loco-rs/loco/pull/216)\n- Fix generator when users adding a `created_at` or `update_at` fields. [https://github.com/loco-rs/loco/pull/214](https://github.com/loco-rs/loco/pull/214)\n- Add: `format::render` which allows a builder-like formatting, including setting etag and ad-hoc headers\n- Add: Etag middleware, enabled by default in starter projects. Once you set an Etag it will check for cache headers and return `304` if needed. To enable etag in your existing project:\n\n```yaml\n#...\nmiddlewares:\n  etag:\n    enable: true\n```\n\nusage:\n\n```rust\n  format::render()\n      .etag(\"foobar\")?\n      .json(Entity::find().all(&ctx.db).await?)\n```\n\n#### Authentication: Added API Token Authentication!\n\n- See [https://github.com/loco-rs/loco/pull/217](https://github.com/loco-rs/loco/pull/217)\n  Now when you generate a `saas starter` or `rest api` starter you will get additional authentication methods for free:\n\n- Added: authentication added -- **api authentication** where each user has an API token in the schema, and you can authenticate with `Bearer` against that user.\n- Added: authentication added -- `JWTWithUser` extractor, which is a convenience for resolving the authenticated JWT claims into a current user from database\n\n**migrating an existing codebase**\n\nAdd the following to your generated `src/models/user.rs`:\n\n```rust\n#[async_trait]\nimpl Authenticable for super::_entities::users::Model {\n    async fn find_by_api_key(db: &DatabaseConnection, api_key: &str) -> ModelResult<Self> {\n        let user = users::Entity::find()\n            .filter(users::Column::ApiKey.eq(api_key))\n            .one(db)\n            .await?;\n        user.ok_or_else(|| ModelError::EntityNotFound)\n    }\n\n    async fn find_by_claims_key(db: &DatabaseConnection, claims_key: &str) -> ModelResult<Self> {\n        super::_entities::users::Model::find_by_pid(db, claims_key).await\n    }\n}\n```\n\nUpdate imports in this file to include `model::Authenticable`:\n\n```rust\nuse loco_rs::{\n    auth, hash,\n    model::{Authenticable, ModelError, ModelResult},\n    validation,\n    validator::Validate,\n};\n```\n\n## v0.1.8\n\n- Added: `loco version` for getting an operable version string containing logical crate version and git SHA if available: `0.3.0 (<git sha>)`\n\nTo migrate to this behavior from earlier versions, it requires adding the following to your `app.rs` app hooks:\n\n```rust\n    fn app_version() -> String {\n        format!(\n            \"{} ({})\",\n            env!(\"CARGO_PKG_VERSION\"),\n            option_env!(\"BUILD_SHA\")\n                .or(option_env!(\"GITHUB_SHA\"))\n                .unwrap_or(\"dev\")\n        )\n    }\n```\n\nReminder: `loco --version` will give you the current Loco framework which your app was built against and `loco version` gives you your app version.\n\n- Added: `loco generate migration` for adding ad-hoc migrations\n- Added: added support in model generator for many-to-many link table generation via `loco generate model --link`\n- Docs: added Migration section, added relations documentation 1:M, M:M\n- Adding .devcontainer to starter projects [https://github.com/loco-rs/loco/issues/170](https://github.com/loco-rs/loco/issues/170)\n- **Braking changes**: Adding `Hooks::boot` application. Migration steps:\n  ```rust\n  // Load boot::{create_app, BootResult, StartMode} from loco_rs lib\n  // Load migration: use migration::Migrator; Only when using DB\n  // Adding boot hook with the following code\n  impl Hooks for App {\n    ...\n    async fn boot(mode: StartMode, environment: &str) -> Result<BootResult> {\n      // With DB:\n      create_app::<Self, Migrator>(mode, environment).await\n      // Without DB:\n      create_app::<Self>(mode, environment).await\n    }\n    ...\n  }\n  ```\n\n## v0.1.7\n\n- Added pretty backtraces [https://github.com/loco-rs/loco/issues/41](https://github.com/loco-rs/loco/issues/41)\n- adding tests for note requests [https://github.com/loco-rs/loco/pull/156](https://github.com/loco-rs/loco/pull/156)\n- Define the min rust version the loco can run [https://github.com/loco-rs/loco/pull/164](https://github.com/loco-rs/loco/pull/164)\n- Added `cargo loco doctor` cli command for validate and diagnose configurations. [https://github.com/loco-rs/loco/pull/145](https://github.com/loco-rs/loco/pull/145)\n- Added ability to specify `settings:` in config files, which are available in context\n- Adding compilation mode in the banner. [https://github.com/loco-rs/loco/pull/127](https://github.com/loco-rs/loco/pull/127)\n- Support shuttle deployment generator. [https://github.com/loco-rs/loco/pull/124](https://github.com/loco-rs/loco/pull/124)\n- Adding a static asset middleware which allows to serve static folder/data. Enable this section in config. [https://github.com/loco-rs/loco/pull/134](https://github.com/loco-rs/loco/pull/134)\n  ```yaml\n  static:\n    enable: true\n    # ensure that both the folder.path and fallback file path are existence.\n    must_exist: true\n    folder:\n      uri: \"/assets\"\n      path: \"frontend/dist\"\n    fallback: \"frontend/dist/index.html\"\n  ```\n- fix: `loco generate request` test template. [https://github.com/loco-rs/loco/pull/133](https://github.com/loco-rs/loco/pull/133)\n- Improve docker deployment generator. [https://github.com/loco-rs/loco/pull/131](https://github.com/loco-rs/loco/pull/131)\n\n## v0.1.6\n\n- refactor: local settings are now `<env>.local.yaml` and available for all environments, for example you can add a local `test.local.yaml` and `development.local.yaml`\n- refactor: removed `config-rs` and now doing config loading by ourselves.\n- fix: email template rendering will not escape URLs\n- Config with variables: It is now possible to use [tera](https://keats.github.io/tera) templates in config YAML files\n\nExample of pulling a port from environment:\n\n```yaml\nserver:\n  port: { { get_env(name=\"NODE_PORT\", default=5150) } }\n```\n\nIt is possible to use any `tera` templating constructs such as loops, conditionals, etc. inside YAML configuration files.\n\n- Mailer: expose `stub` in non-test\n\n- `Hooks::before_run` with a default blank implementation. You can now code some custom loading of resources or other things before the app runs\n- an LLM inference example, text generation in Rust, using an API (`examples/inference`)\n- Loco starters version & create release script [https://github.com/loco-rs/loco/pull/110](https://github.com/loco-rs/loco/pull/110)\n- Configure Cors middleware [https://github.com/loco-rs/loco/pull/114](https://github.com/loco-rs/loco/pull/114)\n- `Hooks::after_routes` Invoke this function after the Loco routers have been constructed. This function enables you to configure custom Axum logics, such as layers, that are compatible with Axum. [https://github.com/loco-rs/loco/pull/114](https://github.com/loco-rs/loco/pull/114)\n- Adding docker deployment generator [https://github.com/loco-rs/loco/pull/119](https://github.com/loco-rs/loco/pull/119)\n\nDOCS:\n\n- Remove duplicated docs in auth section\n- FAQ docs: [https://github.com/loco-rs/loco/pull/116](https://github.com/loco-rs/loco/pull/116)\n\nENHANCEMENTS:\n\n- Remove unused libs: [https://github.com/loco-rs/loco/pull/106](https://github.com/loco-rs/loco/pull/106)\n- turn off default features in tokio [https://github.com/loco-rs/loco/pull/118](https://github.com/loco-rs/loco/pull/118)\n\n## 0.1.5\n\nNEW FEATURES\n\n- `format:html` [https://github.com/loco-rs/loco/issues/74](https://github.com/loco-rs/loco/issues/74)\n- Create a stateless HTML starter [https://github.com/loco-rs/loco/pull/100](https://github.com/loco-rs/loco/pull/100)\n- Added worker generator + adding a way to test workers [https://github.com/loco-rs/loco/pull/92](https://github.com/loco-rs/loco/pull/92)\n\nENHANCEMENTS:\n\n- CI: allows cargo cli run on fork prs [https://github.com/loco-rs/loco/pull/96](https://github.com/loco-rs/loco/pull/96)\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participation in our\ncommunity a harassment-free experience for everyone, regardless of age, body\nsize, visible or invisible disability, ethnicity, sex characteristics, gender\nidentity and expression, level of experience, education, socio-economic status,\nnationality, personal appearance, race, religion, or sexual identity\nand orientation.\n\nWe pledge to act and interact in ways that contribute to an open, welcoming,\ndiverse, inclusive, and healthy community.\n\n## Our Standards\n\nExamples of behavior that contributes to a positive environment for our\ncommunity include:\n\n* Demonstrating empathy and kindness toward other people\n* Being respectful of differing opinions, viewpoints, and experiences\n* Giving and gracefully accepting constructive feedback\n* Accepting responsibility and apologizing to those affected by our mistakes,\n  and learning from the experience\n* Focusing on what is best not just for us as individuals, but for the\n  overall community\n\nExamples of unacceptable behavior include:\n\n* The use of sexualized language or imagery, and sexual attention or\n  advances of any kind\n* Trolling, insulting or derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or email\n  address, without their explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\n## Enforcement Responsibilities\n\nCommunity leaders are responsible for clarifying and enforcing our standards of\nacceptable behavior and will take appropriate and fair corrective action in\nresponse to any behavior that they deem inappropriate, threatening, offensive,\nor harmful.\n\nCommunity leaders have the right and responsibility to remove, edit, or reject\ncomments, commits, code, wiki edits, issues, and other contributions that are\nnot aligned to this Code of Conduct, and will communicate reasons for moderation\ndecisions when appropriate.\n\n## Scope\n\nThis Code of Conduct applies within all community spaces, and also applies when\nan individual is officially representing the community in public spaces.\nExamples of representing our community include using an official e-mail address,\nposting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported to the community leaders responsible for enforcement (open an issue to reach out).\nAll complaints will be reviewed and investigated promptly and fairly.\n\nAll community leaders are obligated to respect the privacy and security of the\nreporter of any incident.\n\n## Enforcement Guidelines\n\nCommunity leaders will follow these Community Impact Guidelines in determining\nthe consequences for any action they deem in violation of this Code of Conduct:\n\n### 1. Correction\n\n**Community Impact**: Use of inappropriate language or other behavior deemed\nunprofessional or unwelcome in the community.\n\n**Consequence**: A private, written warning from community leaders, providing\nclarity around the nature of the violation and an explanation of why the\nbehavior was inappropriate. A public apology may be requested.\n\n### 2. Warning\n\n**Community Impact**: A violation through a single incident or series\nof actions.\n\n**Consequence**: A warning with consequences for continued behavior. No\ninteraction with the people involved, including unsolicited interaction with\nthose enforcing the Code of Conduct, for a specified period of time. This\nincludes avoiding interactions in community spaces as well as external channels\nlike social media. Violating these terms may lead to a temporary or\npermanent ban.\n\n### 3. Temporary Ban\n\n**Community Impact**: A serious violation of community standards, including\nsustained inappropriate behavior.\n\n**Consequence**: A temporary ban from any sort of interaction or public\ncommunication with the community for a specified period of time. No public or\nprivate interaction with the people involved, including unsolicited interaction\nwith those enforcing the Code of Conduct, is allowed during this period.\nViolating these terms may lead to a permanent ban.\n\n### 4. Permanent Ban\n\n**Community Impact**: Demonstrating a pattern of violation of community\nstandards, including sustained inappropriate behavior,  harassment of an\nindividual, or aggression toward or disparagement of classes of individuals.\n\n**Consequence**: A permanent ban from any sort of public interaction within\nthe community.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage],\nversion 2.0, available at\nhttps://www.contributor-covenant.org/version/2/0/code_of_conduct.html.\n\nCommunity Impact Guidelines were inspired by [Mozilla's code of conduct\nenforcement ladder](https://github.com/mozilla/diversity).\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see the FAQ at\nhttps://www.contributor-covenant.org/faq. Translations are available at\nhttps://www.contributor-covenant.org/translations.\n\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to Loco\n\nThank you for taking the time to read this.\n\nThe first way to show support is to star our repos :).\n\n\nLoco is a community driven project. We welcome you to participate, contribute and together build a productivity-first web and api framework in Rust.\n\n## Code of Conduct\n\nThis project is follows [Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code.\n\n## I have a question\n\nIf you have a question to ask, feel free to open an new [discussion](https://github.com/loco-rs/loco/discussions). There are no dumb questions.\n\n## I need a feature\n\nFeature requests from anyone is definitely welcomed! You can open an [issue](https://github.com/loco-rs/loco/issues/new/choose). When you can, illustrate a feature with code, simulated console output, and \"make believe\" console interactions, so we know what you want and what you expect.\n\n## I want to support\n\nAwesome! The best way to support us is to recommend it to your classmates/colleagues/friends, write blog posts and tutorials on our projects and help out other users in the community.\n\n## I want to join\n\nWe are always looking for long-term contributors. If you want to commit longer-term to Loco's open source effort, definitely talk with us!\n\n* From time to time we will make issues clear for newcomers with `mentoring` and `good-first-issue`\n* If no issue exist, just open an issue and ask how to help\n\n### Using an example app to test\n\nOur testing grounds is [examples/demo](examples/demo/) which is pointing to the latest local `loco` framework. You can use it to test out an actual app, using a locally modified `loco`.\n\n\n## Code style\n\nWe use `rustfmt`/`cargo fmt`. A few code style options are set in the [.rustfmt.toml](.rustfmt.toml) file, and some of them are not stable yet and require a nightly version of rustfmt.\n\nIf you're using rustup, the nightly version of rustfmt can be installed by doing the following:\n```sh\nrustup component add rustfmt --toolchain nightly\n```\nAnd then format your code by running:\n```sh\nrustup default nightly\n\ncargo fmt --all\ncargo fmt --all --manifest-path loco-new/Cargo.toml\n\ncargo clippy --fix --allow-dirty --workspace --all-features -- -D warnings -W clippy::pedantic -W clippy::nursery -W rust-2018-idioms\ncargo clippy --fix --allow-dirty --workspace --all-features --manifest-path loco-new/Cargo.toml -- -D warnings -W clippy::pedantic -W clippy::nursery -W rust-2018-idioms\n\nrustup default stable\n```\n\n## Testing\n\nJust clone the project and run `cargo test`.\nYou can see how we test in [.github/workflows](.github/workflows/)\n\n#### Snapshots\nWe use [insta](https://github.com/mitsuhiko/insta) for snapshot testing, which helps us detect changes in output formats and behavior. To work with snapshots:\n\n1. Install the insta CLI tool:\n```sh\ncargo install cargo-insta\n```\n\n2. Run tests and review/update snapshots:\n```sh\ncargo insta test --review\n```\n\nFor CLI-related changes, we maintain separate snapshots of binary command outputs. To update these CLI snapshots:\n```sh\nLOCO_CI_MODE=true TRYCMD=overwrite cargo test\n```\n\n## Docs\n\nThe documentation consists of two main components:\n\n+ The [loco.rs website](https://loco.rs) with its source code available [here](./docs-site/).\n+ RustDocs.\n\nTo reduce duplication in documentation and examples, we use [snipdoc](https://github.com/kaplanelad/snipdoc). As part of our CI process, we ensure that the documentation remains consistent.\n\nUpdating the Documentation\n+ Download [snipdoc](https://github.com/kaplanelad/snipdoc).\n+ Create the snippet in the [yaml file](./snipdoc.yml) or inline the code.\n+ Run `snipdoc run`.\n\nTo run the documentation site locally, we use [zola](https://www.getzola.org/) so you'll need to [install](https://www.getzola.org/documentation/getting-started/installation/) it. The documentation site works with zola version `0.19.2` and since zola still has breaking changes, we make no guarantees about other versions.\n\nRunning the local preview\n+ `cd docs-site`\n+ `npm run serve` or `zola serve`\n\n## Open A Pull Request\n\nThe most recommended and straightforward method to contribute changes to the project involves forking it on GitHub and subsequently initiating a pull request to propose the integration of your modifications into our repository.\n\nChanges a starters project are not recommended. read more [here](./starters/README.md)\n\n### In Your Pull Request Description, Include:\n- References to any bugs fixed by the change\n- Informative notes for the reviewer, aiding their comprehension of the necessity for the change or providing insights on how to conduct a more effective review.\n- A clear explanation of how you tested your changes.\n\n### Your PR must also:\n- be based on the master branch\n- adhere to the code [style](#code-style)\n- Successfully passes the [test suite](#testing)\n"
  },
  {
    "path": "Cargo.toml",
    "content": "[workspace]\nmembers = [\"xtask\", \"loco-gen\"]\nexclude = [\"starters\"]\n\n[workspace.package]\nedition = \"2021\"\nrust-version = \"1.70\"\nlicense = \"Apache-2.0\"\n\n[package]\nname = \"loco-rs\"\nversion = \"0.16.4\"\ndescription = \"The one-person framework for Rust\"\nhomepage = \"https://loco.rs/\"\ndocumentation = \"https://docs.rs/loco-rs\"\nrepository = \"https://github.com/loco-rs/loco\"\nlicense.workspace = true\nedition.workspace = true\nrust-version.workspace = true\n\n# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html\n\n[features]\ndefault = [\n    \"auth_jwt\",\n    \"cli\",\n    \"with-db\",\n    \"cache_inmem\",\n    \"bg_redis\",\n    \"bg_pg\",\n    \"bg_sqlt\",\n]\nauth_jwt = [\"dep:jsonwebtoken\"]\ncli = [\"dep:clap\"]\ntesting = [\"dep:axum-test\", \"dep:scraper\", \"dep:tree-fs\"]\nwith-db = [\n    \"dep:sea-orm\",\n    \"dep:sea-orm-migration\",\n    \"dep:sqlx\",\n    \"loco-gen/with-db\",\n]\n# Storage features\nall_storage = [\"storage_aws_s3\", \"storage_azure\", \"storage_gcp\"]\nstorage_aws_s3 = [\"opendal/services-s3\"]\nstorage_azure = [\"opendal/services-azblob\"]\nstorage_gcp = [\"opendal/services-gcs\"]\n# Cache feature\ncache_inmem = [\"dep:moka\"]\ncache_redis = [\"dep:bb8-redis\", \"dep:bb8\"]\nbg_redis = [\"dep:redis\", \"dep:ulid\"]\nbg_pg = [\"dep:sqlx\", \"dep:ulid\"]\nbg_sqlt = [\"dep:sqlx\", \"dep:ulid\"]\n## Testing feature flags\nintegration_test = []\n# Embed assets into binary\nembedded_assets = []\n\n[dependencies]\nloco-gen = { version = \"0.16.1\", path = \"./loco-gen\" }\nbacktrace_printer = { version = \"1.3.0\" }\n\n# cli\nclap = { version = \"4.4.7\", features = [\"derive\"], optional = true }\ncolored = { workspace = true }\nsea-orm = { version = \"1.1.0\", features = [\n    \"sqlx-postgres\",        # `DATABASE_DRIVER` feature\n    \"sqlx-sqlite\",\n    \"runtime-tokio-rustls\",\n    \"macros\",\n], optional = true }\n\ntokio = { version = \"1.45\", default-features = false }\ntokio-util = \"0.7\"\n# the rest\n\nserde = { workspace = true }\nserde_json = { workspace = true }\nserde_yaml = \"0.9\"\nserde_variant = \"0.1.2\"\ntoml = \"0.8\"\n\nasync-trait = { workspace = true }\n\naxum = { workspace = true }\naxum-extra = { version = \"0.10\", features = [\"cookie\"] }\nregex = { workspace = true }\n# mailer\ntera = { workspace = true }\nheck = { workspace = true }\ncruet = \"0.13.0\"\nlettre = { version = \"0.11.4\", default-features = false, features = [\n    \"builder\",\n    \"hostname\",\n    \"smtp-transport\",\n    \"tokio1-rustls-tls\",\n] }\ninclude_dir = \"0.7.3\"\nthiserror = { workspace = true }\ntracing = { workspace = true }\ntracing-subscriber = { version = \"0.3.16\", default-features = false, features = [\n    \"env-filter\",\n    \"json\",\n    \"ansi\",\n] }\ntracing-appender = { version = \"0.2.3\", default-features = false }\n\nduct = { workspace = true }\nduct_sh = { version = \"1.0.0\" }\n\ntower-http = { workspace = true }\nbyte-unit = \"4.0.19\"\n\nargon2 = { version = \"0.5\", features = [\"std\"] }\nrand = { version = \"0.9\", features = [\"std\"] }\njsonwebtoken = { version = \"9.3.0\", optional = true }\nvalidator = { version = \"0.20.0\", features = [\"derive\"] }\nfutures-util = \"0.3\"\ntower = { workspace = true }\nbytes = \"1.1\"\nipnetwork = \"0.20.0\"\nsemver = \"1\"\n\naxum-test = { version = \"17.0.1\", optional = true }\ntree-fs = { version = \"0.3\", optional = true }\n\nchrono = { workspace = true }\n\nuuid = { version = \"1.10.0\", features = [\"v4\", \"fast-rng\"] }\n\n# File Upload\nopendal = { version = \"0.54\", default-features = false, features = [\n    \"services-memory\",\n    \"services-fs\",\n] }\n\n# cache\nmoka = { version = \"0.12.7\", features = [\"sync\"], optional = true }\nbb8-redis = { version = \"0.23\", optional = true }\nbb8 = { version = \"0.9\", optional = true }\n\n# Scheduler\ntokio-cron-scheduler = { version = \"0.11.0\", features = [\"signal\"] }\nenglish-to-cron = { version = \"0.1.2\" }\n\n# bg_sqlt: sqlite workers\n# bg_pg: postgres workers\nsqlx = { version = \"0.8.2\", default-features = false, features = [\n    \"json\",\n    \"postgres\",\n    \"chrono\",\n    \"sqlite\",\n], optional = true }\nulid = { version = \"1\", optional = true }\n\n# bg_redis: redis workers\nredis = { version = \"0.31\", features = [\"aio\", \"tokio-comp\"], optional = true }\n\nscraper = { version = \"0.25.0\", features = [\"deterministic\"], optional = true }\n\ndashmap = \"6\"\nnotify = \"8.1.0\"\n\n[workspace.dependencies]\ntera = { version = \"1.19.1\" }\ncolored = { version = \"3.0\" }\nchrono = { version = \"0.4\", features = [\"serde\"] }\ntracing = \"0.1.40\"\nregex = \"1\"\nthiserror = \"1\"\nserde = \"1\"\nserde_json = \"1\"\nasync-trait = { version = \"0.1.74\" }\naxum = { version = \"0.8.1\", features = [\"macros\", \"multipart\"] }\ntower = \"0.4\"\ntower-http = { version = \"0.6.8\", features = [\n    \"trace\",\n    \"catch-panic\",\n    \"timeout\",\n    \"add-extension\",\n    \"cors\",\n    \"fs\",\n    \"set-header\",\n    \"compression-full\",\n] }\nheck = \"0.4.0\"\nduct = { version = \"1.0.0\" }\n\n[dependencies.sea-orm-migration]\noptional = true\nversion = \"1.0.0\"\nfeatures = [\n    # Enable at least one `ASYNC_RUNTIME` and `DATABASE_DRIVER` feature if you want to run migration via CLI.\n    # View the list of supported features at https://www.sea-ql.org/SeaORM/docs/install-and-config/database-and-async-runtime.\n    # e.g.\n    \"runtime-tokio-rustls\", # `ASYNC_RUNTIME` feature\n    \"sqlx-postgres\",        # `DATABASE_DRIVER` feature\n    \"sqlx-sqlite\",\n]\n\n[package.metadata.docs.rs]\nfeatures = [\"testing\"]\n\n[dev-dependencies]\nloco-rs = { path = \".\", features = [\"testing\"] }\nrstest = \"0.21.0\"\ninsta = { version = \"1.34.0\", features = [\"redactions\", \"yaml\", \"filters\"] }\ntree-fs = { version = \"0.3\" }\nreqwest = { version = \"0.12.7\", features = [\"json\"] }\ntower = { workspace = true, features = [\"util\"] }\nsqlx = { version = \"0.8.2\", default-features = false, features = [\n    \"macros\",\n    \"json\",\n    \"postgres\",\n    \"chrono\",\n    \"sqlite\",\n    \"migrate\",\n] }\ntestcontainers = { version = \"0.23.3\" }\n"
  },
  {
    "path": "DEVELOPMENT.md",
    "content": "## Blessed dependencies maintenance and `loco doctor`\n\nLoco contain a few major and \"blessed\" dependencies, these appear **both** in an app that was generated at the surface level in their `Cargo.toml` and in the core Loco framework.\n\nIf stale, may require an upgrade as a must.\n\nExample for such dependencies:\n\n* The `sea-orm-cli` - while Loco uses `SeaORM`, it uses the `SeaORM` CLI to generate entities, and so there may be an incompatibility if `SeaORM` has a too large breaking change between their CLI (which ships separately) and their framework. \n* `axum`\n* etc.\n\nThis is why we are checking these automatically as part of `loco doctor`.\n\nWe keep minimal version requirements for these. As a maintainer, you can update these **minimal** versions, only if required in [`doctor.rs`](src/doctor.rs).\n\n\n\n## Running Tests\n\nBefore running tests make sure that:\n\n[ ] redis is running\n[ ] starters/saas frontend package is built:\n\n```\n$ cd starters/saas/frontend\n$ npm i -g pnpm\n$ pnpm i && pnpm build\n```\n\nRunning all tests should be done with:\n\n```\n$ cargo xtask test\n```\n\n## Rebuilding your database and local generated entities\n\nThis should write out a fresh DB structure (drops and migrates):\n\n```\n$ cargo loco db reset\n```\n\nAnd then, the entities generators connect to that newly minted DB, to generate a corresponding entities code:\n\n```\n$ cargo loco db entities\n```\n\n## Publishing a new version\n\n**Test your changes**\n\n* [ ] Ensure you have the necessary local resources, such as `DB`/`Redis`, by executing the command `cargo loco doctor  --environment test`. In case you don't have them, refer to the relevant documentation section for guidance.\n* [ ] run `cargo test` on the root to test Loco itself\n* [ ] cd `examples/demo` and run `cargo test` to test our \"driver app\" which exercises the framework in various ways\n* [ ] push your changes to Github to get the CI running and testing in various additional configurations that you don't have\n* [ ] CI should pass. Take note that all `starters-*` CI are using a **fixed version** of Loco and are not seeing your changes yet\n\n\n**Actually bump version + test and align starters**\n\n* [ ] in project root, run `cargo xtask bump-version` and give it the next version. Versions are without `v` prefix. Example: `0.1.3`. \n* [ ] Did the xtask testing workflow fail?\n  * [ ] YES: fix errors, and re-run `cargo xtask bump-version` **with the same version as before**.\n  * [ ] NO: great, move to publishing\n* [ ] Your repo may be dirty with fixes. Now that tests are passing locally commit the changes. Then run `cargo publish` to publish the next Loco version (remember: the starters at this point are pointing to the **next version already**, so we don't want to push until publish finished)\n* [ ] When publish finished successfully, push your changes to github\n* [ ] Wait for CI to finish. You want to be focusing more at the starters CI, because they will now pull the new version.\n* [ ] Did CI fail?\n  * [ ] YES: This means you had a circumstance that's not predictable (e.g. some operating system issue). Fix the issue and **repeat the bumping process, advance a new version**.\n  * [ ] NO: all good! you're done.\n\n**Book keeping**\n\n* [ ] Update changelog: (1) move vnext to be that new version of yours, (2) create a blank vnext\n* [ ] Think about if any of the items in the new version needs new documentation or update to the documentation -- and do it\n## Errors\n\nErrors are done with `thiserror`. We adopt a minimalistic approach to errors.\n\n* We try to have _one error kind_ for the entirety of Loco.\n* Errors that cannot be handled, are _informative_ and so can be opaque (we don't offer deep matching on those)\n* Errors that can be handled and reasoned upon should be able to be matched and extract good knowledge from\n* To users, error should _not be cryptic_, and should indicate how to fix issues as much as possible, or point to the issue precisely\n\n\n### Auto conversions\n\nWhen possible use `from` conversions.\n\n```rust\n    #[error(transparent)]\n    JSON(#[from] serde_json::Error),\n```\n\nWhen complicated, implement a `From` trait yourself. This is done to _centralize_ errors into one place and not litter needless `map_err` code which holds error conversion logic (an exception is Context, see below).\n\n\n### Context\n\nWhen you know a user might need context, resort to manually shaping the error with extra information. First, define the error:\n\n```rust\n    #[error(\"cannot parse `{1}`: {0}\")]\n    YAMLFile(#[source] serde_yaml::Error, String),\n```\n\nThen, shape it:\n\n```rust\n  serde_yaml::from_str(&rendered)\n      .map_err(|err| Error::YAMLFile(err, selected_path.to_string_lossy().to_string()))\n```\n\nIn this example, the information about where `rendered` came from was long lost at the `serde_yaml::from_str` callsite. Which is why errors were cryptic indicating bad YAML format, but not where it comes from (which file).\n\nIn this case, we duplicate the YAML error type, leave one of those for auto conversions with `from`, where we don't have a file, and create a new specialized error type with the file information: `YAMLFile`.\n\n## The `CONTRIBUTORS` comment\n\nSome files contain a special `CONTRIBUTORS` comment. This comment should\ncontain context, special notes for that module, and a checklist if needed, so please make sure to follow it.\n\n"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1.  Definitions.\n\n    \"License\" shall mean the terms and conditions for use, reproduction,\n    and distribution as defined by Sections 1 through 9 of this document.\n\n    \"Licensor\" shall mean the copyright owner or entity authorized by\n    the copyright owner that is granting the License.\n\n    \"Legal Entity\" shall mean the union of the acting entity and all\n    other entities that control, are controlled by, or are under common\n    control with that entity. For the purposes of this definition,\n    \"control\" means (i) the power, direct or indirect, to cause the\n    direction or management of such entity, whether by contract or\n    otherwise, or (ii) ownership of fifty percent (50%) or more of the\n    outstanding shares, or (iii) beneficial ownership of such entity.\n\n    \"You\" (or \"Your\") shall mean an individual or Legal Entity\n    exercising permissions granted by this License.\n\n    \"Source\" form shall mean the preferred form for making modifications,\n    including but not limited to software source code, documentation\n    source, and configuration files.\n\n    \"Object\" form shall mean any form resulting from mechanical\n    transformation or translation of a Source form, including but\n    not limited to compiled object code, generated documentation,\n    and conversions to other media types.\n\n    \"Work\" shall mean the work of authorship, whether in Source or\n    Object form, made available under the License, as indicated by a\n    copyright notice that is included in or attached to the work\n    (an example is provided in the Appendix below).\n\n    \"Derivative Works\" shall mean any work, whether in Source or Object\n    form, that is based on (or derived from) the Work and for which the\n    editorial revisions, annotations, elaborations, or other modifications\n    represent, as a whole, an original work of authorship. For the purposes\n    of this License, Derivative Works shall not include works that remain\n    separable from, or merely link (or bind by name) to the interfaces of,\n    the Work and Derivative Works thereof.\n\n    \"Contribution\" shall mean any work of authorship, including\n    the original version of the Work and any modifications or additions\n    to that Work or Derivative Works thereof, that is intentionally\n    submitted to Licensor for inclusion in the Work by the copyright owner\n    or by an individual or Legal Entity authorized to submit on behalf of\n    the copyright owner. For the purposes of this definition, \"submitted\"\n    means any form of electronic, verbal, or written communication sent\n    to the Licensor or its representatives, including but not limited to\n    communication on electronic mailing lists, source code control systems,\n    and issue tracking systems that are managed by, or on behalf of, the\n    Licensor for the purpose of discussing and improving the Work, but\n    excluding communication that is conspicuously marked or otherwise\n    designated in writing by the copyright owner as \"Not a Contribution.\"\n\n    \"Contributor\" shall mean Licensor and any individual or Legal Entity\n    on behalf of whom a Contribution has been received by Licensor and\n    subsequently incorporated within the Work.\n\n2.  Grant of Copyright License. Subject to the terms and conditions of\n    this License, each Contributor hereby grants to You a perpetual,\n    worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n    copyright license to reproduce, prepare Derivative Works of,\n    publicly display, publicly perform, sublicense, and distribute the\n    Work and such Derivative Works in Source or Object form.\n\n3.  Grant of Patent License. Subject to the terms and conditions of\n    this License, each Contributor hereby grants to You a perpetual,\n    worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n    (except as stated in this section) patent license to make, have made,\n    use, offer to sell, sell, import, and otherwise transfer the Work,\n    where such license applies only to those patent claims licensable\n    by such Contributor that are necessarily infringed by their\n    Contribution(s) alone or by combination of their Contribution(s)\n    with the Work to which such Contribution(s) was submitted. If You\n    institute patent litigation against any entity (including a\n    cross-claim or counterclaim in a lawsuit) alleging that the Work\n    or a Contribution incorporated within the Work constitutes direct\n    or contributory patent infringement, then any patent licenses\n    granted to You under this License for that Work shall terminate\n    as of the date such litigation is filed.\n\n4.  Redistribution. You may reproduce and distribute copies of the\n    Work or Derivative Works thereof in any medium, with or without\n    modifications, and in Source or Object form, provided that You\n    meet the following conditions:\n\n    (a) You must give any other recipients of the Work or\n    Derivative Works a copy of this License; and\n\n    (b) You must cause any modified files to carry prominent notices\n    stating that You changed the files; and\n\n    (c) You must retain, in the Source form of any Derivative Works\n    that You distribute, all copyright, patent, trademark, and\n    attribution notices from the Source form of the Work,\n    excluding those notices that do not pertain to any part of\n    the Derivative Works; and\n\n    (d) If the Work includes a \"NOTICE\" text file as part of its\n    distribution, then any Derivative Works that You distribute must\n    include a readable copy of the attribution notices contained\n    within such NOTICE file, excluding those notices that do not\n    pertain to any part of the Derivative Works, in at least one\n    of the following places: within a NOTICE text file distributed\n    as part of the Derivative Works; within the Source form or\n    documentation, if provided along with the Derivative Works; or,\n    within a display generated by the Derivative Works, if and\n    wherever such third-party notices normally appear. The contents\n    of the NOTICE file are for informational purposes only and\n    do not modify the License. You may add Your own attribution\n    notices within Derivative Works that You distribute, alongside\n    or as an addendum to the NOTICE text from the Work, provided\n    that such additional attribution notices cannot be construed\n    as modifying the License.\n\n    You may add Your own copyright statement to Your modifications and\n    may provide additional or different license terms and conditions\n    for use, reproduction, or distribution of Your modifications, or\n    for any such Derivative Works as a whole, provided Your use,\n    reproduction, and distribution of the Work otherwise complies with\n    the conditions stated in this License.\n\n5.  Submission of Contributions. Unless You explicitly state otherwise,\n    any Contribution intentionally submitted for inclusion in the Work\n    by You to the Licensor shall be under the terms and conditions of\n    this License, without any additional terms or conditions.\n    Notwithstanding the above, nothing herein shall supersede or modify\n    the terms of any separate license agreement you may have executed\n    with Licensor regarding such Contributions.\n\n6.  Trademarks. This License does not grant permission to use the trade\n    names, trademarks, service marks, or product names of the Licensor,\n    except as required for reasonable and customary use in describing the\n    origin of the Work and reproducing the content of the NOTICE file.\n\n7.  Disclaimer of Warranty. Unless required by applicable law or\n    agreed to in writing, Licensor provides the Work (and each\n    Contributor provides its Contributions) on an \"AS IS\" BASIS,\n    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n    implied, including, without limitation, any warranties or conditions\n    of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n    PARTICULAR PURPOSE. You are solely responsible for determining the\n    appropriateness of using or redistributing the Work and assume any\n    risks associated with Your exercise of permissions under this License.\n\n8.  Limitation of Liability. In no event and under no legal theory,\n    whether in tort (including negligence), contract, or otherwise,\n    unless required by applicable law (such as deliberate and grossly\n    negligent acts) or agreed to in writing, shall any Contributor be\n    liable to You for damages, including any direct, indirect, special,\n    incidental, or consequential damages of any character arising as a\n    result of this License or out of the use or inability to use the\n    Work (including but not limited to damages for loss of goodwill,\n    work stoppage, computer failure or malfunction, or any and all\n    other commercial damages or losses), even if such Contributor\n    has been advised of the possibility of such damages.\n\n9.  Accepting Warranty or Additional Liability. While redistributing\n    the Work or Derivative Works thereof, You may choose to offer,\n    and charge a fee for, acceptance of support, warranty, indemnity,\n    or other liability obligations and/or rights consistent with this\n    License. However, in accepting such obligations, You may act only\n    on Your own behalf and on Your sole responsibility, not on behalf\n    of any other Contributor, and only if You agree to indemnify,\n    defend, and hold each Contributor harmless for any liability\n    incurred by, or claims asserted against, such Contributor by reason\n    of your accepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nAPPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\nCopyright [2022] Dotan Nahum, Elad Kaplan\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n"
  },
  {
    "path": "README-pt_BR.md",
    "content": " <div align=\"center\">\n\n   <img src=\"https://github.com/loco-rs/loco/assets/83390/992d215a-3cd3-42ee-a1c7-de9fd25a5bac\"/>\n\n   <h1>Bem-vindo ao Loco</h1>\n\n   <h3>\n   <!-- <snip id=\"description\" inject_from=\"yaml\"> -->\n🚂 Loco is Rust on Rails.\n<!--</snip> -->\n   </h3>\n\n   [![crate](https://img.shields.io/crates/v/loco-rs.svg)](https://crates.io/crates/loco-rs)\n   [![docs](https://docs.rs/loco-rs/badge.svg)](https://docs.rs/loco-rs)\n   [![Discord channel](https://img.shields.io/badge/discord-Join-us)](https://discord.gg/fTvyBzwKS8)\n\n </div>\n\n[English](./README.md) · [中文](./README-zh_CN.md) · [Français](./README.fr.md) · Portuguese (Brazil) ・ [日本語](./README.ja.md) · [한국어](./README.ko.md) · [Русский](./README.ru.md) · [Español](./README.es.md)\n\n\n## O que é o Loco?\n`Loco` é fortemente inspirado no Rails. Se você conhece Rails e Rust, se sentirá em casa. Se você só conhece Rails e é novo em Rust, achará o Loco refrescante. Não presumimos que você conheça o Rails.\n\nPara uma imersão mais profunda em como o Loco funciona, incluindo guias detalhados, exemplos e referências da API, confira nosso [site de documentação](https://loco.rs).\n\n\n## Recursos do Loco:\n\n* `Convenção sobre Configuração:` Semelhante ao Ruby on Rails, o Loco enfatiza simplicidade e produtividade ao reduzir a necessidade de código boilerplate. Ele utiliza padrões sensatos, permitindo que os desenvolvedores se concentrem em escrever a lógica de negócios em vez de perder tempo com configuração.\n\n* `Desenvolvimento Rápido:` Com o objetivo de alta produtividade para o desenvolvedor, o design do Loco se concentra em reduzir código boilerplate e fornecer APIs intuitivas, permitindo que os desenvolvedores iteren rapidamente e construam protótipos com esforço mínimo.\n\n* `Integração ORM:` Modele seu negócio com entidades robustas, eliminando a necessidade de escrever SQL. Defina relacionamentos, validações e lógica personalizada diretamente em suas entidades para melhorar a manutenção e escalabilidade.\n\n* `Controladores:` Manipule os parâmetros de solicitações web, corpo, validação e renderize uma resposta que é consciente do conteúdo. Usamos Axum para o melhor desempenho, simplicidade e extensibilidade. Os controladores também permitem que você construa facilmente middlewares, que podem ser usados para adicionar lógica como autenticação, registro ou tratamento de erros antes de passar as solicitações para as ações principais do controlador.\n\n* `Views:` O Loco pode se integrar com mecanismos de template para gerar conteúdo HTML dinâmico a partir de templates.\n\n* `Trabalhos em segundo plano:` Realize trabalhos intensivos de computação ou I/O em segundo plano com uma fila baseada em Redis ou com threads. Implementar um trabalhador é tão simples quanto implementar uma função de execução para o trait Worker.\n\n* `Scheduler:` Simplifica o tradicional e frequentemente complicado sistema crontab, tornando mais fácil e elegante agendar tarefas ou scripts shell.\n\n* `Mailers:` Um mailer entregará e-mails em segundo plano usando a infraestrutura de trabalhador existente do loco. Tudo será transparente para você.\n\n* `Armazenamento:` No Armazenamento do Loco, facilitamos o trabalho com arquivos por meio de várias operações. O armazenamento pode ser em memória, no disco ou utilizar serviços em nuvem, como AWS S3, GCP e Azure.\n\n* `Cache:` O Loco fornece uma camada de cache para melhorar o desempenho da aplicação armazenando dados acessados frequentemente.\n\nPara ver mais recursos do Loco, confira nosso [site de documentação](https://loco.rs/docs/getting-started/tour/).\n\n\n\n## Começando\n<!-- <snip id=\"quick-installation-command\" inject_from=\"yaml\" template=\"sh\"> -->\n```sh\ncargo install loco\ncargo install sea-orm-cli # Only when DB is needed\n```\n<!-- </snip> -->\n\nAgora você pode criar seu novo aplicativo (escolha \"`SaaS` app\").\n\n\n<!-- <snip id=\"loco-cli-new-from-template\" inject_from=\"yaml\" template=\"sh\"> -->\n```sh\n❯ loco new\n✔ ❯ App name? · myapp\n✔ ❯ What would you like to build? · Saas App with client side rendering\n✔ ❯ Select a DB Provider · Sqlite\n✔ ❯ Select your background worker type · Async (in-process tokio async tasks)\n\n🚂 Loco app generated successfully in:\nmyapp/\n\n- assets: You've selected `clientside` for your asset serving configuration.\n\nNext step, build your frontend:\n  $ cd frontend/\n  $ npm install && npm run build\n```\n<!-- </snip> -->\n\n Agora execute `cd` no seu `myapp` e inicie seu aplicativo:\n<!-- <snip id=\"starting-the-server-command-with-output\" inject_from=\"yaml\" template=\"sh\"> -->\n```sh\n$ cargo loco start\n\n                      ▄     ▀\n                                ▀  ▄\n                  ▄       ▀     ▄  ▄ ▄▀\n                                    ▄ ▀▄▄\n                        ▄     ▀    ▀  ▀▄▀█▄\n                                          ▀█▄\n▄▄▄▄▄▄▄  ▄▄▄▄▄▄▄▄▄   ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄ ▀▀█\n██████  █████   ███ █████   ███ █████   ███ ▀█\n██████  █████   ███ █████   ▀▀▀ █████   ███ ▄█▄\n██████  █████   ███ █████       █████   ███ ████▄\n██████  █████   ███ █████   ▄▄▄ █████   ███ █████\n██████  █████   ███  ████   ███ █████   ███ ████▀\n  ▀▀▀██▄ ▀▀▀▀▀▀▀▀▀▀  ▀▀▀▀▀▀▀▀▀▀  ▀▀▀▀▀▀▀▀▀▀ ██▀\n      ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀\n                https://loco.rs\n\nlistening on port 5150\n```\n<!-- </snip> -->\n\n## Impulsionado pelo Loco\n+ [SpectralOps](https://spectralops.io) - vários serviços impulsionados pelo framework Loco\n+ [Nativish](https://nativi.sh) - backend do aplicativo impulsionado pelo framework Loco\n\n## Contribuidores ✨\nAgradecimentos a essas pessoas maravilhosas:\n\n<a href=\"https://github.com/loco-rs/loco/graphs/contributors\">\n  <img src=\"https://contrib.rocks/image?repo=loco-rs/loco\" />\n</a>\n"
  },
  {
    "path": "README-zh_CN.md",
    "content": " <div align=\"center\">\n\n   <img src=\"https://github.com/loco-rs/loco/assets/83390/992d215a-3cd3-42ee-a1c7-de9fd25a5bac\"/>\n\n   <h1>Loco</h1>\n\n\n   [![crate](https://img.shields.io/crates/v/loco-rs.svg)](https://crates.io/crates/loco-rs)\n   [![docs](https://docs.rs/loco-rs/badge.svg)](https://docs.rs/loco-rs)\n   [![Discord channel](https://img.shields.io/badge/discord-Join-us)](https://discord.gg/fTvyBzwKS8)\n\n </div>\n\n[English](./README.md) · 中文 · [Français](./README.fr.md) · [Portuguese (Brazil)](./README-pt_BR.md) ・ [日本語](./README.ja.md) · [한국어](./README.ko.md) · [Русский](./README.ru.md) · [Español](./README.es.md)\n\nLoco 是一个用 Rust 编写的 Web 框架，类似于 Rails。Loco 提供快速构建 Web 应用的功能，并且允许创建自定义任务，可以通过 CLI 运行。\n\n## 特性\n\n- **简单的 API**: 使用 Rust 的强类型系统确保安全性和可靠性。\n- **快速开发**: 提供快速构建 Web 应用的工具和模板。\n- **CLI 支持**: 可以创建和运行自定义 CLI 任务。\n- **灵活性**: 支持自定义配置和扩展。\n\n## 安装\n\n通过 Cargo 安装 Loco:\n\n```sh\ncargo install loco\n```\n\n## 快速开始\n\n创建一个新的 Loco 项目:\n\n```sh\nloco new my_project\ncd my_project\n```\n\n启动开发服务器:\n\n```sh\ncargo loco start\n```\n\n## 贡献\n\n欢迎对 Loco 的贡献！请阅读 [CONTRIBUTING.md](CONTRIBUTING.md) 了解更多信息。\n\n## 许可证\n\nLoco 在 MIT 许可证下发布。详情请参阅 [LICENSE](LICENSE)。\n\n---\n\nFor more details, you can visit the [original README file](https://github.com/loco-rs/loco/blob/master/README.md).\n"
  },
  {
    "path": "README.es.md",
    "content": "<div align=\"center\">\n\n   <img src=\"https://github.com/loco-rs/loco/assets/83390/992d215a-3cd3-42ee-a1c7-de9fd25a5bac\"/>\n\n   <h1>Bienvenido a Loco</h1>\n\n   <h3>\n   <!-- <snip id=\"description\" inject_from=\"yaml\"> -->\n🚂 Loco es Rust on Rails.\n<!--</snip> -->\n   </h3>\n\n   [![crate](https://img.shields.io/crates/v/loco-rs.svg)](https://crates.io/crates/loco-rs)\n   [![docs](https://docs.rs/loco-rs/badge.svg)](https://docs.rs/loco-rs)\n   [![Discord channel](https://img.shields.io/badge/discord-Join-us)](https://discord.gg/fTvyBzwKS8)\n\n </div>\n\nEspañol · [English](./README.md) · [中文](./README-zh_CN.md) · [Français](./README.fr.md) · [Português (Brasil)](./README-pt_BR.md) · [日本語](./README.ja.md) · [한국어](./README.ko.md) · [Русский](./README.ru.md) · Español\n\n## ¿Qué es Loco?\n\n`Loco` está fuertemente inspirado en Rails. Si conoces Rails y Rust, te sentirás como en casa. Si solo conoces Rails y eres nuevo en Rust, encontrarás Loco refrescante. No asumimos que conozcas Rails.\n\nPara una explicación más profunda de cómo funciona Loco, incluyendo guías detalladas, ejemplos y referencias de la API, consulta nuestro [sitio de documentación](https://loco.rs).\n\n## Características de Loco\n\n* `Convención sobre configuración:` Al igual que Ruby on Rails, Loco enfatiza la simplicidad y la productividad al reducir la necesidad de código repetitivo. Utiliza valores predeterminados sensatos, permitiendo a los desarrolladores centrarse en la lógica de negocio en lugar de perder tiempo en la configuración.\n\n* `Desarrollo rápido:` Loco está diseñado para una alta productividad del desarrollador, reduciendo el código repetitivo y proporcionando APIs intuitivas, permitiendo iterar rápidamente y construir prototipos con un esfuerzo mínimo.\n\n* `Integración ORM:` Modela tu negocio con entidades robustas, eliminando la necesidad de escribir SQL. Define relaciones, validaciones y lógica personalizada directamente en tus entidades para una mayor mantenibilidad y escalabilidad.\n\n* `Controladores:` Maneja parámetros de solicitudes web, cuerpo, validación y renderiza una respuesta consciente del contenido. Usamos Axum para el mejor rendimiento, simplicidad y extensibilidad. Los controladores también permiten construir middlewares fácilmente, que pueden usarse para agregar lógica como autenticación, registro o manejo de errores antes de pasar las solicitudes a las acciones principales del controlador.\n\n* `Vistas:` Loco puede integrarse con motores de plantillas para generar contenido HTML dinámico a partir de plantillas.\n\n* `Trabajos en segundo plano:` Realiza trabajos intensivos en computación o I/O en segundo plano con una cola respaldada por Redis o con hilos. Implementar un worker es tan simple como implementar una función perform para el trait Worker.\n\n* `Planificador:` Simplifica el tradicional y a menudo engorroso sistema crontab, facilitando y haciendo más elegante la programación de tareas o scripts de shell.\n\n* `Mailers:` Un mailer enviará correos electrónicos en segundo plano usando la infraestructura de background worker de Loco. Todo será transparente para ti.\n\n* `Almacenamiento:` En Loco Storage, facilitamos el trabajo con archivos a través de múltiples operaciones. El almacenamiento puede ser en memoria, en disco o usar servicios en la nube como AWS S3, GCP y Azure.\n\n* `Caché:` Loco proporciona una capa de caché para mejorar el rendimiento de la aplicación almacenando datos de acceso frecuente.\n\nPara ver más características de Loco, consulta nuestro [sitio de documentación](https://loco.rs/docs/getting-started/tour/).\n\n## Primeros pasos\n<!-- <snip id=\"quick-installation-command\" inject_from=\"yaml\" template=\"sh\"> -->\n```sh\ncargo install loco\ncargo install sea-orm-cli # Solo si necesitas base de datos\n```\n<!-- </snip> -->\n\nAhora puedes crear tu nueva app (elige \"`SaaS` app\").\n\n<!-- <snip id=\"loco-cli-new-from-template\" inject_from=\"yaml\" template=\"sh\"> -->\n```sh\n❯ loco new\n✔ ❯ ¿Nombre de la app? · miapp\n✔ ❯ ¿Qué te gustaría construir? · App SaaS con renderizado del lado del cliente\n✔ ❯ Selecciona un proveedor de BD · Sqlite\n✔ ❯ Selecciona el tipo de worker en segundo plano · Async (tareas async in-process con tokio)\n\n🚂 App Loco generada exitosamente en:\nmiapp/\n\n- assets: Has seleccionado `clientside` para la configuración de tu servidor de assets.\n\nSiguiente paso, construye tu frontend:\n  $ cd frontend/\n  $ npm install && npm run build\n```\n<!-- </snip> -->\n\nAhora entra en tu `miapp` y arranca tu app:\n<!-- <snip id=\"starting-the-server-command-with-output\" inject_from=\"yaml\" template=\"sh\"> -->\n```sh\n$ cargo loco start\n\n                      ▄     ▀\n                                ▀  ▄\n                  ▄       ▀     ▄  ▄ ▄▀\n                                    ▄ ▀▄▄\n                        ▄     ▀    ▀  ▀▄▀█▄\n                                          ▀█▄\n▄▄▄▄▄▄▄  ▄▄▄▄▄▄▄▄▄   ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄ ▀▀█\n██████  █████   ███ █████   ███ █████   ███ ▀█\n██████  █████   ███ █████   ▀▀▀ █████   ███ ▄█▄\n██████  █████   ███ █████       █████   ███ ████▄\n██████  █████   ███ █████   ▄▄▄ █████   ███ █████\n██████  █████   ███  ████   ███ █████   ███ ████▀\n  ▀▀▀██▄ ▀▀▀▀▀▀▀▀▀▀  ▀▀▀▀▀▀▀▀▀▀  ▀▀▀▀▀▀▀▀▀▀ ██▀\n      ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀\n                https://loco.rs\n\nlistening on port 5150\n```\n<!-- </snip> -->\n\n## Proyectos impulsados por Loco\n\n* [SpectralOps](https://spectralops.io) - varios servicios impulsados por el framework Loco\n\n* [Nativish](https://nativi.sh) - backend de la app impulsado por el framework Loco\n\n## Contribuidores ✨\n\nGracias a estas personas maravillosas:\n\n<a href=\"https://github.com/loco-rs/loco/graphs/contributors\">\n  <img src=\"https://contrib.rocks/image?repo=loco-rs/loco\" />\n</a>\n"
  },
  {
    "path": "README.fr.md",
    "content": " <div align=\"center\">\n\n   <img src=\"https://github.com/loco-rs/loco/assets/83390/992d215a-3cd3-42ee-a1c7-de9fd25a5bac\"/>\n\n   <h1>Loco vous souhaite la bienvenue</h1>\n\n   <h3>\n🚂 Loco c'est Rust on Rails.\n   </h3>\n\n   [![crate](https://img.shields.io/crates/v/loco-rs.svg)](https://crates.io/crates/loco-rs)\n   [![docs](https://docs.rs/loco-rs/badge.svg)](https://docs.rs/loco-rs)\n   [![Discord channel](https://img.shields.io/badge/discord-Join-us)](https://discord.gg/fTvyBzwKS8)\n\n </div>\n\n[English](./README.md) · [中文](./README-zh_CN.md) · Français · [Portuguese (Brazil)](./README-pt_BR.md) ・ [日本語](./README.ja.md) · [한국어](./README.ko.md) · [Русский](./README.ru.md) · [Español](./README.es.md)\n\n## À propos de Loco\n`Loco` est fortement inspiré de Rails. Si vous connaissez Rails et Rust, vous vous sentirez chez vous. Si vous ne connaissez que Rails et que vous êtes nouveau sur Rust, vous trouverez Loco rafraîchissant. Nous ne supposons pas que vous connaissez Rails.\nPour un aperçu plus approfondie du fonctionnement de Loco, y compris des guides détaillés, des exemples et des références API, consultez notre [site Web de documentation](https://loco.rs).\n\n## Caractéristiques de Loco:\n\n* `Convention plutôt que configuration`: Semblable à Ruby on Rails, Loco met l'accent sur la simplicité et la productivité en réduisant le besoin de code passe-partout. Il utilise des valeurs par défaut raisonnables, permettant aux développeurs de se concentrer sur l'écriture de la logique métier plutôt que de consacrer du temps à la configuration.\n\n* `Développement rapide`: Visant une productivité élevée des développeurs, la conception de Loco se concentre sur la réduction du code passe-partout et la fourniture d'API intuitives, permettant aux développeurs d'intégrer rapidement et de créer des prototypes avec un minimum d'effort.\n\n* `Intégration ORM`: Modélisez avec des entités robustes, éliminant le besoin d'écrire du SQL. Définissez les relations, la validation et la logique sur mesure directement sur vos entités pour une maintenabilité et une évolutivité améliorées.\n\n* `Contrôleurs`: Gérez les paramètres et le contenu des requêtes Web, la validation des requêtes et affichez une réponse tenant compte du contenu. Nous utilisons Axum pour une meilleure performance, simplicité et extensibilité. Les contrôleurs vous permettent également de créer facilement des middlewares, qui peuvent être utilisés pour ajouter une logique telle que l'authentification, la journalisation (logging) ou la gestion des erreurs avant de transmettre les requêtes aux actions du contrôleur principal.\n\n* `Vues`: Loco peut s'intégrer aux moteurs de _templates_ pour générer du contenu HTML dynamique à partir de modèles template.\n\n* `Tâches en arrière-plan`: Effectuer des calculs informatiques ou d'I/O (Entrée/Sortie) intensives en arrière-plan avec une file d'attente sauvegardée Redis ou avec des threads. Implémenter un travailleur (worker) est aussi simple que d'implémenter une fonction d'exécution pour le trait Worker.\n\n* `Scheduler`: Simplifie le système crontab traditionnel, souvent encombrant, en rendant plus facile et plus élégante la planification de tâches ou de scripts shell.\n\n* `Mailers`: Un logiciel de messagerie enverra des e-mails en arrière-plan en utilisant l'infrastructure de travail d'arrière-plan de Loco existante. Tout se passera sans problème pour vous.\n\n* `Stockage`: Loco Storage facilite le travail avec des fichiers via plusieurs opérations. Le stockage peut être en mémoire, sur disque ou utiliser des services cloud tels qu'AWS S3, GCP et Azure.\n\n* `Cache :` Loco fournit une strate cache pour améliorer les performances des applications en stockant les données fréquemment consultées.\n\nPour en savoir plus sur les fonctionnalités de Loco, consultez notre [site Web de documentation](https://loco.rs/docs/getting-started/tour/).\n\n\n## Commencez rapidement\n<!-- <snip id=\"quick-installation-command\" inject_from=\"yaml\" template=\"sh\"> -->\n```sh\ncargo install loco\ncargo install sea-orm-cli # Only when DB is needed\n```\n<!-- </snip> -->\n\nVous pouvez maintenant créer votre nouvelle application (choisissez \"`SaaS` app\").\n\n\n<!-- <snip id=\"loco-cli-new-from-template\" inject_from=\"yaml\" template=\"sh\"> -->\n```sh\n❯ loco new\n✔ ❯ App name? · myapp\n✔ ❯ What would you like to build? · Saas App with client side rendering\n✔ ❯ Select a DB Provider · Sqlite\n✔ ❯ Select your background worker type · Async (in-process tokio async tasks)\n\n🚂 Loco app generated successfully in:\nmyapp/\n\n- assets: You've selected `clientside` for your asset serving configuration.\n\nNext step, build your frontend:\n  $ cd frontend/\n  $ npm install && npm run build\n```\n<!-- </snip> -->\n\nMaintenant, faite `cd` dans votre `myapp` et démarrez votre application:\n\n<!-- <snip id=\"starting-the-server-command-with-output\" inject_from=\"yaml\" template=\"sh\"> -->\n```sh\n$ cargo loco start\n\n                      ▄     ▀\n                                ▀  ▄\n                  ▄       ▀     ▄  ▄ ▄▀\n                                    ▄ ▀▄▄\n                        ▄     ▀    ▀  ▀▄▀█▄\n                                          ▀█▄\n▄▄▄▄▄▄▄  ▄▄▄▄▄▄▄▄▄   ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄ ▀▀█\n██████  █████   ███ █████   ███ █████   ███ ▀█\n██████  █████   ███ █████   ▀▀▀ █████   ███ ▄█▄\n██████  █████   ███ █████       █████   ███ ████▄\n██████  █████   ███ █████   ▄▄▄ █████   ███ █████\n██████  █████   ███  ████   ███ █████   ███ ████▀\n  ▀▀▀██▄ ▀▀▀▀▀▀▀▀▀▀  ▀▀▀▀▀▀▀▀▀▀  ▀▀▀▀▀▀▀▀▀▀ ██▀\n      ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀\n                https://loco.rs\n\nlistening on port 5150\n```\n<!-- </snip> -->\n\n## Servi par Loco\n+ [SpectralOps](https://spectralops.io) - divers services servi par le framework Loco\n+ [Nativish](https://nativi.sh) - app backend servi par le framework Loco\n\n## Contributeurs ✨\nMerci à ces personnes formidables :\n\n<a href=\"https://github.com/loco-rs/loco/graphs/contributors\">\n  <img src=\"https://contrib.rocks/image?repo=loco-rs/loco\" />\n</a>\n\n"
  },
  {
    "path": "README.ja.md",
    "content": "<div align=\"center\">\n\n   <img src=\"https://github.com/loco-rs/loco/assets/83390/992d215a-3cd3-42ee-a1c7-de9fd25a5bac\"/>\n\n   <h1>Locoへようこそ</h1>\n\n   <h3>\n🚂 LocoはRust on Railsです。\n   </h3>\n\n   [![crate](https://img.shields.io/crates/v/loco-rs.svg)](https://crates.io/crates/loco-rs)\n   [![docs](https://docs.rs/loco-rs/badge.svg)](https://docs.rs/loco-rs)\n   [![Discord channel](https://img.shields.io/badge/discord-Join-us)](https://discord.gg/fTvyBzwKS8)\n\n </div>\n\nEnglish · [中文](./README-zh_CN.md) · [Français](./README.fr.md) · [Portuguese (Brazil)](./README-pt_BR.md) ・ 日本語 · [한국어](./README.ko.md) · [Русский](./README.ru.md)\n\n## Locoとは？\n`Loco`はRailsに強くインスパイアされています。RailsとRustの両方を知っているなら、すぐに馴染むでしょう。Railsしか知らなく、Rustに新しい方でも、Locoは新鮮に感じるでしょう。Railsを知っているとは仮定していません。\n\nLocoの動作についての詳細なガイド、例、APIリファレンスは、[ドキュメント](https://loco.rs)をチェックしてください。\n\n## Locoの特徴：\n\n* `設定より規約:` Ruby on Railsに似て、Locoはボイラープレートコードを減らすことでシンプルさと生産性を発揮します。合理的なデフォルトを使用し、開発者が設定に時間を費やすのではなく、ビジネスロジックの記述に集中できるようにします。\n\n* `迅速な開発:` 高い開発者生産性を目指し、Locoの設計はボイラープレートコードを減らし、直感的なAPIを提供することに焦点を当てています。これにより、開発者は迅速に反復し、最小限の努力でプロトタイプを構築できます。\n\n* `ORM統合:` ビジネスモデルを堅牢なエンティティで表現し、SQLを書く必要をなくします。エンティティに直接関係、検証、およびカスタムロジックを定義でき、メンテナンス性とスケーラビリティが向上します。\n\n* `コントローラー:` ウェブリクエストのパラメータ、ボディ、検証を処理し、コンテンツに応じたレスポンスをレンダリングします。最高のパフォーマンス、シンプルさ、拡張性のためにAxumを使用しています。コントローラーは、認証、ロギング、エラーハンドリングなどのロジックを追加するためのミドルウェアを簡単に構築できます。\n\n* `ビュー:` Locoはテンプレートエンジンと統合し、テンプレートから動的なHTMLコンテンツを生成できます。\n\n* `バックグラウンドジョブ:` Redisバックエンドキューやスレッドを使用して、計算またはI/O集約型のジョブをバックグラウンドで実行します。ワーカーを実装するのは、Workerトレイトのperform関数を実装するだけです。\n\n* `スケジューラー:` 従来の、しばしば面倒なcrontabシステムを簡素化し、タスクやシェルスクリプトをスケジュールするのをより簡単かつエレガントにします。\n\n* `メール送信:` メール送信者は、既存のLocoバックグラウンドワーカーインフラストラクチャを使用して、バックグラウンドでメールを配信します。すべてがシームレスに行われます。\n\n* `ストレージ:` Locoのストレージでは、ファイル操作を簡素化します。ストレージはメモリ内、ディスク上、またはAWS S3、GCP、Azureなどのクラウドサービスを使用できます。\n\n* `キャッシュ:` Locoは、頻繁にアクセスされるデータを保存することでアプリケーションのパフォーマンスを向上させるためのキャッシュレイヤーを提供します。\n\nLocoの詳細な機能については、[ドキュメントウェブサイト](https://loco.rs/docs/getting-started/tour/)を確認してください。\n\n## 始め方\n```sh\ncargo install loco\ncargo install sea-orm-cli # データベースが必要な場合のみ\n```\n\n以下で新しいアプリを作成できます（「`SaaS`アプリ」を選択）。\n\n```sh\n❯ loco new\n✔ ❯ App name? · myapp\n✔ ❯ What would you like to build? · SaaS app (with DB and user auth)\n✔ ❯ Select a DB Provider · Sqlite\n✔ ❯ Select your background worker type · Async (in-process tokio async tasks)\n✔ ❯ Select an asset serving configuration · Client (configures assets for frontend serving)\n\n🚂 Loco app generated successfully in:\nmyapp/\n```\n\n次に`myapp`に移動し、アプリを起動します：\n```sh\n$ cargo loco start\n\n                      ▄     ▀\n                                ▀  ▄\n                  ▄       ▀     ▄  ▄ ▄▀\n                                    ▄ ▀▄▄\n                        ▄     ▀    ▀  ▀▄▀█▄\n                                          ▀█▄\n▄▄▄▄▄▄▄  ▄▄▄▄▄▄▄▄▄   ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄ ▀▀█\n██████  █████   ███ █████   ███ █████   ███ ▀█\n██████  █████   ███ █████   ▀▀▀ █████   ███ ▄█▄\n██████  █████   ███ █████       █████   ███ ████▄\n██████  █████   ███ █████   ▄▄▄ █████   ███ █████\n██████  █████   ███  ████   ███ █████   ███ ████▀\n  ▀▀▀██▄ ▀▀▀▀▀▀▀▀▀▀  ▀▀▀▀▀▀▀▀▀▀  ▀▀▀▀▀▀▀▀▀▀ ██▀\n      ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀\n                https://loco.rs\n\nlistening on port 5150\n```\n\n## Locoによって開発されています\n+ [SpectralOps](https://spectralops.io) - Locoフレームワークによる各種サービス\n+ [Nativish](https://nativi.sh) - Locoフレームワークによるアプリバックエンド\n\n## 貢献者 ✨\nこれらの素晴らしい人々に感謝します：\n\n<a href=\"https://github.com/loco-rs/loco/graphs/contributors\">\n  <img src=\"https://contrib.rocks/image?repo=loco-rs/loco\" />\n</a>\n"
  },
  {
    "path": "README.ko.md",
    "content": " <div align=\"center\">\n\n   <img src=\"https://github.com/loco-rs/loco/assets/83390/992d215a-3cd3-42ee-a1c7-de9fd25a5bac\"/>\n\n   <h1>Loco에 오신 것을 환영합니다</h1>\n\n   <h3>\n   🚂 Loco는 Rust on Rails입니다.\n   </h3>\n\n   [![crate](https://img.shields.io/crates/v/loco-rs.svg)](https://crates.io/crates/loco-rs)\n   [![docs](https://docs.rs/loco-rs/badge.svg)](https://docs.rs/loco-rs)\n   [![Discord channel](https://img.shields.io/badge/discord-Join-us)](https://discord.gg/fTvyBzwKS8)\n\n </div>\n\n[English](./README.md) · [中文](./README-zh_CN.md) · [Français](./README.fr.md) · [Portuguese (Brazil)](./README-pt_BR.md) ・ [日本語](./README.ja.md) · 한국어 · [Русский](./README.ru.md) · [Español](./README.es.md)\n\n\n## Loco란?\n`Loco`는 Rails에서 강한 영감을 받았습니다. Rails와 Rust를 모두 알고 계신다면 친숙하게 느껴지실 것이며, Rails만 알고 Rust를 처음 접하시는 분들에게도 Loco는 새롭게 다가올 것입니다. 참고로, Rails에 대한 사전 지식은 필수가 아닙니다.\n\nLoco의 작동 방식에 대해 더 자세히 알아보려면 가이드, 예제, API 참조를 포함한 [문서 웹사이트](https://loco.rs)를 확인해보세요.\n\n## Loco의 주요 기능:\n\n* `설정보다 관습`: Ruby on Rails와 유사하게, Loco는 상용구 코드의 필요성을 줄임으로써 단순성과 생산성을 강조합니다. 합리적인 기본값을 사용하여 개발자가 설정보다는 비즈니스 로직 작성에 집중할 수 있게 합니다.\n\n* `빠른 개발`: 높은 개발자 생산성을 목표로 하며, Loco의 설계는 상용구 코드를 줄이고 직관적인 API를 제공하여 개발자가 최소한의 노력으로 빠르게 반복하고 프로토타입을 구축할 수 있도록 합니다.\n\n* `ORM 통합`: SQL 작성 없이 비즈니스를 강력한 엔티티로 모델링합니다. 관계, 유효성 검사, 사용자 정의 로직을 엔티티에 직접 정의하여 유지보수성과 확장성을 향상시킵니다.\n\n* `컨트롤러`: 웹 요청 매개변수, 본문, 유효성 검사를 처리하고 컨텐츠를 인식하는 응답을 렌더링합니다. 최고의 성능, 단순성, 확장성을 위해 Axum을 사용합니다. 또한 컨트롤러를 통해 인증, 로깅, 오류 처리와 같은 로직을 추가할 수 있는 미들웨어를 쉽게 구축할 수 있습니다.\n\n* `뷰`: Loco는 템플릿에서 동적 HTML 콘텐츠를 생성하기 위해 템플릿 엔진과 통합할 수 있습니다.\n\n* `백그라운드 작업`: Redis 기반 큐 또는 스레드를 사용하여 계산이나 I/O 집약적인 작업을 백그라운드에서 수행합니다. Worker 트레이트에 대한 perform 함수를 구현하는 것만으로도 워커를 구현할 수 있습니다.\n\n* `스케줄러`: 전통적이고 번거로운 crontab 시스템을 단순화하여 작업이나 셸 스크립트를 더 쉽고 우아하게 예약할 수 있습니다.\n\n* `메일러`: 메일러는 기존 loco 백그라운드 워커 인프라를 사용하여 이메일을 백그라운드에서 전달합니다. 모든 과정이 매끄럽게 처리됩니다.\n\n* `스토리지`: Loco 스토리지는 여러 작업을 통해 파일 작업을 용이하게 합니다. 메모리 내, 디스크, AWS S3, GCP, Azure와 같은 클라우드 서비스를 사용할 수 있습니다.\n\n* `캐시`: Loco는 자주 접근하는 데이터를 저장하여 애플리케이션 성능을 향상시키는 캐시 레이어를 제공합니다.\n\n더 많은 Loco 기능을 보려면 [문서 웹사이트](https://loco.rs/docs/getting-started/tour/)를 확인하세요.\n\n\n## 시작하기\n<!-- <snip id=\"quick-installation-command\" inject_from=\"yaml\" template=\"sh\"> -->\n```sh\ncargo install loco\ncargo install sea-orm-cli # Only when DB is needed\n```\n<!-- </snip> -->\n\n이제 새로운 앱을 만들 수 있습니다 (\"`SaaS 앱`\" 선택).\n\n\n<!-- <snip id=\"loco-cli-new-from-template\" inject_from=\"yaml\" template=\"sh\"> -->\n```sh\n❯ loco new\n✔ ❯ App name? · myapp\n✔ ❯ What would you like to build? · Saas App with client side rendering\n✔ ❯ Select a DB Provider · Sqlite\n✔ ❯ Select your background worker type · Async (in-process tokio async tasks)\n\n🚂 Loco app generated successfully in:\nmyapp/\n\n- assets: You've selected `clientside` for your asset serving configuration.\n\nNext step, build your frontend:\n  $ cd frontend/\n  $ npm install && npm run build\n```\n<!-- </snip> -->\n\n이제 `myapp` 디렉토리로 이동하여 앱을 시작하세요:\n\n<!-- <snip id=\"starting-the-server-command-with-output\" inject_from=\"yaml\" template=\"sh\"> -->\n```sh\n$ cargo loco start\n\n                      ▄     ▀\n                                ▀  ▄\n                  ▄       ▀     ▄  ▄ ▄▀\n                                    ▄ ▀▄▄\n                        ▄     ▀    ▀  ▀▄▀█▄\n                                          ▀█▄\n▄▄▄▄▄▄▄  ▄▄▄▄▄▄▄▄▄   ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄ ▀▀█\n██████  █████   ███ █████   ███ █████   ███ ▀█\n██████  █████   ███ █████   ▀▀▀ █████   ███ ▄█▄\n██████  █████   ███ █████       █████   ███ ████▄\n██████  █████   ███ █████   ▄▄▄ █████   ███ █████\n██████  █████   ███  ████   ███ █████   ███ ████▀\n  ▀▀▀██▄ ▀▀▀▀▀▀▀▀▀▀  ▀▀▀▀▀▀▀▀▀▀  ▀▀▀▀▀▀▀▀▀▀ ██▀\n      ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀\n                https://loco.rs\n\nlistening on port 5150\n```\n<!-- </snip> -->\n\n## Loco 사용 사례\n+ [SpectralOps](https://spectralops.io) - Loco 프레임워크로 구동되는 다양한 서비스\n+ [Nativish](https://nativi.sh) - Loco 프레임워크로 구동되는 앱 백엔드\n\n## 기여자 ✨\n이 멋진 분들께 감사드립니다:\n\n<a href=\"https://github.com/loco-rs/loco/graphs/contributors\">\n  <img src=\"https://contrib.rocks/image?repo=loco-rs/loco\" />\n</a>\n"
  },
  {
    "path": "README.md",
    "content": " <div align=\"center\">\n\n   <img src=\"https://github.com/loco-rs/loco/assets/83390/992d215a-3cd3-42ee-a1c7-de9fd25a5bac\"/>\n\n   <h1>Welcome to Loco</h1>\n\n   <h3>\n   <!-- <snip id=\"description\" inject_from=\"yaml\"> -->\n🚂 Loco is Rust on Rails.\n<!--</snip> -->\n   </h3>\n\n   [![crate](https://img.shields.io/crates/v/loco-rs.svg)](https://crates.io/crates/loco-rs)\n   [![docs](https://docs.rs/loco-rs/badge.svg)](https://docs.rs/loco-rs)\n   [![Discord channel](https://img.shields.io/badge/discord-Join-us)](https://discord.gg/fTvyBzwKS8)\n\n </div>\n\n\nEnglish · [中文](./README-zh_CN.md) · [Français](./README.fr.md) · [Portuguese (Brazil)](./README-pt_BR.md) ・ [日本語](./README.ja.md) · [한국어](./README.ko.md) · [Русский](./README.ru.md) · [Español](./README.es.md)\n\n\n## What's Loco?\n`Loco` is strongly inspired by Rails. If you know Rails and Rust, you'll feel at home. If you only know Rails and new to Rust, you'll find Loco refreshing. We do not assume you know Rails.\n\nFor a deeper dive into how Loco works, including detailed guides, examples, and API references, check out our [documentation website](https://loco.rs).\n\n\n## Features of Loco:\n\n* `Convention Over Configuration:` Similar to Ruby on Rails, Loco emphasizes simplicity and productivity by reducing the need for boilerplate code. It uses sensible defaults, allowing developers to focus on writing business logic rather than spending time on configuration.\n\n* `Rapid Development:` Aim for high developer productivity, Loco’s design focuses on reducing boilerplate code and providing intuitive APIs, allowing developers to iterate quickly and build prototypes with minimal effort.\n\n* `ORM Integration:` Model your business with robust entities, eliminating the need to write SQL. Define relationships, validation, and custom logic directly on your entities for enhanced maintainability and scalability.\n\n* `Controllers`: Handle web requests parameters, body, validation, and render a response that is content-aware. We use Axum for the best performance, simplicity, and extensibility. Controllers also allow you to easily build middlewares, which can be used to add logic such as authentication, logging, or error handling before passing requests to the main controller actions.\n\n* `Views:` Loco can integrate with templating engines to generate dynamic HTML content from templates.\n\n* `Background Jobs:` Perform compute or I/O intensive jobs in the background with a Redis backed queue, or with threads. Implementing a worker is as simple as implementing a perform function for the Worker trait.\n\n* `Scheduler:` Simplifies the traditional, often cumbersome crontab system, making it easier and more elegant to schedule tasks or shell scripts.\n\n* `Mailers:` A mailer will deliver emails in the background using the existing loco background worker infrastructure. It will all be seamless for you.\n\n* `Storage:` In Loco Storage, we facilitate working with files through multiple operations. Storage can be in-memory, on disk, or use cloud services such as AWS S3, GCP, and Azure.\n\n* `Cache:` Loco provides an cache layer to improve application performance by storing frequently accessed data.\n\nSo see more Loco features, check out our [documentation website](https://loco.rs/docs/getting-started/tour/).\n\n\n\n## Getting Started\n<!-- <snip id=\"quick-installation-command\" inject_from=\"yaml\" template=\"sh\"> -->\n```sh\ncargo install loco\ncargo install sea-orm-cli # Only when DB is needed\n```\n<!-- </snip> -->\n\nNow you can create your new app (choose \"`SaaS` app\").\n\n\n<!-- <snip id=\"loco-cli-new-from-template\" inject_from=\"yaml\" template=\"sh\"> -->\n```sh\n❯ loco new\n✔ ❯ App name? · myapp\n✔ ❯ What would you like to build? · Saas App with client side rendering\n✔ ❯ Select a DB Provider · Sqlite\n✔ ❯ Select your background worker type · Async (in-process tokio async tasks)\n\n🚂 Loco app generated successfully in:\nmyapp/\n\n- assets: You've selected `clientside` for your asset serving configuration.\n\nNext step, build your frontend:\n  $ cd frontend/\n  $ npm install && npm run build\n```\n<!-- </snip> -->\n\n Now `cd` into your `myapp` and start your app:\n<!-- <snip id=\"starting-the-server-command-with-output\" inject_from=\"yaml\" template=\"sh\"> -->\n```sh\n$ cargo loco start\n\n                      ▄     ▀\n                                ▀  ▄\n                  ▄       ▀     ▄  ▄ ▄▀\n                                    ▄ ▀▄▄\n                        ▄     ▀    ▀  ▀▄▀█▄\n                                          ▀█▄\n▄▄▄▄▄▄▄  ▄▄▄▄▄▄▄▄▄   ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄ ▀▀█\n██████  █████   ███ █████   ███ █████   ███ ▀█\n██████  █████   ███ █████   ▀▀▀ █████   ███ ▄█▄\n██████  █████   ███ █████       █████   ███ ████▄\n██████  █████   ███ █████   ▄▄▄ █████   ███ █████\n██████  █████   ███  ████   ███ █████   ███ ████▀\n  ▀▀▀██▄ ▀▀▀▀▀▀▀▀▀▀  ▀▀▀▀▀▀▀▀▀▀  ▀▀▀▀▀▀▀▀▀▀ ██▀\n      ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀\n                https://loco.rs\n\nlistening on port 5150\n```\n<!-- </snip> -->\n\n## Powered by Loco\n+ [SpectralOps](https://spectralops.io) - various services powered by Loco\n  framework\n+ [Nativish](https://nativi.sh) - app backend powered by Loco framework\n\n## Contributors ✨\nThanks goes to these wonderful people:\n\n<a href=\"https://github.com/loco-rs/loco/graphs/contributors\">\n  <img src=\"https://contrib.rocks/image?repo=loco-rs/loco\" />\n</a>\n"
  },
  {
    "path": "README.ru.md",
    "content": " <div align=\"center\">\n\n   <img src=\"https://github.com/loco-rs/loco/assets/83390/992d215a-3cd3-42ee-a1c7-de9fd25a5bac\"/>\n\n   <h1>Добро пожаловать в *Loco*</h1>\n\n   <h3>\n   <!-- <snip id=\"description\" inject_from=\"yaml\"> -->\n🚂 Loco is Rust on Rails.\n<!--</snip> -->\n   </h3>\n\n   [![crate](https://img.shields.io/crates/v/loco-rs.svg)](https://crates.io/crates/loco-rs)\n   [![docs](https://docs.rs/loco-rs/badge.svg)](https://docs.rs/loco-rs)\n   [![Discord channel](https://img.shields.io/badge/discord-Join-us)](https://discord.gg/fTvyBzwKS8)\n\n </div>\n\n[English](./README.md) · [中文](./README-zh_CN.md) · [Français](./README.fr.md) · [Portuguese (Brazil)](./README-pt_BR.md) ・ [日本語](./README.ja.md) · Русский · [Español](./README.es.md)\n\n\n## Что такое Loco?\n*Loco* сильно вдохновлён проектом *Ruby on Rails*. Если вы знакомы и с *Rails*, и с *Rust*, вы будете чувствовать себя как дома. Если вы знаете только *Rails*, и не знакомы с *Rust*, *Loco* будет для вас чем-то освежающим.\n\nЕсли вам интересно узнать внутрение устройство *Loco*, включая детальные гайды, примеры, и устройство API, почитайте нашу [документацию](https://loco.rs).\n\n\n## Фишки Loco:\n\n- **Простота превыше конфигурации**: Подобно *Ruby on Rails*, *Loco* делает упор на простоту и продуктивность, снижая потребность в лишнем коде. *Loco* использует оптимальные настройки по-умолчанию, давая разработчикам возможность сфокусироваться на написании бизнес логики, а не конфигурации.\n- **Быстрая разработка**: Ставя акцент на высокой производительности разработчика, Дизайн *Loco* фокусируется на сокращении ненужного кода и предоставления интуитивного API. Это позволяет быстро создавать прототипы без лишних усилий.\n- **ORM интеграция**: Стройте свой бизнес с крепкими составляющими, убирая необходимость писать SQL. Определяйте взаимосвязи, проверку, и кастомную логику прямо в составляющих, упрощая поддержку и рост кодовой базы.\n- **Контролеры**: Обрабатывайте параметры и данные web-запросов, проверяйте их содержимое, отображайте ответ с учетом запроса. Мы используем *Axum* для достижения наилучшей производительности, простоты, и возможности расширения. Также, контролеры облегчают внедрение middleware. Это может быть использовано для добавления всевозможной логики: аутентификации, логгинга, или обработки ошибок перед отправкой на сервер.\n- **Виды**: *Loco* может интегрироваться с template-движками для генерации динамического HTML из шаблонов.\n- **Фоновые задачи**: Исполняйте I/O и другие тяжелые операции в фоновом режиме с помощью *Redis*, или потоков. Для написания функционала фоновой задачи нужно всего лишь написать функцию `perform` из `trait Worker`.\n- **Планировщик**: Облегчает традиционную, часто громоздкую систему, упрощая планировку задач и исполнение shell-скриптов.\n- **Отправка электронной почты**: Отправка электронной почты в фоновом режиме, без необходимости создавать новую фоновую задачу.\n- **Хранилище**: Мы способствуем работе с файлами несколькими путями: хранение в памяти, на диске, или использование облачных сервисов как *AWS*, *S3*, *GCP*, и *Azure*.\n- **Кэширование**: *Loco* кэширует частые запросы для улучшения производительности приложения.\n\nУ *Loco* есть ещё множество фишек, котрые вы можете посмотреть на [сайте документации](https://loco.rs/docs/getting-started/tour/).\n\n\n## Установка\n<!-- <snip id=\"quick-installation-command\" inject_from=\"yaml\" template=\"sh\"> -->\n```sh\ncargo install loco\ncargo install sea-orm-cli # Only when DB is needed\n```\n<!-- </snip> -->\n\nТеперь вы можете создать свое новое приложение (выберете \"`SaaS` app\").\n\n\n<!-- <snip id=\"loco-cli-new-from-template\" inject_from=\"yaml\" template=\"sh\"> -->\n```sh\n❯ loco new\n✔ ❯ App name? · myapp\n✔ ❯ What would you like to build? · Saas App with client side rendering\n✔ ❯ Select a DB Provider · Sqlite\n✔ ❯ Select your background worker type · Async (in-process tokio async tasks)\n\n🚂 Loco app generated successfully in:\nmyapp/\n\n- assets: You've selected `clientside` for your asset serving configuration.\n\nNext step, build your frontend:\n  $ cd frontend/\n  $ npm install && npm run build\n```\n<!-- </snip> -->\n\nТеперь выполните `cd` в папку `myapp` и запускайте приложение:\n<!-- <snip id=\"starting-the-server-command-with-output\" inject_from=\"yaml\" template=\"sh\"> -->\n```sh\n$ cargo loco start\n\n                      ▄     ▀\n                                ▀  ▄\n                  ▄       ▀     ▄  ▄ ▄▀\n                                    ▄ ▀▄▄\n                        ▄     ▀    ▀  ▀▄▀█▄\n                                          ▀█▄\n▄▄▄▄▄▄▄  ▄▄▄▄▄▄▄▄▄   ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄ ▀▀█\n██████  █████   ███ █████   ███ █████   ███ ▀█\n██████  █████   ███ █████   ▀▀▀ █████   ███ ▄█▄\n██████  █████   ███ █████       █████   ███ ████▄\n██████  █████   ███ █████   ▄▄▄ █████   ███ █████\n██████  █████   ███  ████   ███ █████   ███ ████▀\n  ▀▀▀██▄ ▀▀▀▀▀▀▀▀▀▀  ▀▀▀▀▀▀▀▀▀▀  ▀▀▀▀▀▀▀▀▀▀ ██▀\n      ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀\n                https://loco.rs\n\nlistening on port 5150\n```\n<!-- </snip> -->\n\n## Проекты, использующие *Loco*\n+ [SpectralOps](https://spectralops.io) - различные сервисы, использующие *Loco*\n  framework\n+ [Nativish](https://nativi.sh) - backend приложения, использующий *Loco*\n\n## Контрибьютеры ✨\nСпасибо всем этим прекрасным людям:\n\n<a href=\"https://github.com/loco-rs/loco/graphs/contributors\">\n  <img src=\"https://contrib.rocks/image?repo=loco-rs/loco\" />\n</a>\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\nBy researching and submitting a vulnerability, you'll be helping open source and this project's goal to provide a fast to build fast to run Web framework based on Rust.\n\n\n## Reporting a Vulnerability\n\nPlease report directly to [dotan@rng0.io](mailto:dotan@rng0.io).\n\nWe will credit you as a committer with every vulnerability you find that we can validate.\n"
  },
  {
    "path": "build/embedded_assets.rs",
    "content": "use std::collections::{HashMap, HashSet};\nuse std::{\n    env,\n    fs::{self, File},\n    io::{self, Write},\n    path::{Path, PathBuf},\n};\n\npub fn build_static_assets(out_dir: &Path) {\n    // Determine the application root directory using Cargo environment variables\n    let Some(app_dir) = find_app_directory(out_dir) else {\n        eprintln!(\"Error: Could not determine application directory\");\n        return;\n    };\n\n    let app_dir_str = app_dir.to_string_lossy().to_string();\n\n    println!(\"cargo:warning=Building with embedded_assets feature\");\n    println!(\"cargo:warning=Application directory: {app_dir_str}\");\n    println!(\"cargo:warning=Assets will only be loaded from the application directory\");\n    println!(\"cargo:rerun-if-changed={app_dir_str}/assets/\");\n    println!(\"cargo:rerun-if-changed={app_dir_str}/src/assets/\");\n    // Also run build script again if the build files change\n    println!(\"cargo:rerun-if-changed=build/embedded_assets.rs\");\n\n    let generated_path = out_dir.join(\"generated_code\");\n\n    // Create the directory if it doesn't exist\n    if let Err(e) = fs::create_dir_all(&generated_path) {\n        eprintln!(\"Warning: Could not create directory: {e}\");\n        return;\n    }\n\n    // Only search in the application directory\n    let app_root = app_dir;\n\n    // Find all directories recursively, without filtering by name\n    let all_dirs = discover_all_directories(&app_root.join(\"assets\"));\n\n    println!(\"cargo:warning=Discovered directories for assets:\");\n    for dir in &all_dirs {\n        println!(\"cargo:warning=  - {}\", dir.display());\n    }\n\n    // Single collection for all files\n    let mut all_files = HashMap::new();\n\n    // Store the assets directory reference to pass to collect_all_files\n    let assets_dir = app_root.join(\"assets\");\n\n    // Process all discovered directories\n    for dir in &all_dirs {\n        // Process all files in this directory\n        collect_all_files(dir, &assets_dir, &mut all_files);\n    }\n\n    // Generate code for all assets\n    if all_files.is_empty() {\n        println!(\"cargo:warning=No asset files found\");\n        // Generate empty asset files if no files found\n        if let Err(e) = generate_empty_asset_files(&generated_path) {\n            eprintln!(\"Warning: Failed to generate empty asset files: {e}\");\n        }\n    } else {\n        println!(\"cargo:warning=Found {} asset files\", all_files.len());\n        if let Err(e) = generate_asset_code(&all_files, &generated_path) {\n            eprintln!(\"Warning: Failed to generate asset code: {e}\");\n        }\n    }\n}\n\npub fn find_app_directory(out_dir: &Path) -> Option<PathBuf> {\n    // Find project root from OUT_DIR by going up to parent of \"target\" directory\n    let mut path = out_dir.to_path_buf();\n    while path.pop() {\n        if path.file_name().and_then(|n| n.to_str()) == Some(\"target\") && path.pop() {\n            return Some(path);\n        }\n\n        // Safety check\n        if path.as_os_str().is_empty() {\n            break;\n        }\n    }\n\n    // Fallback to current directory\n    env::current_dir().ok()\n}\n\npub fn discover_all_directories(app_root: &Path) -> Vec<PathBuf> {\n    let mut directories = Vec::new();\n    let mut visited = HashSet::new();\n\n    // Only include the directory if it exists\n    if app_root.exists() {\n        // Add the root directory itself\n        directories.push(app_root.to_path_buf());\n\n        // Start recursive discovery\n        recursively_collect_directories(app_root, &mut directories, &mut visited);\n    }\n\n    // Sort directories by their string representation to ensure consistent ordering\n    directories.sort_by(|a, b| {\n        a.to_string_lossy()\n            .to_string()\n            .cmp(&b.to_string_lossy().to_string())\n    });\n\n    directories\n}\n\npub fn recursively_collect_directories(\n    dir: &Path,\n    directories: &mut Vec<PathBuf>,\n    visited: &mut std::collections::HashSet<PathBuf>,\n) {\n    // Check if we've already visited this directory\n    if !visited.insert(dir.to_path_buf()) {\n        return;\n    }\n\n    // Continue recursively discovering subdirectories\n    if let Ok(entries) = fs::read_dir(dir) {\n        for entry in entries.flatten() {\n            let path = entry.path();\n\n            if path.is_dir() {\n                // Add this directory to our list\n                directories.push(path.clone());\n                // Continue recursion\n                recursively_collect_directories(&path, directories, visited);\n            }\n        }\n    }\n}\n\npub fn collect_all_files(dir: &Path, assets_dir: &Path, all_files: &mut HashMap<String, String>) {\n    if let Ok(entries) = fs::read_dir(dir) {\n        for entry in entries.flatten() {\n            let path = entry.path();\n\n            if path.is_file() {\n                // Skip if we can't determine the file path or extension\n                let full_path = path.to_string_lossy().to_string();\n\n                // Create a relative path based on the assets directory\n                let Ok(rel_path) = path.strip_prefix(assets_dir) else {\n                    println!(\n                        \"cargo:warning=Failed to strip prefix for path: {}\",\n                        path.display()\n                    );\n                    continue; // Skip this file if we can't determine its relative path\n                };\n\n                // Format the key as a path, using forward slashes\n                let mut key = format!(\"/{}\", rel_path.to_string_lossy().replace('\\\\', \"/\"));\n\n                // Remove any double slashes\n                key = key.replace(\"//\", \"/\");\n\n                // Special handling for templates in views directory\n                if key.starts_with(\"/views/\") {\n                    // For templates, we want to:\n                    // 1. Strip \"/views/\" prefix for proper Tera template inheritance\n                    // 2. Keep the relative path structure for nested templates\n                    key = key.trim_start_matches(\"/views/\").to_string();\n                }\n\n                // Log what we found\n                println!(\"cargo:warning=Found asset: {} -> {}\", path.display(), key);\n\n                // Store the file\n                all_files.insert(full_path, key);\n            }\n        }\n    }\n}\n\n#[allow(clippy::too_many_lines)]\npub fn generate_asset_code(\n    all_files: &HashMap<String, String>,\n    output_path: &Path,\n) -> io::Result<()> {\n    // Create vectors to track which files go where\n    let mut static_assets = Vec::new();\n    let mut template_files = Vec::new();\n\n    // Simple categorization: if file ends with .html or .htm, it's a template, otherwise static asset\n    for (path, key) in all_files {\n        if std::path::Path::new(key)\n            .extension()\n            .is_some_and(|ext| ext.eq_ignore_ascii_case(\"html\"))\n            || std::path::Path::new(key)\n                .extension()\n                .is_some_and(|ext| ext.eq_ignore_ascii_case(\"htm\"))\n        {\n            template_files.push((path.clone(), key.clone()));\n        } else {\n            static_assets.push((path.clone(), key.clone()));\n        }\n    }\n\n    // Sort static assets by key for consistent output\n    static_assets.sort_by(|a, b| a.1.cmp(&b.1));\n\n    // Build template dependency map and sort templates\n    let mut template_deps: HashMap<String, Option<String>> = HashMap::new();\n\n    println!(\"cargo:warning=Analyzing template dependencies...\");\n\n    // First pass: read all template contents and find their dependencies\n    for (path, key) in &template_files {\n        println!(\"cargo:warning=Reading template: {key}\");\n        match fs::read_to_string(path) {\n            Ok(content) => {\n                // Look for {% extends \"...\" %} pattern\n                if let Some(extends) = content\n                    .lines()\n                    .find(|line| line.trim().starts_with(\"{% extends\"))\n                {\n                    if let Some(parent) = extends\n                        .split('\"')\n                        .nth(1)\n                        .or_else(|| extends.split('\\'').nth(1))\n                    {\n                        template_deps.insert(key.clone(), Some(parent.to_string()));\n                        println!(\"cargo:warning=Template {key} extends {parent}\");\n                    }\n                } else {\n                    template_deps.insert(key.clone(), None);\n                    println!(\"cargo:warning=Template {key} has no parent\");\n                }\n            }\n            Err(e) => {\n                println!(\"cargo:warning=Failed to read template {path}: {e}\");\n            }\n        }\n    }\n\n    println!(\"cargo:warning=Template dependencies:\");\n    for (template, parent) in &template_deps {\n        if let Some(p) = parent {\n            println!(\"cargo:warning=  {template} -> {p}\");\n        } else {\n            println!(\"cargo:warning=  {template} (no parent)\");\n        }\n    }\n\n    // Sort templates so that parents come before children\n    let mut sorted_templates = Vec::new();\n    let mut processed = HashSet::new();\n\n    // First add all base templates (those with no parents), sorted alphabetically\n    let mut base_templates: Vec<_> = template_deps\n        .iter()\n        .filter(|(_, parent)| parent.is_none())\n        .map(|(key, _)| key.clone())\n        .collect();\n    base_templates.sort(); // Sort base templates alphabetically\n    for key in base_templates {\n        println!(\"cargo:warning=Adding base template: {key}\");\n        processed.insert(key.clone());\n        sorted_templates.push(key);\n    }\n\n    // Then add all child templates, level by level\n    let mut added_in_this_pass;\n    while {\n        added_in_this_pass = false;\n        let mut level_templates = Vec::new();\n\n        // Collect all templates at this level\n        for (key, parent) in &template_deps {\n            if processed.contains(key) {\n                continue;\n            }\n            if let Some(parent) = parent {\n                if processed.contains(parent) {\n                    level_templates.push(key.clone());\n                }\n            }\n        }\n\n        // Sort templates at this level alphabetically\n        level_templates.sort();\n\n        // Add them to the final list\n        for key in level_templates {\n            if let Some(Some(parent)) = template_deps.get(&key) {\n                println!(\"cargo:warning=Adding child template: {key} (extends {parent})\");\n            }\n            processed.insert(key.clone());\n            sorted_templates.push(key);\n            added_in_this_pass = true;\n        }\n\n        added_in_this_pass\n    } {}\n\n    // Add any remaining templates that weren't processed, sorted alphabetically\n    let mut remaining: Vec<_> = template_deps\n        .keys()\n        .filter(|key| !processed.contains(*key))\n        .cloned()\n        .collect();\n    remaining.sort();\n    for key in remaining {\n        println!(\"cargo:warning=Adding unprocessed template: {key}\");\n        sorted_templates.push(key);\n    }\n\n    println!(\"cargo:warning=Final template order:\");\n    for (idx, template) in sorted_templates.iter().enumerate() {\n        println!(\"cargo:warning=  {}. {}\", idx + 1, template);\n    }\n\n    // Generate static assets file\n    let static_file = output_path.join(\"static_assets.rs\");\n\n    // Create the static assets content\n    let mut static_lines = vec![\n        \"#[must_use]\\n\".to_string(),\n        \"pub fn get_embedded_static_assets() -> std::collections::HashMap<String, &'static [u8]> {\\n\".to_string(),\n        \"    let mut assets = std::collections::HashMap::new();\\n\".to_string()\n    ];\n\n    for (path, key) in &static_assets {\n        let insert_line = format!(\n            r#\"    assets.insert(\"{0}\".to_string(), include_bytes!(\"{1}\") as &[u8]);\"#,\n            key,\n            path.replace('\\\\', \"/\")\n        );\n        static_lines.push(format!(\"{insert_line}\\n\"));\n    }\n\n    static_lines.push(\"    assets\\n\".to_string());\n    static_lines.push(\"}\\n\".to_string());\n\n    // Write static assets content to file\n    let mut static_file = File::create(static_file)?;\n    for line in static_lines {\n        static_file.write_all(line.as_bytes())?;\n    }\n\n    // Generate templates file\n    let templates_file = output_path.join(\"view_templates.rs\");\n\n    // Create the templates content with detailed comments\n    let mut template_lines = vec![\n        \"/// Returns a BTreeMap of templates in dependency order (parents before children)\\n\"\n            .to_string(),\n        \"#[must_use]\\n\".to_string(),\n        \"pub fn get_embedded_templates() -> std::collections::BTreeMap<String, &'static str> {\\n\"\n            .to_string(),\n        \"    let mut templates = std::collections::BTreeMap::new();\\n\".to_string(),\n    ];\n\n    // Add templates in dependency order with comments\n    for template_key in &sorted_templates {\n        if let Some((path, _)) = template_files.iter().find(|(_, k)| k == template_key) {\n            // Add a comment showing the dependency\n            if let Some(Some(parent)) = template_deps.get(template_key) {\n                template_lines.push(format!(\"    // Template that extends {parent}\\n\"));\n            } else {\n                template_lines.push(\"    // Base template with no parent\\n\".to_string());\n            }\n\n            let insert_line = format!(\n                r#\"    templates.insert(\"{0}\".to_string(), include_str!(\"{1}\"));\"#,\n                template_key,\n                path.replace('\\\\', \"/\")\n            );\n            template_lines.push(format!(\"{insert_line}\\n\"));\n        }\n    }\n\n    template_lines.push(\"\\n    templates\\n\".to_string());\n    template_lines.push(\"}\\n\".to_string());\n\n    // Write templates content to file\n    let mut templates_file = File::create(templates_file)?;\n    for line in template_lines {\n        templates_file.write_all(line.as_bytes())?;\n    }\n\n    println!(\n        \"cargo:warning=Generated code for {} static assets and {} templates\",\n        static_assets.len(),\n        sorted_templates.len()\n    );\n\n    Ok(())\n}\n\npub fn generate_empty_asset_files(output_path: &Path) -> io::Result<()> {\n    // Generate empty static assets file\n    let static_file = output_path.join(\"static_assets.rs\");\n    let static_code = r\"#[must_use]\npub fn get_embedded_static_assets() -> std::collections::HashMap<String, &'static [u8]> {\n    // No assets found\n    std::collections::HashMap::new()\n}\n\";\n    let mut file = File::create(static_file)?;\n    file.write_all(static_code.as_bytes())?;\n\n    // Generate empty templates file\n    let templates_file = output_path.join(\"view_templates.rs\");\n    let templates_code = r\"#[must_use]\npub fn get_embedded_templates() -> std::collections::HashMap<String, &'static str> {\n    // No templates found\n    std::collections::HashMap::new()\n}\n\";\n    let mut file = File::create(templates_file)?;\n    file.write_all(templates_code.as_bytes())?;\n\n    Ok(())\n}\n"
  },
  {
    "path": "build.rs",
    "content": "#[cfg(feature = \"embedded_assets\")]\nuse std::{env, path::Path};\n\nfn main() {\n    #[cfg(feature = \"embedded_assets\")]\n    embedded_assets_main();\n\n    #[cfg(not(feature = \"embedded_assets\"))]\n    {\n        // No-op when feature is disabled\n    }\n}\n\n#[cfg(feature = \"embedded_assets\")]\nfn embedded_assets_main() {\n    // Import the embedded_assets module from the build directory\n    #[path = \"build/embedded_assets.rs\"]\n    mod embedded_assets;\n    use embedded_assets::build_static_assets;\n\n    // Get OUT_DIR environment variable - this is required for build scripts\n    let out_dir = env::var(\"OUT_DIR\").unwrap_or_else(|e| {\n        // This should trigger a build failure\n        panic!(\"OUT_DIR environment variable not set: {e}\");\n    });\n\n    // Convert to a path\n    let out_dir_path = Path::new(&out_dir);\n\n    // Call the build_static_assets function with the OUT_DIR\n    build_static_assets(out_dir_path);\n}\n"
  },
  {
    "path": "docs-site/.gitignore",
    "content": "public/\nnode_modules/"
  },
  {
    "path": "docs-site/config.toml",
    "content": "# The URL the site will be built for\nbase_url = \"https://loco.rs\"\ntitle = \"Loco.rs - Productivity-first Rust Fullstack Web Framework\"\ndescription = \"Loco.rs is like Ruby on Rails for Rust. Use it to quickly build and deploy Rust based apps from zero to production.\"\n\n\n# Whether to automatically compile all Sass files in the sass directory\ncompile_sass = true\n\n# Whether to generate a feed file for the site\ngenerate_feeds = true\n\nfeed_filenames = [\"blog/atom.xml\", \"blog/rss.xml\"]\n\n# When set to \"true\", the generated HTML files are minified.\nminify_html = false\n\n# The taxonomies to be rendered for the site and their configuration.\ntaxonomies = [\n  { name = \"authors\" }, # Basic definition: no feed or pagination\n]\n\n\n# Whether to build a search index to be used later on by a JavaScript library\nbuild_search_index = true\n\n[markdown]\n# Whether to do syntax highlighting\n# Theme can be customised by setting the `highlight_theme` variable to a theme supported by Zola\nhighlight_theme = \"css\"\nhighlight_code = true\nhighlight_themes_css = [\n  { theme = \"OneHalfDark\", filename = \"syntax-theme-dark.css\" },\n  { theme = \"OneHalfLight\", filename = \"syntax-theme-light.css\" },\n]\n[extra]\n# Put all your custom variables here\n\n# Menu items\n[[extra.menu.main]]\nname = \"Docs\"\nsection = \"docs\"\nurl = \"/docs/getting-started/tour/\"\nweight = 10\n\n[[extra.menu.main]]\nname = \"Blog\"\nsection = \"blog\"\nurl = \"/blog/\"\nweight = 20\n\n[[extra.menu.main]]\nname = \"Casts\"\nsection = \"casts\"\nurl = \"/casts/\"\n\n[[extra.menu.social]]\nname = \"Twitter\"\npre = '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-twitter\"><path d=\"M23 3a10.9 10.9 0 0 1-3.14 1.53 4.48 4.48 0 0 0-7.86 3v1A10.66 10.66 0 0 1 3 4s-4 9 5 13a11.64 11.64 0 0 1-7 2c9 5 20 0 20-11.5a4.5 4.5 0 0 0-.08-.83A7.72 7.72 0 0 0 23 3z\"></path></svg>'\nurl = \"https://twitter.com/jondot\"\nweight = 10\n\n[[extra.menu.social]]\nname = \"GitHub\"\npre = '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-github\"><path d=\"M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22\"></path></svg>'\nurl = \"https://github.com/loco-rs/loco\"\npost = \"v0.1.0\"\nweight = 20\n\n\n[[extra.homepage.features]]\nname = \"Models\"\ndescription = 'Model your business with rich entities and avoid writing SQL, backed by SeaORM. Build relations, validation and custom logic on your entities for the best maintainability.'\nexample = '''```rust\nimpl Model {\n  pub async fn find_by_email(db: &DatabaseConnection, email: &str)\n  -> ModelResult<Self> {\n\n      Users::find()\n        .filter(eq(Column::Email, email))\n        .one(db).await?\n        .ok_or_else(|| ModelError::EntityNotFound)\n  }\n  \n  pub async fn create_report(&self, ctx: &AppContext) -> Result<()> {\n      ReportWorker::perform_later(\n        &ctx, \n        ReportArgs{ user_id: self.id }\n      ).await?;\n  }\n}\n```\n'''\n\n[[extra.homepage.features]]\nname = \"Controllers\"\ndescription = 'Handle Web requests parameters, body, validation, and render a response that is content-aware. We use Axum for the best performance, simplicity and extensibility.'\nexample = '''```rust\npub async fn get_one(\n    respond_to: RespondTo,\n    Path(id): Path<i32>,\n    State(ctx): State<AppContext>,\n) -> Result<Response> {\n    let item = Notes::find_by_id(id).one(&ctx.db).await?;\n    match respond_to {\n      RespondTo::Html => html_view(&item),\n      _ => format::json(item),\n    }\n}\n\npub fn routes() -> Routes {\n    Routes::new()\n      .prefix(\"notes\")\n      .add(\"/{id}\", get(get_one))\n}\n```\n'''\n\n[[extra.homepage.features]]\nname = \"Views\"\ndescription = 'Use server-rendered templates with Tera or JSON. Loco can render views on the server or work with a frontend app seamlessly. Configure your fullstack set up any way you like.'\nexample = '''```rust\n\n// Literals\nformat::text(\"Loco\")\n\n// Tera view engine\nformat::render().view(v, \"home/hello.html\", json!({}))\n\n// strongly typed JSON responsed, backed by `serde`\nformat::json(Health { ok: true })\n\n// Etags, cookies, and more\nformat::render().etag(\"loco-etag\")?.empty()\n\n```\n'''\n\n[[extra.homepage.features]]\nname = \"Background Jobs\"\ndescription = 'Perform compute or I/O intensive jobs in the background with a Redis backed queue, or with threads. Implementing a worker is as simple as implementing a <code>perform</code> function for the <code>Worker</code> trait.'\nexample = '''```rust\nimpl worker::Worker<DownloadArgs> for UsersReportWorker {\n    async fn perform(&self, args: DownloadArgs) -> worker::Result<()> {\n        let all = Users::find()\n          .all(&self.ctx.db)\n          .await\n          .map_err(Box::from)?;\n        for user in &all {\n          println!(\"user: {}\", user.id);\n        }\n        Ok(())\n    }\n}\n\n```\n'''\n\n[[extra.homepage.features]]\nname = \"Deployment\"\ndescription = 'Easily generate deployment configurations with a guided CLI interface. Select from deployment options for tailored deployment setups.'\nexample = '''```sh\n\n$ cargo loco generate deployment\n? ❯ Choose your deployment ›\n❯ Docker\n❯ Nginx\n\n..\n✔ ❯ Choose your deployment · Docker\nskipped (exists): \"Dockerfile\"\nadded: \".dockerignore\"\n\n```\n'''\n\n[[extra.homepage.features]]\nname = \"Scheduler\"\ndescription = 'Simplifies the traditional, often cumbersome crontab system, making it easier and more elegant to schedule tasks or shell scripts.'\nexample = '''```yaml\njobs:\n  db_vaccum:\n    run: \"db_vaccum.sh\"\n    shell: true\n    schedule: \"0 0 * * *\"\n    tags: [\"maintenance\"]\n\n  send_birthday:\n    run: \"user_birthday_task\"\n    schedule: \"Run every 2 hours\"\n    tags: [\"marketing\"]\n       \n```\n'''\n"
  },
  {
    "path": "docs-site/content/_index.md",
    "content": "+++\ntitle = \"Loco\"\n\n\n# The homepage contents\n[extra]\nlead = 'The <em>one-person framework</em> for Rust for side-projects and startups'\nurl = \"/docs/getting-started/tour/\"\nurl_button = \"Get started\"\n\n# Menu items\n[[extra.menu.main]]\nname = \"Docs\"\nsection = \"docs\"\nurl = \"/docs/getting-started/tour/\"\nweight = 10\n\n[[extra.menu.main]]\nname = \"Blog\"\nsection = \"blog\"\nurl = \"/blog/\"\nweight = 20\n\n[[extra.menu.main]]\nname = \"Casts\"\nsection = \"casts\"\nurl = \"/casts/\"\n\nweight = 20\n[[extra.list]]\ntitle = \"🔋 Batteries included\"\ncontent = 'Empower the 1-person team. Service, data, emails, background jobs, tasks, CLI to drive it, everything is included.'\n\n[[extra.list]]\ntitle = \"🔮 Rails is great\"\ncontent = 'Loco follows Rails. There, I said it. Rails concepts are carefully adapted to modern Rust development.'\n\n[[extra.list]]\ntitle = \"🏅 Deliver with confidence\"\ncontent = \"Unapologetically optimized for the solo developer. Complexity and heavylifting is tucked away.\"\n\n[[extra.list]]\ntitle = \"⚡️ Scale when needed\"\ncontent = \"Split, reconfigure, or use only parts of Loco when you need to. Build and grow without pain.\"\n\n[[extra.list]]\ntitle = \"🚀️ Build incrementally\"\ncontent = \"Use what you need. Just a service, a service with a database, a background job worker, or a task.\"\n\n[[extra.list]]\ntitle = \"🚦Test driven everything\"\ncontent = \"Test your app with very little effort. Models, controllers, background jobs and more. Ship fast with confidence.\"\n\n+++\n"
  },
  {
    "path": "docs-site/content/authors/_index.md",
    "content": "+++\ntitle = \"Authors\"\ndescription = \"The authurs of the blog articles.\"\ndraft = false\n\n# If add a new author page in this section, please add a new item,\n# and the format is as follows:\n#\n# \"author-name-in-url\" = \"the-full-path-of-the-author-page\"\n#\n# Note: We use quoted keys here.\n[extra.author_pages]\n\"team-loco\" = \"authors/team-loco.md\"\n\"limpidcrypto\" = \"authors/limpidcrypto.md\"\n+++\n\nThe authors of the blog articles."
  },
  {
    "path": "docs-site/content/authors/limpidcrypto.md",
    "content": "+++\r\ntitle = \"LimpidCrypto\"\r\ndescription = \"Building open source tools for cryptocurrency development\"\r\ndate = 2024-01-25T18:03:52+01:00\r\nupdated = 2024-01-25T18:03:52+01:00\r\ndraft = false\r\n+++\r\n\r\nCreating the Building Blocks for Cryptocurrency Development. To my [website](https://limpidcrypto.com)."
  },
  {
    "path": "docs-site/content/authors/team-loco.md",
    "content": "+++\ntitle = \"Team Loco\"\ndescription = \"Creators of the Loco framework\"\ndate = 2021-04-01T08:50:45+00:00\nupdated = 2021-04-01T08:50:45+00:00\ndraft = false\n+++\n\nPrimary maintainers of the [Loco](https://loco.rs) framework: [Dotan Nahum](https://github.com/jondot), [Elad Kaplan](https://github.com/kaplanelad).\n\n"
  },
  {
    "path": "docs-site/content/blog/_index.md",
    "content": "+++\ntitle = \"Blog\"\ndescription = \"Blog\"\nsort_by = \"date\"\npaginate_by = 10\ntemplate = \"blog/section.html\"\n+++\n"
  },
  {
    "path": "docs-site/content/blog/angular-frontend.md",
    "content": "+++\r\ntitle = \"Creating Frontend Website Using Angular\"\r\ndescription = \"Setting up a Loco app for serving an Angular clientside app is easy. Learn how to configure and set up a full-stack Angular app with Loco.\"\r\ndate = 2024-01-25T18:03:52+01:00\r\nupdated = 2024-01-25T18:03:52+01:00\r\ndraft = false\r\ntemplate = \"blog/page.html\"\r\n\r\n[taxonomies]\r\nauthors = [\"LimpidCrypto\"]\r\n\r\n+++\r\n\r\n## Overview\r\n\r\n1. Create new SaaS project\r\n2. Edit `.devcontainer/Dockerfile`\r\n3. Reopen the project in the Dev Container\r\n4. Delete frontend directory\r\n5. Generate new Angular frontend\r\n6. Build frontend\r\n7. Edit `config/development.yml`\r\n8. Start Loco\r\n\r\n## Create new SaaS project\r\n\r\n1. Run `loco new` to create a new project\r\n2. Navigate through the instructions until you reach the point where to decide what type of project to create\r\n3. Select \"SaaS app (with DB and user auth)\"\r\n\r\n## Edit \".devcontainer/Dockerfile\"\r\n\r\n1. Open `.devcontainer/Dockerfile`\r\n2. Replace the content with the following:\r\n\r\n```Dockerfile\r\nFROM mcr.microsoft.com/vscode/devcontainers/rust:0-1\r\n\r\n# Install postgresql-client and sea-orm-cli\r\nRUN apt-get update && export DEBIAN_FRONTEND=noninteractive \\\r\n    && apt-get -y install --no-install-recommends postgresql-client \\\r\n    && cargo install sea-orm-cli \\\r\n    && chown -R vscode /usr/local/cargo\r\n\r\n# Install Node.js and npm\r\nRUN curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - \\\r\n    && apt-get install -y nodejs\r\n# Install Angular CLI\r\nRUN npm install -g @angular/cli\r\n\r\nCOPY .env /.env\r\n```\r\n\r\nThe Dockerfile will provide you with everything you need to develop a Loco app with an Angular frontend.\r\n\r\n## Reopen the project in the Dev Container\r\n\r\nWith VSCode it is super easy to reopen and run the project in a Dev Container.\r\n\r\n1. Press `Crtl + Shift + P`\r\n2. Select `Dev Containers: Repopen in Container`\r\n3. VSCode will open the project in the dev container. This can take a while when it is built for the first time.\r\n4. Delete the existing `frontend` directory\r\n\r\nLoco comes with a Vite React frontend. We can delete the whole directory because the Angular CLI will set up everything we need\r\n\r\n## Generate new Angular frontend\r\n\r\n1. From the project root execute `ng new frontend` to create a new Angular project\r\n2. Navigate through the instructions\r\n\r\n## Build frontend\r\n\r\n1. Run `ng build` to build the Angular frontend\r\n\r\n## Edit \"config/development.yml\"\r\n\r\nAs you may have noticed Angular has built the frontend into `frontend/dist/frontend/browser`. We now need to configure Loco to access the built frontend from there.\r\n\r\n1. Open `config/development.yml`\r\n2. Set the configs to the frontend build path:\r\n\r\n   a. `server.middlewares.static.folder.path: \"frontend/dist/frontend/browser\"`\r\n\r\n   b. `server.middlewares.static.fallback: \"frontend/dist/frontend/browser/index.html\"`\r\n\r\n## Start Loco\r\n\r\n1. Start Loco with `cargo loco start`\r\n2. Open http://localhost:5150/\r\n\r\nYou should now see the Angular starter Website :smile:\r\n"
  },
  {
    "path": "docs-site/content/blog/axum-session.md",
    "content": "+++\ntitle = \"Building a Rust App with Axum Session\"\ndescription = \"Add sessions to your app with Axum Sessions. Configure a session provider, and set up Axum Session and Loco with simple app hooks.\"\ndate = 2023-12-19T09:19:42+00:00\nupdated = 2023-12-19T09:19:42+00:00\ndraft = false\ntemplate = \"blog/page.html\"\n\n[taxonomies]\nauthors = [\"Team Loco\"]\n\n+++\n\nTo build a Rust app with [Axum session](https://crates.io/crates/axum_session), the first step is to choose your server. In this case, we'll use [loco](https://loco.rs) :)\n\nStart by creating a new project and selecting the `SaaS app` template:\n\n```sh\n$ cargo install loco\n$ loco new\n✔ ❯ App name? · myapp\n? ❯ What would you like to build? ›\n  lightweight-service (minimal, only controllers and views)\n  Rest API (with DB and user auth)\n❯ SaaS app (with DB and user auth)\n```\n\n## Creating Session Memory Store Only\n\nFirst, add the Axum session crate to Cargo.toml:\n\n```toml\naxum_session = {version = \"0.10.1\", default-features = false}\n```\n\nThen, add an Axum session layer to your router. Open app.rs and add the following hook:\n\n```rust\npub struct App;\n#[async_trait]\nimpl Hooks for App {\n    fn app_name() -> &'static str {\n        env!(\"CARGO_CRATE_NAME\")\n    }\n\n    // Other hooks...\n    async fn after_routes(router: AxumRouter, _ctx: &AppContext) -> Result<AxumRouter> {\n        let session_config =\n            axum_session::SessionConfig::default().with_table_name(\"sessions_table\");\n\n        let session_store =\n            axum_session::SessionStore::<axum_session::SessionNullPool>::new(None, session_config)\n                .await\n                .unwrap();\n\n        let router = router.layer(axum_session::SessionLayer::new(session_store));\n        Ok(router)\n    }\n    // Other hooks...\n}\n\n```\n\nNow, you can create your controller that uses Axum session. Use the `cargo loco generate controller` command:\n\n```sh\n❯ cargo loco generate controller mysession --api\n    Finished dev [unoptimized + debuginfo] target(s) in 0.36s\n     Running `target/debug/axum-session-cli generate controller mysession`\nadded: \"src/controllers/mysession.rs\"\ninjected: \"src/controllers/mod.rs\"\ninjected: \"src/app.rs\"\nadded: \"tests/requests/mysession.rs\"\ninjected: \"tests/requests/mod.rs\"\n```\n\nOpen the `src/controllers/mysession.rs` file created by the controller generator and replace its content with the following code:\n\n```rust\n#![allow(clippy::unused_async)]\nuse axum_session::{Session, SessionNullPool};\nuse loco_rs::prelude::*;\n\npub async fn get_session(session: Session<SessionNullPool>) -> Result<()> {\n    println!(\"{:#?}\", session);\n    format::empty()\n}\n\npub fn routes() -> Routes {\n    Routes::new().prefix(\"mysession\").add(\"/\", get(get_session))\n}\n```\n\nNow, you can call the `http://127.0.0.1:5150/mysession` endpoint to see the session.\n\n## Creating Session With DB Encryption\n\nTo add session DB encryption, include the Axum session crate and PostgreSQL with SQLx in Cargo.toml:\n\n```toml\naxum_session = {version = \"0.10.1\"}\nsqlx = { version = \"0.7.2\", features = [\n  \"macros\",\n  \"postgres\",\n  \"_unstable-all-types\",\n  \"tls-rustls\",\n  \"runtime-tokio\",\n] }\n\n```\n\nCreate a `session.rs` file with the following content:\nThe `connect_to_database` getting an `Database` configuration and returns a PgPool instance that axum session expected.\n\n```rust\nuse sqlx::postgres::PgPool;\nuse loco_rs::{\n    config::Database,\n    errors::Error,\n    Result,\n};\n\nasync fn connect_to_database(config: &Database) -> Result<PgPool> {\n    PgPool::connect(&config.uri)\n        .await\n        .map_err(|e| Error::Any(e.into()))\n}\n\n```\n\nAdd the Axum session layer to your router in `app.rs`:\n\n```rust\nuse session; // This is the session.rs file\npub struct App;\n#[async_trait]\nimpl Hooks for App {\n    fn app_name() -> &'static str {\n        env!(\"CARGO_CRATE_NAME\")\n    }\n\n    // Other hooks...\n    async fn after_routes(router: AxumRouter, ctx: &AppContext) -> Result<AxumRouter> {\n        let conn = session.connect_to_database(&ctx.config.database).await?;\n        let session_config = axum_session::SessionConfig::default()\n            .with_table_name(\"sessions_table\")\n            .with_key(axum_session::Key::generate())\n            .with_database_key(axum_session::Key::generate())\n            .with_security_mode(axum_session::SecurityMode::PerSession);\n\n        let session_store = axum_session::SessionStore::<axum_session::SessionPgPool>::new(\n            Some(conn.clone().into()),\n            session_config,\n        )\n        .await\n        .unwrap();\n\n        let router = router.layer(axum_session::SessionLayer::new(session_store));\n        Ok(router)\n    }\n    // Other hooks...\n}\n\n```\n\nCreate the controller as before using `cargo loco generate controller`\n\n```sh\n❯ cargo loco generate controller mysession --api\n    Finished dev [unoptimized + debuginfo] target(s) in 0.36s\n     Running `target/debug/axum-session-cli generate controller mysession`\nadded: \"src/controllers/mysession.rs\"\ninjected: \"src/controllers/mod.rs\"\ninjected: \"src/app.rs\"\nadded: \"tests/requests/mysession.rs\"\ninjected: \"tests/requests/mod.rs\"\n```\n\nand replace the content of `src/controllers/mysession.rs` with the provided code.\n\n```rust\n#![allow(clippy::unused_async)]\nuse axum_session::{Session, SessionPgPool};\nuse loco_rs::prelude::*;\n\npub async fn get_session(session: Session<SessionPgPool>) -> Result<()> {\n    println!(\"{:#?}\", session);\n    format::empty()\n}\n\npub fn routes() -> Routes {\n    Routes::new().prefix(\"mysession\").add(\"/\", get(get_session))\n}\n\n```\n\nNow, calling the `http://127.0.0.1:5150/mysession` endpoint will display the session.\n"
  },
  {
    "path": "docs-site/content/blog/deploy-aws.md",
    "content": "+++\ntitle = \"Deploying Rust App with Terraform on AWS Fargate\"\ndescription = \"Learn how to deploy a Loco app with Terraform (IaC). Generate a deployment with Loco generators and set it up step-by-step.\"\ndate = 2023-12-20T16:04:40+00:00\nupdated = 2023-12-16T04:20:40+00:00\ndraft = false\ntemplate = \"blog/page.html\"\n\n[taxonomies]\nauthors = [\"Antonio Souza\"]\n\n+++\n\nIn today's rapidly evolving technological landscape, Infrastructure as Code (IaC) has become a cornerstone for efficient, scalable, and maintainable cloud infrastructure deployment. IaC involves managing and provisioning computing infrastructure through machine-readable script files, rather than through physical hardware configuration or interactive configuration tools. This allows for the automation of infrastructure deployment and management, which in turn reduces the risk of human error and increases the speed of deployment.\n\nIn this article, we will explore how to deploy a Rust app built with [loco](https://loco.rs) on AWS Fargate using Terraform. We will start by creating a new project and selecting the `Rest API` template:\n\n````sh\n\n```sh\n$ cargo install loco\n$ loco new\n✔ ❯ App name? · myapp\n? ❯ What would you like to build? ›\n  lightweight-service (minimal, only controllers and views)\n❯ Rest API (with DB and user auth)\n  SaaS app (with DB and user auth)\n````\n\n## Prerequisites\n\nTo deploy our app on AWS Fargate, we will need to have the following tools installed:\n\n- [Docker](https://docs.docker.com/get-docker/) - Docker is a containerization platform that allows you to package your application and all of its dependencies into a standardized unit for software development.\n- [Terraform](https://learn.hashicorp.com/tutorials/terraform/install-cli) - Terraform is an open-source infrastructure as code software tool that enables you to safely and predictably create, change, and improve infrastructure.\n- [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) - The AWS Command Line Interface (CLI) is a unified tool to manage your AWS services.\n\n## Creating the Docker Image\n\nTo create the Docker image for our app, we will use the loco CLI. The `cargo loco generate deployment` command will create a Docker image for our app. It will also create a `Dockerfile` for us, which we can use to build the image.\n\n```sh\n$ cargo loco generate deployment\n? ❯ Choose your deployment ›\n❯ Docker\n\nadded: \"Dockerfile\"\nadded: \".dockerignore\"\n```\n\nNow, we can build the Docker image which will be used to deploy our app on AWS Fargate.\n\n```sh\n$ docker build -t myapp .\n\n[+] Building 237.1s (16/16) FINISHED                                                                                                               docker:desktop-linux\n => [internal] load build definition from Dockerfile                                                                                                               0.0s\n => => transferring Dockerfile: 331B                                                                                                                               0.0s\n ...\n => => writing image sha256:07416ca8195e4026ab65bc567f990ea83141aa10890f8443deb8f54a8bae7f0a                                                                       0.0s\n => => naming to docker.io/library/myapp\n```\n\n## Setting up AWS\n\nTo deploy our app on AWS Fargate, we will need to create an AWS account and set up the AWS CLI. You can create an AWS account [here](https://portal.aws.amazon.com/billing/signup#/start/email).\n\nYou will also need to install the AWS CLI. You can find instructions on how to do this [here](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html).\n\nFinally, you need to create an IAM user to use with the AWS CLI. You can find instructions on how to do this [here](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_users_create.html).\n\nNow, we can configure the AWS CLI with the credentials of the IAM user we just created.\n\n```sh\n$ aws configure\nAWS Access Key ID [None]: <your access key id>\nAWS Secret Access Key [None]: <your secret access key>\nDefault region name [None]: <your region>\nDefault output format [None]: json\n```\n\n## Creating the repository on ECR\n\nTo deploy our app on AWS Fargate, we will need to create a repository on ECR. You can do this by running the following command:\n\n```sh\n$ aws ecr create-repository --repository-name myapp\n\n{\n    \"repository\": {\n        \"repositoryArn\": \"arn:aws:ecr:us-east-1:123456789012:repository/myapp\",\n        \"registryId\": \"123456789012\",\n        \"repositoryName\": \"myapp\",\n        \"repositoryUri\": \"123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp\",\n        \"createdAt\": 1627981234.0,\n        \"imageTagMutability\": \"MUTABLE\",\n        \"imageScanningConfiguration\": {\n            \"scanOnPush\": false\n        }\n    }\n}\n```\n\n## Pushing the Docker image to ECR\n\nNow, we can push the Docker image to ECR. You can do this by running the following commands:\n\n-1. Log in to ECR\n\n```sh\n$ aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin 123456789012.dkr.ecr.us-east-1.amazonaws.com\n```\n\n-2. Tag the Docker image\n\n```sh\n$ docker tag myapp:latest 123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp:latest\n```\n\n-3. Push the Docker image to ECR\n\n```sh\n$ docker push 123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp:latest\n```\n\n## Creating the main.tf file for Terraform\n\nThis is the main Terraform file that will be used to deploy our app on AWS Fargate. It will create the following resources:\n\n```hcl\nterraform {\n  required_providers {\n    aws = {\n      source = \"hashicorp/aws\"\n      version = \"~> 4.0\"\n    }\n    archive = {\n      source = \"hashicorp/archive\"\n      version = \"~> 2.2.0\"\n    }\n  }\n\n  required_version = \"~> 1.0\"\n}\n\n# Configure the AWS Provider\nprovider \"aws\" {\n  region = \"us-east-1\" // Change this to your region\n  access_key = \"<your access key>\" // Change this to your access key\n  secret_key = \"your secret key\" // Change this to your secret key\n}\n\nresource \"aws_ecr_repository\" \"myapp\" {\n  name = \"myapp\"\n}\n\nresource \"aws_ecs_cluster\" \"myapp_cluster\" {\n  name = \"myapp_cluster\"\n}\n\nresource \"aws_cloudwatch_log_group\" \"myapp\" {\n  name = \"/ecs/myapp\"\n}\n\nresource \"aws_ecs_task_definition\" \"myapp_task\" {\n  family                   = \"myapp-task\"\n  container_definitions    = <<DEFINITION\n  [\n    {\n      \"name\": \"myapp-task\",\n      \"image\": \"${aws_ecr_repository.myapp.repository_url}\",\n      \"essential\": true,\n      \"portMappings\": [\n        {\n          \"containerPort\": 5150\n        }\n      ],\n      \"command\": [\"start\"],\n      \"memory\": 512,\n      \"cpu\": 256,\n      \"logConfiguration\": {\n        \"logDriver\": \"awslogs\",\n        \"options\": {\n          \"awslogs-region\": \"us-east-2\",\n          \"awslogs-group\": \"/ecs/myapp\",\n          \"awslogs-stream-prefix\": \"ecs\"\n        }\n      }\n    }\n  ]\n  DEFINITION\n  requires_compatibilities = [\"FARGATE\"]\n  network_mode             = \"awsvpc\"\n  memory                   = 512\n  cpu                      = 256\n  execution_role_arn       = aws_iam_role.ecsTaskExecutionRole.arn\n}\n\nresource \"aws_iam_role\" \"ecsTaskExecutionRole\" {\n  name               = \"ecsTaskExecutionRoleMyapp\"\n  assume_role_policy = data.aws_iam_policy_document.assume_role_policy.json\n}\n\ndata \"aws_iam_policy_document\" \"assume_role_policy\" {\n  statement {\n    actions = [\"sts:AssumeRole\"]\n\n    principals {\n      type        = \"Service\"\n      identifiers = [\"ecs-tasks.amazonaws.com\"]\n    }\n  }\n}\n\nresource \"aws_iam_role_policy_attachment\" \"ecsTaskExecutionRole_policy\" {\n  role       = aws_iam_role.ecsTaskExecutionRole.name\n  policy_arn = \"arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy\"\n}\n\nresource \"aws_alb\" \"myapp\" {\n  name               = \"myapp-lb\"\n  internal           = false\n  load_balancer_type = \"application\"\n  enable_deletion_protection = true\n\n  subnets = [\n    aws_subnet.public_d.id,\n    aws_subnet.public_e.id,\n  ]\n\n  security_groups = [\n    aws_security_group.http.id,\n    aws_security_group.https.id,\n    aws_security_group.egress_all.id,\n  ]\n\n  depends_on = [aws_internet_gateway.igw]\n}\n\n\nresource \"aws_security_group\" \"load_balancer_security_group\" {\n  ingress {\n    from_port   = 80\n    to_port     = 80\n    protocol    = \"tcp\"\n    cidr_blocks = [\"0.0.0.0/0\"]\n  }\n  egress {\n    from_port   = 0\n    to_port     = 0\n    protocol    = \"-1\"\n    cidr_blocks = [\"0.0.0.0/0\"]\n  }\n}\nresource \"aws_lb_target_group\" \"myapp\" {\n  name        = \"myapp-tg\"\n  port        = 5150\n  protocol    = \"HTTP\"\n  target_type = \"ip\"\n  vpc_id      = aws_vpc.myapp_vpc.id\n\n  health_check {\n    enabled = true\n    path    = \"/_health\"\n    matcher = \"200,202\"\n  }\n\n  depends_on = [aws_alb.myapp]\n}\n\nresource \"aws_alb_listener\" \"myapp_http\" {\n  load_balancer_arn = aws_alb.myapp.arn\n  port              = \"80\"\n  protocol          = \"HTTP\"\n\n  default_action {\n    type =  \"redirect\"\n    redirect {\n      port        = \"443\"\n      protocol    = \"HTTPS\"\n      status_code = \"HTTP_301\"\n    }\n  }\n}\n\nresource \"aws_alb_listener\" \"myapp_https\" {\n  load_balancer_arn = aws_alb.myapp.arn\n  port              = \"443\"\n  protocol          = \"HTTPS\"\n  ssl_policy        = \"ELBSecurityPolicy-2016-08\"\n\n  certificate_arn = \"<your arn for the certificate>\" // Change this to your certificate ARN\n\n  default_action {\n    type             = \"forward\"\n    target_group_arn = aws_lb_target_group.myapp.arn\n  }\n}\n\noutput \"alb_url\" {\n  value = \"https://${aws_alb.myapp.dns_name}\"\n}\nresource \"aws_ecs_service\" \"myapp\" {\n  name            = \"myapp-service\"\n  cluster         = aws_ecs_cluster.myapp_cluster.id\n  task_definition = aws_ecs_task_definition.myapp_task.arn\n  launch_type     = \"FARGATE\"\n  desired_count   = 1\n\n  load_balancer {\n    target_group_arn = aws_lb_target_group.myapp.arn\n    container_name   = aws_ecs_task_definition.myapp_task.family\n    container_port   = 5150\n  }\n\n  network_configuration {\n    assign_public_ip = false\n\n    security_groups = [\n      aws_security_group.egress_all.id,\n      aws_security_group.ingress_api.id,\n    ]\n\n    subnets = [\n    aws_subnet.private_d.id,\n    aws_subnet.private_e.id,\n    ]\n  }\n}\n\n\nresource \"aws_security_group\" \"service_security_group\" {\n  ingress {\n    from_port       = 0\n    to_port         = 0\n    protocol        = \"-1\"\n    security_groups = [\"${aws_security_group.load_balancer_security_group.id}\"]\n  }\n\n  egress {\n    from_port   = 0\n    to_port     = 0\n    protocol    = \"-1\"\n    cidr_blocks = [\"0.0.0.0/0\"]\n  }\n}\n```\n\nThis file will create the following resources:\n\n- An ECR repository for our app\n- An ECS cluster for our app\n- An ECS task definition for our app\n- An ECS service for our app\n\nNow, we need to create a `network.tf` file to define the network configuration for our app. This file will create the following resources:\n\n```hcl\nresource \"aws_vpc\" \"myapp_vpc\" {\n  cidr_block = \"10.0.0.0/16\"\n}\n\nresource \"aws_subnet\" \"public_d\" {\n  vpc_id            = aws_vpc.myapp_vpc.id\n  cidr_block        = \"10.0.1.0/25\"\n  availability_zone = \"us-east-2a\"\n\n  tags = {\n    \"Name\" = \"public | us-east-2a\"\n  }\n}\n\nresource \"aws_subnet\" \"private_d\" {\n  vpc_id            = aws_vpc.myapp_vpc.id\n  cidr_block        = \"10.0.2.0/25\"\n  availability_zone = \"us-east-2b\"\n\n  tags = {\n    \"Name\" = \"private | us-east-2b\"\n  }\n}\n\nresource \"aws_subnet\" \"public_e\" {\n  vpc_id            = aws_vpc.myapp_vpc.id\n  cidr_block        = \"10.0.1.128/25\"\n  availability_zone = \"us-east-2c\"\n\n  tags = {\n    \"Name\" = \"public | us-east-2c\"\n  }\n}\n\nresource \"aws_subnet\" \"private_e\" {\n  vpc_id            = aws_vpc.myapp_vpc.id\n  cidr_block        = \"10.0.2.128/25\"\n  availability_zone = \"us-east-2c\"\n\n  tags = {\n    \"Name\" = \"private | us-east-2c\"\n  }\n}\n\nresource \"aws_route_table\" \"public\" {\n  vpc_id = aws_vpc.myapp_vpc.id\n  tags = {\n    \"Name\" = \"public\"\n  }\n}\n\nresource \"aws_route_table\" \"private\" {\n  vpc_id = aws_vpc.myapp_vpc.id\n  tags = {\n    \"Name\" = \"private\"\n  }\n}\n\nresource \"aws_route_table_association\" \"public_d_subnet\" {\n  subnet_id      = aws_subnet.public_d.id\n  route_table_id = aws_route_table.public.id\n}\n\nresource \"aws_route_table_association\" \"private_d_subnet\" {\n  subnet_id      = aws_subnet.private_d.id\n  route_table_id = aws_route_table.private.id\n}\n\nresource \"aws_route_table_association\" \"public_e_subnet\" {\n  subnet_id      = aws_subnet.public_e.id\n  route_table_id = aws_route_table.public.id\n}\n\nresource \"aws_route_table_association\" \"private_e_subnet\" {\n  subnet_id      = aws_subnet.private_e.id\n  route_table_id = aws_route_table.private.id\n}\n\nresource \"aws_eip\" \"nat\" {\n  vpc = true\n}\n\nresource \"aws_internet_gateway\" \"igw\" {\n  vpc_id = aws_vpc.myapp_vpc.id\n}\n\nresource \"aws_nat_gateway\" \"ngw\" {\n  subnet_id     = aws_subnet.public_d.id\n  allocation_id = aws_eip.nat.id\n\n  depends_on = [aws_internet_gateway.igw]\n}\n\nresource \"aws_route\" \"public_igw\" {\n  route_table_id         = aws_route_table.public.id\n  destination_cidr_block = \"0.0.0.0/0\"\n  gateway_id             = aws_internet_gateway.igw.id\n}\n\nresource \"aws_route\" \"private_ngw\" {\n  route_table_id         = aws_route_table.private.id\n  destination_cidr_block = \"0.0.0.0/0\"\n  nat_gateway_id         = aws_nat_gateway.ngw.id\n}\n\nresource \"aws_security_group\" \"http\" {\n  name        = \"http\"\n  description = \"HTTP traffic\"\n  vpc_id      = aws_vpc.myapp_vpc.id\n\n  ingress {\n    from_port   = 80\n    to_port     = 80\n    protocol    = \"TCP\"\n    cidr_blocks = [\"0.0.0.0/0\"]\n  }\n}\n\nresource \"aws_security_group\" \"https\" {\n  name        = \"https\"\n  description = \"HTTPS traffic\"\n  vpc_id      = aws_vpc.myapp_vpc.id\n\n  ingress {\n    from_port   = 443\n    to_port     = 443\n    protocol    = \"TCP\"\n    cidr_blocks = [\"0.0.0.0/0\"]\n  }\n}\n\nresource \"aws_security_group\" \"egress_all\" {\n  name        = \"egress-all\"\n  description = \"Allow outbound traffic\"\n  vpc_id      = aws_vpc.myapp_vpc.id\n\n  egress {\n    from_port   = 0\n    to_port     = 0\n    protocol    = \"-1\"\n    cidr_blocks = [\"0.0.0.0/0\"]\n  }\n}\n\nresource \"aws_security_group\" \"ingress_api\" {\n  name        = \"ingress-api\"\n  description = \"Allow ingress to App\"\n  vpc_id      = aws_vpc.myapp_vpc.id\n\n  ingress {\n    from_port   = 5150\n    to_port     = 5150\n    protocol    = \"TCP\"\n    cidr_blocks = [\"0.0.0.0/0\"]\n  }\n}\n```\n\nThe network configuration will be responsible for creating all the infrastructure needed to deploy our app on AWS Fargate in terms of networking. I recommend you to read the [AWS Fargate documentation](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/AWS_Fargate.html) to understand how it works, also you can read the Terraform documentation for [AWS Fargate](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_task_definition) and [AWS VPC](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc).\n\nSo, now we have the main Terraform file and the network configuration file for our app. We can now deploy our app on AWS Fargate.\n\n## Deploying the app on AWS Fargate\n\nTo deploy our app on AWS Fargate, we will need to run the following commands:\n\n-1. Initialize Terraform\n\n```sh\n$ terraform init\n```\n\n-2. Plan the deployment\n\n```sh\n$ terraform plan\n```\n\n-3. Apply the deployment\n\n````sh\n$ terraform apply\n```****\n\nTheses commands will create all the resources we need to deploy our app on AWS Fargate. After running you will see the url from our alb_url output.\n\n```sh\nApply complete! Resources: 20 added, 0 changed, 0 destroyed.\n\nOutputs:\n\nalb_url = https://myapp-lb-1234567890.us-east-2.elb.amazonaws.com\n````\n\nNow, we can access our app by going to the url from our alb_url output.\n\n## Conclusion\n\nIn this article, we explored how to deploy a Rust app built with loco on AWS Fargate using Terraform. We started by creating a new project and selecting the `Rest API` template. Then, we created the Docker image for our app and pushed it to ECR. Finally, we created the main Terraform file and the network configuration file for our app and deployed it on AWS Fargate.\n\nThis approach allows us to deploy our app on AWS Fargate in a fast and reliable way. It also allows us to easily scale our app by adding more instances of it.\n"
  },
  {
    "path": "docs-site/content/blog/frontend-website.md",
    "content": "+++\ntitle = \"Creating Frontend Website\"\ndescription = \"Build a REST API quickly with Loco and then follow by building a React frontend app to use it. Learn about generators, configuring asset serving and client-side apps with Loco.\"\ndate = 2023-12-14T09:19:42+00:00\nupdated = 2023-12-14T09:19:42+00:00\ndraft = false\ntemplate = \"blog/page.html\"\n\n[taxonomies]\nauthors = [\"Team Loco\"]\n\n+++\n\n## Overview\n\nThis guide provides a comprehensive walkthrough on using `Loco` to build a Todo list application with a REST API and a React frontend. The steps outlined cover everything from project creation to deployment.\n\nExplore the example repository [here](https://github.com/loco-rs/todo-list-example)\n\nThe key steps include:\n\n- Creating a Loco project with the SaaS starter\n- Setting up a Vite frontend with React\n- Configuring Loco to serve frontend static assets\n- Implementing the Notes model/controller in the REST API\n- Reloading the server and frontend during development\n- Deploying the website to production\n\n## Selecting SaaS Starter\n\nTo begin, run the following command to create a new Loco app using the SaaS starter:\n\n```sh\n& loco new\n✔ ❯ App name? · todolist\n✔ ❯ What would you like to build? · SaaS app (with DB and user auth)\n\n🚂 Loco app generated successfully in:\n/tmp/todolist\n```\n\nFollow the prompts to specify the app name (e.g., todolist) and choose the SaaS app option.\n\nAfter generating the app, ensure you have the necessary resources by running:\n\n```\n$ cd todolist\n$ cargo loco doctor\n✅ SeaORM CLI is installed\n✅ DB connection: success\n✅ Redis connection: success\n```\n\nVerify that SeaORM CLI is installed, and the database and Redis connections are successful. If any resources fail, refer to the [quick tour guide](@/docs/getting-started/guide.md) for troubleshooting.\n\nOnce `cargo loco doctor` shows all checks passed, start the server:\n\n```\n$ cargo loco start\n   Updating crates.io index\n   .\n   .\n   .\n\n                      ▄     ▀\n                                 ▀  ▄\n                  ▄       ▀     ▄  ▄ ▄▀\n                                    ▄ ▀▄▄\n                        ▄     ▀    ▀  ▀▄▀█▄\n                                          ▀█▄\n▄▄▄▄▄▄▄  ▄▄▄▄▄▄▄▄▄   ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄ ▀▀█\n ██████  █████   ███ █████   ███ █████   ███ ▀█\n ██████  █████   ███ █████   ▀▀▀ █████   ███ ▄█▄\n ██████  █████   ███ █████       █████   ███ ████▄\n ██████  █████   ███ █████   ▄▄▄ █████   ███ █████\n ██████  █████   ███  ████   ███ █████   ███ ████▀\n   ▀▀▀██▄ ▀▀▀▀▀▀▀▀▀▀  ▀▀▀▀▀▀▀▀▀▀  ▀▀▀▀▀▀▀▀▀▀ ██▀\n       ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀\n                https://loco.rs\n\nenvironment: development\n   database: automigrate\n     logger: debug\n      modes: server\n\nlistening on port 5150\n```\n\n## Creating the Frontend\n\nFor the frontend, we'll use [Vite](https://vitejs.dev/guide/) with React. In the `todolist` folder, run:\n\n```sh\n$ npm create vite@latest\nNeed to install the following packages:\n  create-vite@5.1.0\nOk to proceed? (y) y\n✔ Project name: … frontend\n✔ Select a framework: › React\n✔ Select a variant: › JavaScript\n```\n\nFollow the prompts to set up the `frontend` as a project name.\n\nNavigate to the frontend folder and install dependencies:\n\n```\n$ cd todolist/frontend\n$ pnpm install\n```\n\nStart the development server:\n\n```sh\n$ pnpm dev\n```\n\n### Serving Static Assets in Loco\n\nFirst, move all our rest api endpoint under `/api` prefix. for doing it go to `src/app.rs`. in `routes` hooks function add `.prefix(\"/api\")` to the default routes.\n```rust\nfn routes() -> AppRoutes {\n    AppRoutes::with_default_routes()\n        .prefix(\"/api\")\n        .add_route(controllers::notes::routes())\n}\n```\n\nBuild the frontend for production:\n\n```sh\npnpm build\n```\n\nIn the `frontend` folder, a `dist` directory is created. Update the `config/development.yaml` file in the main folder to include a static middleware:\n\n```yaml\nserver:\n  middlewares:\n    static:\n      enable: true\n      must_exist: true\n      folder:\n        uri: \"/\"\n        path: \"frontend/dist\"\n      fallback: \"frontend/dist/index.html\"\n```\n\nNow, run the Loco server again and you should see frontend app serving via Loco\n```sh\n$ cargo loco start\n```\n\nIf you see the default fallback page, you have to disable the fallback middleware. The default fallback takes priority over the static handler, so no static content will be served if it is enabled. You can disable it like so:\n\n```yaml\nserver:\n  middlewares:\n    fallback:\n      enable: false\n    static:\n      ...\n```\n\n# Developing the UI\n\nInstall `react-router-dom`, `react-query` and `axios`\n\n```sh\n$ pnpm install react-router-dom react-query axios\n```\n\n1. Copy [main.jsx](https://github.com/loco-rs/todo-list-example/blob/main/frontend/src/main.jsx) to frontend/src/main.jsx.\n2. Copy [App.jsx](https://github.com/loco-rs/todo-list-example/blob/main/frontend/src/App.jsx) to frontend/src/App.jsx.\n3. Copy [App.css](https://github.com/loco-rs/todo-list-example/blob/main/frontend/src/App.css) to frontend/src/App.css.\n\nNow, run the server `cargo loco start` and the UI pnpm dev in the frontend folder, and start adding your todo list!\n\n## Improve Development\n\nuse [cargo-watch](https://crates.io/crates/cargo-watch) for hot reloading the server:\n\n```sh\n$ cargo watch --ignore \"frontend\" -x check -s 'cargo run start'\n```\n\nNow, any changes in your Rust code will automatically reload the server, and any changes in your frontend Vite will reload the frontend app.\n\n## Deploy To Production\n\nIn the `frontend` folder, run `pnpm build`. After a successful build, go to the Loco server and run `cargo loco start`. Loco will serve the frontend static files directly from the server.\n\n### Prepare Docker Image\n\nRun `cargo loco generate deployment` and select Docker as the deployment type:\n\n```sh\n$ cargo loco generate deployment\n✔ ❯ Choose your deployment · Docker\nadded: \"Dockerfile\"\nadded: \".dockerignore\"\n```\n\nLoco will add a `Dockerfile` and a `.dockerignore `file. Note that Loco detect the static assent and included them as part of the image\n\nBuild the container:\n\n```sh\n$ docker build . -t loco-todo-list\n```\n\nNow run the container:\n\n```sh\n$ docker run -e LOCO_ENV=production -p 5150:5150 loco-todo-list start\n```\n"
  },
  {
    "path": "docs-site/content/blog/hello-world.md",
    "content": "+++\ntitle = \"What if Rails was Built on Rust?\"\ndescription = \"Introducing Loco: a Rails-inspired Rust web framework. See how Rust can be as expressive as Ruby and how we can build a good deal of magic that Rails has with Rust.\"\ndate = 2023-11-24T09:19:42+00:00\nupdated = 2023-11-24T09:19:42+00:00\ndraft = false\ntemplate = \"blog/page.html\"\n\n[taxonomies]\nauthors = [\"Team Loco\"]\n\n+++\n\n<center>\n<img width=\"150\" src=\"/icon.svg\"/> \n\n\n**What if [Rails](https://rubyonrails.org) was built on Rust and not Ruby?**\n</center>\n\n\n\nThen it would look like this:\n\n```rust\nasync fn current(\n    auth: middleware::auth::Auth,\n    State(ctx): State<AppContext>,\n) -> Result<Response> {\n    let user = users::Model::find_by_pid(&ctx.db, &auth.claims.pid).await?;\n    format::json(CurrentResponse::new(&user))\n}\n\npub fn routes() -> Routes {\n    Routes::new().prefix(\"user\").add(\"/current\", get(current))\n}\n\n```\n\n## Introducing: Loco\n\nLoco is a Rails inspired web framework for Rust. It inlcudes _almost every Rails feature_ with best-effort Rust ergonomics:\n\n* Controllers and routing via [axum](https://github.com/tokio-rs/axum)\n* Models, migration, and ActiveRecord via [SeaORM](https://www.sea-ql.org/SeaORM/)\n* Views via [serde](https://serde.rs/json.html)\n* Seamless, Background jobs, multi modal: in process, out of process, async via Tokio\n* Mailers\n* Tasks\n* Seeding\n* Environment-aware configuration\n* Tracing, logging, seamlessly integrated via [tracing](https://docs.rs/tracing)\n* Generators via [rrgen](https://github.com/jondot/rrgen)\n* Batteries-included authentication (like Rails' `devise`)\n* Testing kit, with automatic truncation, fixture seeding, auto migration, snapshotting and redaction\n\nIt's full stack for real.\n\n## Why not Rails?\n\nIf you're happy with Ruby, use Rails. Don't spend time looking elsewhere because of performance -- Rails and Ruby are good enough.\n\n**But if you love Rust**, you can now build companies like Rubyists have been building for ages -- use Loco.\n\n* You'll get **Rust's safety, strong typing, fantastic concurrency models, and super super stable libraries and ecosystem**. Build once, then forget about it.\n* Deployment is copying a **single binary** over to a server.\n* You'll be getting **an order of 100,000 requests/sec** without any effort. And 50k requests/sec with database calls. You will never need more than a couple servers. Heck, you can deploy on a Rasberry Pi and be happy..\n\n## The One Person Framework\n\nInspired by [DHH's approach](https://world.hey.com/dhh/the-one-person-framework-711e6318), Loco's guiding principle is above all:\n\n> The one person framework\n\nFrom this single guiding principles comes everything else.\n\nFor example, one person team, or one person company:\n\n\n* Has **no time to debate libraries**, tooling, linting rules: strong opinions are welcome. Tell me how I should work.\n* **Needs a driving tool** in addition to their brainpower -- that's the Loco CLI. Generate code, operate your project.\n* **Needs stability**, anything that breaks is a waste of time, any surprise is a waste of time\n* **Needs simplicity** -- don't surprise me\n* **Needs a single operability story**. Deploys should be simple. No Kubernetes, no IAC, no preconditions.\n* **Needs control**. Send emails and author the emails locally, not on some remote service\n* **Needs locality**. Everything that happens in production should first happen in development and locally\n* **Needs ad-hocness**. No holy grail ceremonies. Build tasks to run birthday emails to your users, rather than go on a crusade for an \"Admin\" project.\n\nLoco is the one person framework for **indy hackers, hobbyists, and startups**.\n\nWith around **20mb of a deploy binary, and 50k requests/sec** - all you need is a single small/medium server, Postgres or Sqlite and an internet connection. Startups should be cheap!\n\n\nGet started with [Loco](https://loco.rs) today!\n"
  },
  {
    "path": "docs-site/content/casts/001-dynamic-responses-and-content-types.md",
    "content": "+++\ntitle = \"Dynamic responses and content types\"\ndescription = \"Learn how to respond to incoming requests with the appropriate content type. Match on the incoming format, and render JSON, HTML or other types of responses.\"\ndate = 2024-06-10T09:19:42+00:00\nupdated = 2024-06-10T09:19:42+00:00\ndraft = false\ntemplate = \"casts/page.html\"\n\n[taxonomies]\nauthors = [\"Team Loco\"]\n\n[extra]\nnum = \"001\"\nid = \"l_hxXsHHSSU\"\n\n+++\n\nReference material for this episode:\n\n* Loco.rs docs: [sending responses](https://loco.rs/docs/the-app/controller/#sending-responses)\n* Rails [responders](https://api.rubyonrails.org/v4.1/classes/ActionController/Responder.html)\n\n"
  },
  {
    "path": "docs-site/content/casts/002-routes-and-prefixes.md",
    "content": "+++\ntitle = \"Routes and prefixes\"\ndescription = \"Routes in Loco are derived from how Axum does Routes. Learn how to shape your API and draw your routes, from an individual controller to your global app route set up.\"\ndate = 2024-06-13T09:19:42+00:00\nupdated = 2024-06-13T09:19:42+00:00\ndraft = false\ntemplate = \"casts/page.html\"\n\n[taxonomies]\nauthors = [\"Team Loco\"]\n\n[extra]\nnum = \"002\"\nid = \"IGPE0_ptaHY\"\n\n+++\n\nReference material for this episode:\n\n* Loco.rs docs: [routes in controllers](https://loco.rs/docs/the-app/controller/#routes-in-controllers)\n* The [SaaS starter](https://loco.rs/docs/starters/saas/)\n\n"
  },
  {
    "path": "docs-site/content/casts/003-scaffolding-crud-with-html.md",
    "content": "+++\ntitle = \"Scaffolding full CRUD with HTML views\"\ndescription = \"Loco generators are very powerful. Generate a full CRUD app with a single command, by including the main entity type and its set of fields. Loco will generate models, controllers, views, and migrations for you.\"\ndate = 2024-06-14T09:19:42+00:00\nupdated = 2024-06-14T09:19:42+00:00\ndraft = false\ntemplate = \"casts/page.html\"\n\n[taxonomies]\nauthors = [\"Team Loco\"]\n\n[extra]\nnum = \"003\"\nid = \"EircfwF8c0E\"\n\n+++\n\nReference material for this episode:\n\n* Loco.rs docs: [routes in controllers](https://loco.rs/docs/the-app/controller/#routes-in-controllers)\n* The [SaaS starter](https://loco.rs/docs/starters/saas/)\n* The [REST API starter](https://loco.rs/docs/starters/rest-api/)\n"
  },
  {
    "path": "docs-site/content/casts/004-creating-tasks.md",
    "content": "+++\ntitle = \"Creating tasks\"\ndescription = \"Ever reached out to write a small script to reset a user password? send email notifications to your users? You can use Tasks in Loco to write these operational bits in pure Rust and access your full app from your task.\"\ndate = 2024-06-18T09:19:42+00:00\nupdated = 2024-06-18T09:19:42+00:00\ndraft = false\ntemplate = \"casts/page.html\"\n\n[taxonomies]\nauthors = [\"Team Loco\"]\n\n[extra]\nnum = \"004\"\nid = \"gn7Hkq7T9dI\"\n\n+++\n\nReference material for this episode:\n\n* Loco.rs docs: [routes in controllers](https://loco.rs/docs/the-app/task/)\n* The [SaaS starter](https://loco.rs/docs/starters/saas/)\n* The [REST API starter](https://loco.rs/docs/starters/rest-api/)\n* The [Lightweight starter](https://loco.rs/docs/starters/service/)\n"
  },
  {
    "path": "docs-site/content/casts/005-testing-tasks.md",
    "content": "+++\ntitle = \"Testing tasks\"\ndescription = \"See how tasks in Loco are a simple linear workflow with access to your full app context, and how to easily test them. You can write a linear business workflow and test it giving it input and asserting its output.\"\ndate = 2024-06-18T09:20:42+00:00\nupdated = 2024-06-18T09:20:42+00:00\ndraft = false\ntemplate = \"casts/page.html\"\n\n[taxonomies]\nauthors = [\"Team Loco\"]\n\n[extra]\nnum = \"005\"\nid = \"485JlLA-T6U\"\n\n+++\n\nReference material for this episode:\n\n* Loco.rs docs: [routes in controllers](https://loco.rs/docs/processing/task/)\n* The [SaaS starter](https://loco.rs/docs/getting-started/starters/#saas-starter)\n* The [REST API starter](https://loco.rs/docs/getting-started/starters/#rest-api-starter)\n* The [Lightweight starter](https://loco.rs/docs/getting-started/starters/#lightweight-service-starter)\n"
  },
  {
    "path": "docs-site/content/casts/006-mailers.md",
    "content": "+++\ntitle = \"Mailers\"\ndescription = \"Learn how to send emails from your app. As it turns out, emails are still an important core feature in business apps. You can send emails from multiple types of providers, and enjoy a great developer experience.\"\ndate = 2024-06-27T09:20:42+00:00\nupdated = 2024-06-27T09:20:42+00:00\ndraft = false\ntemplate = \"casts/page.html\"\n\n[taxonomies]\nauthors = [\"Team Loco\"]\n\n[extra]\nnum = \"006\"\nid = \"ieGeihxLGC8\"\n+++\n\nReference material for this episode:\n\n* Loco.rs docs: [Mailers](https://loco.rs/docs/processing/mailers/)\n"
  },
  {
    "path": "docs-site/content/casts/007-htmx.md",
    "content": "+++\ntitle = \"Full CRUD with HTMX scaffold generator\"\ndescription = \"Learn how to use HTMX with Loco. Using Loco's core generator scaffolding abilities, we added support for generating a set of HTMX powered views, which makes it a joy to build a fullstack UI app.\"\ndate = 2024-06-27T14:20:42+00:00\nupdated = 2024-06-27T14:20:42+00:00\ndraft = false\ntemplate = \"casts/page.html\"\n\n[taxonomies]\nauthors = [\"Team Loco\"]\n\n[extra]\nnum = \"007\"\nid = \"OWUvUSC1KvY\"\n+++\n\nReference material for this episode:\n\n* Loco.rs docs: [Views](https://loco.rs/docs/the-app/views/)\n* HTMX [website](https://htmx.org/)\n"
  },
  {
    "path": "docs-site/content/casts/_index.md",
    "content": "+++\ntitle = \"Loco Casts\"\ndescription =  \"Loco Casts\"\nsort_by = \"date\"\npaginate_by = 10\ntemplate = \"casts/section.html\"\n+++\n"
  },
  {
    "path": "docs-site/content/docs/_index.md",
    "content": "+++\ntitle = \"Docs\"\ndescription = \"Docs for loco\"\nsort_by = \"weight\"\nweight = 1\ntemplate = \"docs/section.html\"\n+++\n"
  },
  {
    "path": "docs-site/content/docs/extras/_index.md",
    "content": "+++\ntitle = \"Extras\"\ndescription = \"\"\ntemplate = \"docs/section.html\"\nsort_by = \"weight\"\nweight = 5\ndraft = false\n+++\n"
  },
  {
    "path": "docs-site/content/docs/extras/authentication.md",
    "content": "+++\ntitle = \"Authentication\"\ndescription = \"\"\ndate = 2021-05-01T18:20:00+00:00\nupdated = 2021-05-01T18:20:00+00:00\ndraft = false\nweight = 1\nsort_by = \"weight\"\ntemplate = \"docs/page.html\"\n\n[extra]\nlead = \"\"\ntoc = true\ntop = false\nflair =[]\n+++\n\n## User Password Authentication\n\n`Loco` simplifies the user authentication process, allowing you to set up a new website quickly. This feature not only saves time but also provides the flexibility to focus on crafting the core logic of your application.\n\n### Authentication Configuration\n\nThe `auth` feature comes as a default with the library. If desired, you can turn it off and handle authentication manually.\n\n### Getting Started with a SaaS App\n\nCreate your app using the [loco cli](/docs/getting-started/tour) and select the `SaaS app (with DB and user auth)` option.\n\nTo explore the out-of-the-box auth controllers, run the following command:\n\n```sh\n$ cargo loco routes\n .\n .\n .\n[POST] /api/auth/forgot\n[POST] /api/auth/login\n[POST] /api/auth/register\n[POST] /api/auth/reset\n[GET] /api/auth/verify\n[GET] /api/auth/current\n .\n .\n .\n```\n\n### Registering a New User\n\nThe `/api/auth/register` endpoint creates a new user in the database with an `email_verification_token` for account verification. A welcome email is sent to the user with a verification link.\n\n##### Example Curl Request:\n\n```sh\ncurl --location '127.0.0.1:5150/api/auth/register' \\\n     --header 'Content-Type: application/json' \\\n     --data-raw '{\n         \"name\": \"Loco user\",\n         \"email\": \"user@loco.rs\",\n         \"password\": \"12341234\"\n     }'\n```\n\nFor security reasons, if the user is already registered, no new user is created, and a 200 status is returned without exposing user email details.\n\n### Login\n\nAfter registering a new user, use the following request to log in:\n\n##### Example Curl Request:\n\n```sh\ncurl --location '127.0.0.1:5150/api/auth/login' \\\n     --header 'Content-Type: application/json' \\\n     --data-raw '{\n         \"email\": \"user@loco.rs\",\n         \"password\": \"12341234\"\n     }'\n```\n\nThe response includes a JWT token for authentication, user ID, name, and verification status.\n\n```sh\n{\n    \"token\": \"...\",\n    \"pid\": \"2b20f998-b11e-4aeb-96d7-beca7671abda\",\n    \"name\": \"Loco user\",\n    \"is_verified\": false\n}\n```\n\n- **Token**: A JWT token enabling requests to authentication endpoints. Refer to the [configuration documentation](@/docs/the-app/your-project.md#your-app-configuration) to customize the default token expiration and ensure that the secret differs between environments.\n- **pid** - A unique identifier generated when creating a new user.\n- **Name** - The user's name associated with the account.\n- **Is Verified** - A flag indicating whether the user has verified their account.\n\n### Account Verification\n\nUpon user registration, an email with a verification link is sent. Visiting this link updates the `email_verified_at` field in the database, changing the `is_verified` flag in the login response to true.\n\n#### Example Curl request:\n\n```sh\ncurl --location --request GET '127.0.0.1:5150/api/auth/verify/TOKEN' \\\n     --header 'Content-Type: application/json'\n```\n\n### Reset Password Flow\n\n#### Forgot Password\n\nThe `forgot` endpoint requires only the user's email in the payload. An email is sent with a reset password link, and a `reset_token` is set in the database.\n\n##### Example Curl request:\n\n```sh\ncurl --location '127.0.0.1:5150/api/auth/forgot' \\\n     --header 'Content-Type: application/json' \\\n     --data-raw '{\n         \"email\": \"user@loco.rs\"\n     }'\n```\n\n#### Reset Password\n\nTo reset the password, send the token generated in the `forgot` endpoint along with the new password.\n\n##### Example Curl request:\n\n```sh\ncurl --location '127.0.0.1:5150/api/auth/reset' \\\n     --header 'Content-Type: application/json' \\\n     --data '{\n         \"token\": \"TOKEN\",\n         \"password\": \"new-password\"\n     }'\n```\n\n### Get current user\n\nThis endpoint is protected by auth middleware.\n\n```sh\ncurl --location --request GET '127.0.0.1:5150/api/auth/current' \\\n     --header 'Content-Type: application/json' \\\n     --header 'Authorization: Bearer TOKEN'\n```\n\n### Creating an Authenticated Endpoint\n\nTo establish an authenticated endpoint, import `controller::extractor::auth` from the `loco_rs` library and incorporate the auth middleware into the function endpoint parameters.\n\nConsider the following example in Rust:\n\n```rust\nuse axum::{extract::State, Json};\nuse loco_rs::{\n    app::AppContext,\n    controller::extractor::auth,\n    Result,\n};\n\nasync fn current(\n    auth: auth::JWT,\n    State(ctx): State<AppContext>,\n) -> Result<Response> {\n    let user = users::Model::find_by_pid(&ctx.db, &auth.claims.pid).await?;\n    /// Some response\n}\n\n```\n\n## API Authentication\n\n### Creating new app\n\nFor this time, let create your rest app using the [loco cli](/docs/getting-started/tour) and select the `Rest app` option.\nTo create new app, run the following command and follow the instructions:\n\n```sh\n$ loco new\n```\n\nTo explore the out-of-the-box auth controllers, run the following command:\n\n```sh\n$ cargo loco routes\n .\n .\n .\n[POST] /api/auth/forgot\n[POST] /api/auth/login\n[POST] /api/auth/register\n[POST] /api/auth/reset\n[GET] /api/auth/verify\n[GET] /api/auth/current\n .\n .\n .\n```\n\n### Registering new user\n\nThe `/api/auth/register` endpoint creates a new user in the database with an `api_key` for request authentication. `api_key` will be used for authentication in the future requests.\n\n#### Example Curl Request:\n\n```sh\ncurl --location '127.0.0.1:5150/api/auth/register' \\\n     --header 'Content-Type: application/json' \\\n     --data-raw '{\n         \"name\": \"Loco user\",\n         \"email\": \"user@loco.rs\",\n         \"password\": \"12341234\"\n     }'\n```\n\nAfter registering a new user, make sure you see the `api_key` in the database for the new user.\n\n### Creating an Authenticated Endpoint with API Authentication\n\nTo set up an API-authenticated endpoint, import `controller::extractor::auth` from the loco_rs library and include the auth middleware in the function endpoint parameters using `auth::ApiToken`.\n\nConsider the following example in Rust:\n\n```rust\nuse loco_rs::prelude::*;\nuse loco_rs::controller::extractor::auth;\nuse crate::{models::_entities::users, views::user::CurrentResponse};\n\nasync fn current_by_api_key(\n    auth: auth::ApiToken<users::Model>,\n    State(_ctx): State<AppContext>,\n) -> Result<Response> {\n    format::json(CurrentResponse::new(&auth.user))\n}\n\npub fn routes() -> Routes {\n    Routes::new()\n        .prefix(\"user\")\n        .add(\"/current-api\", get(current_by_api_key))\n}\n```\n\n### Requesting an API Authenticated Endpoint\n\nTo request an authenticated endpoint, you need to pass the `API_KEY` in the `Authorization` header.\n\n#### Example Curl Request:\n\n```sh\ncurl --location '127.0.0.1:5150/api/user/current-api' \\\n     --header 'Content-Type: application/json' \\\n     --header 'Authorization: Bearer API_KEY'\n```\n\nIf the `API_KEY` is valid, you will get the response with the user details.\n"
  },
  {
    "path": "docs-site/content/docs/extras/pluggability.md",
    "content": "+++\ntitle = \"Pluggability\"\ndescription = \"\"\ndate = 2021-05-01T18:10:00+00:00\nupdated = 2021-05-01T18:10:00+00:00\ndraft = false\nweight = 3\nsort_by = \"weight\"\ntemplate = \"docs/page.html\"\n\n[extra]\nlead = \"\"\ntoc = true\ntop = false\nflair =[]\n+++\n\n## Error levels and options\n\nAs a reminder, error levels and their logging can be controlled in your `development.yaml`:\n\n### Logger\n\n<!-- <snip id=\"configuration-logger\" inject_from=\"code\" template=\"yaml\"> -->\n\n```yaml\n# Application logging configuration\nlogger:\n  # Enable or disable logging.\n  enable: true\n  # Enable pretty backtrace (sets RUST_BACKTRACE=1)\n  pretty_backtrace: true\n  # Log level, options: trace, debug, info, warn or error.\n  level: debug\n  # Define the logging format. options: compact, pretty or json\n  format: compact\n  # By default the logger has filtering only logs that came from your code or logs that came from `loco` framework. to see all third party libraries\n  # Uncomment the line below to override to see all third party libraries you can enable this config and override the logger filters.\n  # override_filter: trace\n```\n\n<!-- </snip> -->\n\nThe most important knobs here are:\n\n- `level` - your standard logging levels. Typically `debug` or `trace` in development. In production, choose what you are used to.\n- `pretty_backtrace` - provides a clear, concise path to the line of code causing the error. Use `true` in development and turn it off in production. In cases where you are debugging things in production and need some extra hand, you can turn it on and then off when you're done.\n\n### Controller logging\n\nIn `server.middlewares` you will find:\n\n```yaml\nserver:\n  middlewares:\n    #\n    # ...\n    #\n    # Generating a unique request ID and enhancing logging with additional information such as the start and completion of request processing, latency, status code, and other request details.\n    logger:\n      # Enable/Disable the middleware.\n      enable: true\n```\n\nYou should enable it to get detailed request errors and a useful `request-id` that can help collate multiple request-scoped errors.\n\n### Database\n\nYou have the option of logging live SQL queries, in your `database` section:\n\n```yaml\ndatabase:\n  # When enabled, the sql query will be logged.\n  enable_logging: false\n```\n\n### Operating around errors\n\nYou'll be mostly looking at your terminal for errors while developing your app, it can look something like this:\n\n```bash\n2024-02-xxx DEBUG http-request: tower_http::trace::on_request: started processing request http.method=GET http.uri=/notes http.version=HTTP/1.1 http.user_agent=curl/8.1.2 environment=development request_id=8622e624-9bda-49ce-9730-876f2a8a9a46\n2024-02-xxx11T12:19:25.295954Z ERROR http-request: loco_rs::controller: controller_error error.msg=invalid type: string \"foo\", expected a sequence error.details=JSON(Error(\"invalid type: string \\\"foo\\\", expected a sequence\", line: 0, column: 0)) error.chain=\"\" http.method=GET http.uri=/notes http.version=HTTP/1.1 http.user_agent=curl/8.1.2 environment=development request_id=8622e624-9bda-49ce-9730-876f2a8a9a46\n```\n\nUsually you can expect the following from errors:\n\n- `error.msg` a `to_string()` version of an error, for operators.\n- `error.detail` a debug representation of an error, for developers.\n- An error **type** e.g. `controller_error` as the primary message tailored for searching, rather than a verbal error message.\n- Errors are logged as _tracing_ events and spans, so that you can build any infrastructure you want to provide custom tracing subscribers. Check out the [prometheus](https://github.com/loco-rs/loco-extras/blob/main/src/initializers/prometheus.rs) example in `loco-extras`.\n\nNotes:\n\n- An _error chain_ was experimented with, but provides little value in practice.\n- Errors that an end user sees are a completely different thing. We strive to provide **minimal internal details** about an error for an end user when we know a user can't do anything about an error (e.g. \"database offline error\"), mostly it will be a generic \"Internal Server Error\" on purpose -- for security reasons.\n\n### Producing errors\n\nWhen you build controllers, you write your handlers to return `Result<impl IntoResponse>`. The `Result` here is a Loco `Result`, which means it also associates a Loco `Error` type.\n\nIf you reach out for the Loco `Error` type you can use any of the following as a response:\n\n```rust\nErr(Error::string(\"some custom message\"));\nErr(Error::msg(other_error)); // turns other_error to its string representation\nErr(Error::wrap(other_error));\nErr(Error::Unauthorized(\"some message\"))\n\n// or through controller helpers:\nunauthorized(\"some message\") // create a full response object, calling Err on a created error\n```\n\n## Initializers\n\nInitializers are a way to encapsulate a piece of infrastructure \"wiring\" that you need to do in your app. You put initializers in `src/initializers/`.\n\n### Writing initializers\n\nCurrently, an initializer is anything that implements the `Initializer` trait:\n\n<!-- <snip id=\"initializers-trait\" inject_from=\"code\" template=\"rust\"> -->\n\n```rust\npub trait Initializer: Sync + Send {\n    /// The initializer name or identifier\n    fn name(&self) -> String;\n\n    /// Occurs after the app's `before_run`.\n    /// Use this to for one-time initializations, load caches, perform web\n    /// hooks, etc.\n    async fn before_run(&self, _app_context: &AppContext) -> Result<()> {\n        Ok(())\n    }\n\n    /// Occurs after the app's `after_routes`.\n    /// Use this to compose additional functionality and wire it into an Axum\n    /// Router\n    async fn after_routes(&self, router: AxumRouter, _ctx: &AppContext) -> Result<AxumRouter> {\n        Ok(router)\n    }\n\n    /// Perform health checks for this initializer.\n    /// This method is called during the doctor command to validate the initializer's configuration.\n    /// Return `None` if no check is needed, or `Some(Check)` if a check should be performed.\n    async fn check(&self, _app_context: &AppContext) -> Result<Option<crate::doctor::Check>> {\n        Ok(None)\n    }\n}\n```\n\n<!-- </snip> -->\n\n### Example: Integrating Axum Session\n\nYou might want to add sessions to your app using `axum-session`. Also, you might want to share that piece of functionality between your own projects, or grab that piece of code from someone else.\n\nYou can achieve this reuse easily, if you code the integration as an _initializer_:\n\n```rust\n// place this in `src/initializers/axum_session.rs`\n#[async_trait]\nimpl Initializer for AxumSessionInitializer {\n    fn name(&self) -> String {\n        \"axum-session\".to_string()\n    }\n\n    async fn after_routes(&self, router: AxumRouter, _ctx: &AppContext) -> Result<AxumRouter> {\n        let session_config =\n            axum_session::SessionConfig::default().with_table_name(\"sessions_table\");\n        let session_store =\n            axum_session::SessionStore::<axum_session::SessionNullPool>::new(None, session_config)\n                .await\n                .unwrap();\n        let router = router.layer(axum_session::SessionLayer::new(session_store));\n        Ok(router)\n    }\n}\n```\n\nAnd now your app structure looks like this:\n\n```\nsrc/\n bin/\n controllers/\n    :\n    :\n initializers/       <--- a new folder\n   mod.rs            <--- a new module\n   axum_session.rs   <--- your new initializer\n    :\n    :\n  app.rs   <--- register initializers here\n```\n\n### Using initializers\n\nAfter you've implemented your own initializer, you should implement the `initializers(..)` hook in your `src/app.rs` and provide a Vec of your initializers:\n\n<!-- <snip id=\"app-initializers\" inject_from=\"code\" template=\"rust\"> -->\n\n```rust\n    async fn initializers(_ctx: &AppContext) -> Result<Vec<Box<dyn Initializer>>> {\n        let initializers: Vec<Box<dyn Initializer>> = vec![\n            Box::new(initializers::axum_session::AxumSessionInitializer),\n            Box::new(initializers::view_engine::ViewEngineInitializer),\n            Box::new(initializers::hello_view_engine::HelloViewEngineInitializer),\n        ];\n\n        Ok(initializers)\n    }\n```\n\n<!-- </snip> -->\n\nLoco will now run your initializer stack in the correct places during the app boot process.\n\n### Initializer Health Checks\n\nInitializers can now provide their own health checks by implementing the `check` method. This allows each initializer to validate its configuration and test its connections during the `cargo loco doctor` command.\n\n#### Implementing Health Checks\n\nTo add health checks to your initializer, implement the `check` method:\n\n```rust\nuse async_trait::async_trait;\nuse loco_rs::app::{AppContext, Initializer};\nuse loco_rs::doctor::{Check, CheckStatus};\n\nstruct MyCustomInitializer;\n\n#[async_trait]\nimpl Initializer for MyCustomInitializer {\n    fn name(&self) -> String {\n        \"my_custom_initializer\".to_string()\n    }\n\n    async fn check(&self, app_context: &AppContext) -> loco_rs::Result<Option<Check>> {\n        // Check if your configuration exists\n        let config = app_context.config.initializers.as_ref()\n            .and_then(|init| init.get(\"my_custom_initializer\"))\n            .ok_or_else(|| loco_rs::Error::Message(\"Configuration not found\".to_string()))?;\n\n        // Perform your health check\n        match self.test_connection(config).await {\n            Ok(()) => Ok(Some(Check {\n                status: CheckStatus::Ok,\n                message: \"My custom service: success\".to_string(),\n                description: None,\n            })),\n            Err(err) => Ok(Some(Check {\n                status: CheckStatus::NotOk,\n                message: \"My custom service: failed\".to_string(),\n                description: Some(err.to_string()),\n            })),\n        }\n    }\n}\n```\n\n#### Health Check Return Values\n\nThe `check` method returns `Result<Option<Check>>`:\n\n- **`Ok(None)`**: No health check needed (default behavior)\n- **`Ok(Some(Check))`**: Health check result to be displayed\n\n#### Check Status Types\n\n```rust\npub enum CheckStatus {\n    Ok,           // ✅ Component is healthy\n    NotOk,        // ❌ Component has issues\n    NotConfigure, // ⚠️ Component not configured (may be intentional)\n}\n```\n\n#### Running Initializer Health Checks\n\nHealth checks are automatically run when you execute:\n\n```sh\ncargo loco doctor\n```\n\nThe output will include your initializer checks:\n\n```\n✅ Database connection: success\n✅ Initializer my_custom_initializer: My custom service: success\n❌ Initializer failing_service: Service connection: failed\n   connection timeout after 30 seconds\n```\n\n#### Optional Health Checks\n\nHealth checks are completely optional. If your initializer doesn't implement the `check` method, it will use the default implementation that returns `Ok(None)`, meaning no health check will be performed.\n\nThis makes the feature backward-compatible and allows initializers to opt-in to health checking when needed.\n\n### What other things you can do?\n\nRight now initializers contain two integration points:\n\n- `before_run` - happens before running the app -- this is a pure \"initialization\" type of a hook. You can send web hooks, metric points, do cleanups, pre-flight checks, etc.\n- `after_routes` - happens after routes have been added. You have access to the Axum router and its powerful layering integration points, this is where you will spend most of your time.\n\n### Compared to Rails initializers\n\nRails initializers, are regular scripts that run once -- for initialization and have access to everything. They get their power from being able to access a \"live\" Rails app, modify it as a global instance.\n\nIn Loco, accessing a global instance and mutating it is not possible in Rust (for a good reason!), and so we offer two integration points which are explicit and safe:\n\n1. Pure initialization (without any influence on a configured app)\n2. Integration with a running app (via Axum router)\n\nRails initializers need _ordering_ and _modification_. Meaning, a user should be certain that they run in a specific order (or re-order them), and a user is able to remove initializers that other people set before them.\n\nIn Loco, we circumvent this complexity by making the user _provide a full vec_ of initializers. Vecs are ordered, and there are no implicit initializers.\n\n### The global logger initializer\n\nSome developers would like to customize their logging stack. In Loco this involves setting up tracing and tracing subscribers.\n\nBecause at the moment tracing does not allow for re-initialization, or modification of an in-flight tracing stack, you _only get one chance to initialize and registr a global tracing stack_.\n\nThis is why we added a new _App level hook_, called `init_logger`, which you can use to provide your own logging stack initialization.\n\n```rust\n// in src/app.rs\nimpl Hooks for App {\n    // return `Ok(true)` if you took over initializing logger\n    // otherwise, return `Ok(false)` to use the Loco logging stack.\n    fn init_logger(_ctx: &AppContext) -> Result<bool> {\n        Ok(false)\n    }\n}\n```\n\nAfter you've set up your own logger, return `Ok(true)` to signal that you took over initialization.\n\n## Middlewares\n\n`Loco` is a framework that is built on top of [`axum`](https://crates.io/crates/axum)\nand [`tower`](https://crates.io/crates/tower). They provide a way to\nadd [layers](https://docs.rs/tower/latest/tower/trait.Layer.html)\nand [services](https://docs.rs/tower/latest/tower/trait.Service.html) as middleware to your routes and handlers.\n\nMiddleware is a way to add pre- and post-processing to your requests. This can be used for logging, authentication, rate\nlimiting, route-specific processing, and more.\n\n### Source Code\n\n`Loco`'s implementation of route middleware/layer is similar\nto `axum`'s [`Router::layer`](https://github.com/tokio-rs/axum/blob/main/axum/src/routing/mod.rs#L275). You can\nfind the source code for middleware in\nthe [`src/controllers/routes`](https://github.com/loco-rs/loco/blob/master/src/controller/routes.rs) directory.\nThis `layer` function will attach the\nmiddleware layer to each handler of the route.\n\n```rust\n// src/controller/routes.rs\nuse axum::{extract::Request, response::IntoResponse, routing::Route};\nuse tower::{Layer, Service};\n\nimpl Routes {\n    pub fn layer<L>(self, layer: L) -> Self\n        where\n            L: Layer<Route> + Clone + Send + 'static,\n            L::Service: Service<Request> + Clone + Send + 'static,\n            <L::Service as Service<Request>>::Response: IntoResponse + 'static,\n            <L::Service as Service<Request>>::Error: Into<Infallible> + 'static,\n            <L::Service as Service<Request>>::Future: Send + 'static,\n    {\n        Self {\n            prefix: self.prefix,\n            handlers: self\n                .handlers\n                .iter()\n                .map(|handler| Handler {\n                    uri: handler.uri.clone(),\n                    actions: handler.actions.clone(),\n                    method: handler.method.clone().layer(layer.clone()),\n                })\n                .collect(),\n        }\n    }\n}\n```\n\n### Basic Middleware\n\nIn this example, we will create a basic middleware that will log the request method and path.\n\n```rust\n// src/controllers/middleware/log.rs\nuse std::{\n    convert::Infallible,\n    task::{Context, Poll},\n};\n\nuse axum::{\n    body::Body,\n    extract::{FromRequestParts, Request},\n    response::Response,\n};\nuse futures_util::future::BoxFuture;\nuse loco_rs::prelude::{auth::JWTWithUser, *};\nuse tower::{Layer, Service};\n\nuse crate::models::{users};\n\n#[derive(Clone)]\npub struct LogLayer;\n\nimpl LogLayer {\n    pub fn new() -> Self {\n        Self {}\n    }\n}\n\nimpl<S> Layer<S> for LogLayer {\n    type Service = LogService<S>;\n\n    fn layer(&self, inner: S) -> Self::Service {\n        Self::Service {\n            inner,\n        }\n    }\n}\n\n#[derive(Clone)]\npub struct LogService<S> {\n    // S is the inner service, in the case, it is the `/auth/register` handler\n    inner: S,\n}\n\n/// Implement the Service trait for LogService\n/// # Generics\n/// * `S` - The inner service, in this case is the `/auth/register` handler\n/// * `B` - The body type\nimpl<S, B> Service<Request<B>> for LogService<S>\n    where\n        S: Service<Request<B>, Response=Response<Body>, Error=Infallible> + Clone + Send + 'static, /* Inner Service must return Response<Body> and never error, which is typical for handlers */\n        S::Future: Send + 'static,\n        B: Send + 'static,\n{\n    // Response type is the same as the inner service / handler\n    type Response = S::Response;\n    // Error type is the same as the inner service / handler\n    type Error = S::Error;\n    // Future type is the same as the inner service / handler\n    type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;\n\n    // poll_ready is used to check if the service is ready to process a request\n    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {\n        // Our middleware doesn't care about backpressure, so it's ready as long\n        // as the inner service is ready.\n        self.inner.poll_ready(cx)\n    }\n\n    fn call(&mut self, req: Request<B>) -> Self::Future {\n        let clone = self.inner.clone();\n        // take the service that was ready\n        let mut inner = std::mem::replace(&mut self.inner, clone);\n        Box::pin(async move {\n            let (mut parts, body) = req.into_parts();\n            tracing::info!(\"Request: {:?} {:?}\", parts.method, parts.uri.path());\n            let req = Request::from_parts(parts, body);\n            inner.call(req).await\n        })\n    }\n}\n```\n\nAt the first glance, this middleware is a bit overwhelming. Let's break it down.\n\nThe `LogLayer` is a [`tower::Layer`](https://docs.rs/tower/latest/tower/trait.Layer.html) that wraps around the inner\nservice.\n\nThe `LogService` is a [`tower::Service`](https://docs.rs/tower/latest/tower/trait.Service.html) that implements\nthe `Service` trait for the request.\n\n### Generics Explanation\n\n**`Layer`**\n\nIn the `Layer` trait, `S` represents the inner service, which in this case is the `/auth/register` handler. The `layer`\nfunction takes this inner service and returns a new service that wraps around it.\n\n**`Service`**\n\n`S` is the inner service, in this case, it is the `/auth/register` handler. If we have a look about\nthe [`get`](https://docs.rs/axum/latest/axum/routing/method_routing/fn.get.html), [`post`](https://docs.rs/axum/latest/axum/routing/method_routing/fn.post.html), [`put`](https://docs.rs/axum/latest/axum/routing/method_routing/fn.put.html), [`delete`](https://docs.rs/axum/latest/axum/routing/method_routing/fn.delete.html)\nfunctions which we use for handlers, they all return\na [`MethodRoute<S, Infallible>`(Which is a service)](https://docs.rs/axum/latest/axum/routing/method_routing/struct.MethodRouter.html).\n\nTherefore, `S: Service<Request<B>, Response = Response<Body>, Error = Infallible>` means it takes in a `Request<B>`(\nRequest with a body) and returns a `Response<Body>`. The `Error` is `Infallible` which means the handler never errors.\n\n`S::Future: Send + 'static` means the future of the inner service must implement `Send` trait and `'static`.\n\n`type Response = S::Response` means the response type of the middleware is the same as the inner service.\n\n`type Error = S::Error` means the error type of the middleware is the same as the inner service.\n\n`type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>` means the future type of the middleware is the\nsame as the inner service.\n\n`B: Send + 'static` means the request body type must implement the `Send` trait and `'static`.\n\n### Function Explanation\n\n**`LogLayer`**\n\nThe `LogLayer::new` function is used to create a new instance of the `LogLayer`.\n\n**`LogService`**\n\nThe `LogService::poll_ready` function is used to check if the service is ready to process a request. It can be used for\nbackpressure, for more information see\nthe [`tower::Service` documentation](https://docs.rs/tower/latest/tower/trait.Service.html)\nand [Tokio tutorial](https://tokio.rs/blog/2021-05-14-inventing-the-service-trait#backpressure).\n\nThe `LogService::call` function is used to process the request. In this case, we are logging the request method and\npath. Then we are calling the inner service with the request.\n\n**Importance of `poll_ready`:**\n\nIn the Tower framework, before a service can be used to handle a request, it must be\nchecked for readiness\nusing the\n`poll_ready` method. This method returns `Poll::Ready(Ok(()))` when the service is ready to process a request. If a\nservice is not ready, it may return `Poll::Pending`, indicating that the caller should wait before sending a request.\nThis mechanism ensures that the service has the necessary resources or state to process the request efficiently and\ncorrectly.\n\n**Cloning and Readiness**\n\nWhen cloning a service, particularly to move it into a boxed future or similar context, it's crucial to understand that\nthe clone does not inherit the readiness state of the original service. Each clone of a service maintains its own state.\nThis means that even if the original service was ready `(Poll::Ready(Ok(())))`, the cloned service might not be in the\nsame state immediately after cloning. This can lead to issues where a cloned service is used before it is ready,\npotentially causing panics or other failures.\n\n**Correct approach to cloning services using `std::mem::replace`**\nTo handle cloning correctly, it's recommended to use `std::mem::replace` to swap the ready service with its clone in a\ncontrolled manner. This approach ensures that the service being used to handle the request is the one that has been\nverified as ready. Here's how it works:\n\n- Clone the service: First, create a clone of the service. This clone will eventually replace the original service in\n  the service handler.\n- Replace the original with the clone: Use `std::mem::replace` to swap the original service with the clone. This\n  operation ensures that the service handler continues to hold a service instance.\n- Use the original service to handle the request: Since the original service was already checked for readiness (via\n  `poll_ready`), it's safe to use it to handle the incoming request. The clone, now in the handler, will be the one\n  checked for readiness next time.\n\nThis method ensures that each service instance used to handle requests is always the one that has been explicitly\nchecked for readiness, thus maintaining the integrity and reliability of the service handling process.\n\nHere is a simplified example to illustrate this pattern:\n\n```rust\n// Wrong\nfn call(&mut self, req: Request<B>) -> Self::Future {\n    let mut inner = self.inner.clone();\n    Box::pin(async move {\n        /* ... */\n        inner.call(req).await\n    })\n}\n\n// Correct\nfn call(&mut self, req: Request<B>) -> Self::Future {\n    let clone = self.inner.clone();\n    // take the service that was ready\n    let mut inner = std::mem::replace(&mut self.inner, clone);\n    Box::pin(async move {\n        /* ... */\n        inner.call(req).await\n    })\n}\n```\n\nIn this example, `inner` is the service that was ready, and after handling the request, `self.inner` now holds the\nclone, which will be checked for readiness in the next cycle. This careful management of service readiness and cloning\nis essential for maintaining robust and error-free service operations in asynchronous Rust applications using Tower.\n\n[Tower Service Cloning Documentation](https://docs.rs/tower/latest/tower/trait.Service.html#be-careful-when-cloning-inner-services)\n\n### Adding Middleware to Handler\n\nAdd the middleware to the `auth::register` handler.\n\n```rust\n// src/controllers/auth.rs\npub fn routes() -> Routes {\n    Routes::new()\n        .prefix(\"auth\")\n        .add(\"/register\", post(register).layer(middlewares::log::LogLayer::new()))\n}\n```\n\nNow when you make a request to the `auth::register` handler, you will see the request method and path logged.\n\n```shell\n2024-XX-XXTXX:XX:XX.XXXXXZ  INFO http-request: xx::controllers::middleware::log Request: POST \"/auth/register\" http.method=POST http.uri=/auth/register http.version=HTTP/1.1  environment=development request_id=xxxxx\n```\n\n## Adding Middleware to Route\n\nAdd the middleware to the `auth` route.\n\n```rust\n// src/main.rs\npub struct App;\n\n#[async_trait]\nimpl Hooks for App {\n    fn routes(_ctx: &AppContext) -> AppRoutes {\n        AppRoutes::with_default_routes()\n            .add_route(\n                controllers::auth::routes()\n                    .layer(middlewares::log::LogLayer::new()),\n            )\n    }\n}\n```\n\nNow when you make a request to any handler in the `auth` route, you will see the request method and path logged.\n\n```shell\n2024-XX-XXTXX:XX:XX.XXXXXZ  INFO http-request: xx::controllers::middleware::log Request: POST \"/auth/register\" http.method=POST http.uri=/auth/register http.version=HTTP/1.1  environment=development request_id=xxxxx\n```\n\n### Advanced Middleware (With AppContext)\n\nThere will be times when you need to access the `AppContext` in your middleware. For example, you might want to access\nthe database connection to perform some authorization checks. To do this, you can add the `AppContext` to\nthe `Layer` and `Service`.\n\nHere we will create a middleware that checks the JWT token and gets the user from the database then prints the user's\nname\n\n```rust\n// src/controllers/middleware/log.rs\nuse std::{\n    convert::Infallible,\n    task::{Context, Poll},\n};\n\nuse axum::{\n    body::Body,\n    extract::{FromRequestParts, Request},\n    response::Response,\n};\nuse futures_util::future::BoxFuture;\nuse loco_rs::prelude::{auth::JWTWithUser, *};\nuse tower::{Layer, Service};\n\nuse crate::models::{users};\n\n#[derive(Clone)]\npub struct LogLayer {\n    state: AppContext,\n}\n\nimpl LogLayer {\n    pub fn new(state: AppContext) -> Self {\n        Self { state }\n    }\n}\n\nimpl<S> Layer<S> for LogLayer {\n    type Service = LogService<S>;\n\n    fn layer(&self, inner: S) -> Self::Service {\n        Self::Service {\n            inner,\n            state: self.state.clone(),\n        }\n    }\n}\n\n#[derive(Clone)]\npub struct LogService<S> {\n    inner: S,\n    state: AppContext,\n}\n\nimpl<S, B> Service<Request<B>> for LogService<S>\n    where\n        S: Service<Request<B>, Response=Response<Body>, Error=Infallible> + Clone + Send + 'static, /* Inner Service must return Response<Body> and never error */\n        S::Future: Send + 'static,\n        B: Send + 'static,\n{\n    // Response type is the same as the inner service / handler\n    type Response = S::Response;\n    // Error type is the same as the inner service / handler\n    type Error = S::Error;\n    // Future type is the same as the inner service / handler\n    type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;\n    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {\n        self.inner.poll_ready(cx)\n    }\n\n    fn call(&mut self, req: Request<B>) -> Self::Future {\n        let state = self.state.clone();\n        let clone = self.inner.clone();\n        // take the service that was ready\n        let mut inner = std::mem::replace(&mut self.inner, clone);\n        Box::pin(async move {\n            // Example of extracting JWT token from the request\n            let (mut parts, body) = req.into_parts();\n            let auth = JWTWithUser::<users::Model>::from_request_parts(&mut parts, &state).await;\n\n            match auth {\n                Ok(auth) => {\n                    // Example of getting user from the database\n                    let user = users::Model::find_by_email(&state.db, &auth.user.email).await.unwrap();\n                    tracing::info!(\"User: {}\", user.name);\n                    let req = Request::from_parts(parts, body);\n                    inner.call(req).await\n                }\n                Err(_) => {\n                    // Handle error, e.g., return an unauthorized response\n                    Ok(Response::builder()\n                        .status(401)\n                        .body(Body::empty())\n                        .unwrap()\n                        .into_response())\n                }\n            }\n        })\n    }\n}\n```\n\nIn this example, we have added the `AppContext` to the `LogLayer` and `LogService`. We are using the `AppContext` to get\nthe database connection and the JWT token for pre-processing.\n\n### Adding Middleware to Route (advanced)\n\nAdd the middleware to the `notes` route.\n\n```rust\n// src/app.rs\npub struct App;\n\n#[async_trait]\nimpl Hooks for App {\n    fn routes(ctx: &AppContext) -> AppRoutes {\n        AppRoutes::with_default_routes()\n            .add_route(controllers::notes::routes().layer(middlewares::log::LogLayer::new(ctx)))\n    }\n}\n```\n\nNow when you make a request to any handler in the `notes` route, you will see the user's name logged.\n\n```shell\n2024-XX-XXTXX:XX:XX.XXXXXZ  INFO http-request: xx::controllers::middleware::log User: John Doe  environment=development request_id=xxxxx\n```\n\n### Adding Middleware to Handler (advanced)\n\nIn order to add the middleware to the handler, you need to add the `AppContext` to the `routes` function\nin `src/app.rs`.\n\n```rust\n// src/app.rs\npub struct App;\n\n#[async_trait]\nimpl Hooks for App {\n    fn routes(ctx: &AppContext) -> AppRoutes {\n        AppRoutes::with_default_routes()\n            .add_route(\n                controllers::notes::routes(ctx)\n            )\n    }\n}\n```\n\nThen add the middleware to the `notes::create` handler.\n\n```rust\n// src/controllers/notes.rs\npub fn routes(ctx: &AppContext) -> Routes {\n    Routes::new()\n        .prefix(\"notes\")\n        .add(\"/create\", post(create).layer(middlewares::log::LogLayer::new(ctx)))\n}\n```\n\nNow when you make a request to the `notes::create` handler, you will see the user's name logged.\n\n```shell\n2024-XX-XXTXX:XX:XX.XXXXXZ  INFO http-request: xx::controllers::middleware::log User: John Doe  environment=development request_id=xxxxx\n```\n\n## Application SharedStore\n\nLoco provides a flexible mechanism called `SharedStore` within the `AppContext` to store and share arbitrary custom data or services across your application. This feature allows you to inject your own types into the application context without modifying Loco's core structures, enhancing pluggability and customization.\n\n`AppContext.shared_store` is a type-safe, thread-safe heterogeneous storage. You can store any type that implements `'static + Send + Sync`.\n\n### Why Use SharedStore?\n\n- **Sharing Custom Services:** Inject your own service clients (e.g., a custom API client) and access them from controllers or background workers.\n- **Storing Configuration:** Keep application-specific configuration objects accessible globally.\n- **Shared State:** Manage state needed by different parts of your application.\n\n### How to Use SharedStore\n\nYou typically insert your custom data into the `shared_store` during application startup (e.g., in `src/app.rs`) and then retrieve it within your controllers or other components.\n\n**1. Define Your Data Structures:**\n\nCreate the structs for the data or services you want to share. Note whether they implement `Clone`.\n\n```rust\n// In src/app.rs or a dedicated module (e.g., src/services.rs)\n\n// This service can be cloned\n#[derive(Clone, Debug)]\npub struct MyClonableService {\n    pub api_key: String,\n}\n\n// This service cannot (or should not) be cloned\n#[derive(Debug)]\npub struct MyNonClonableService {\n    pub api_key: String,\n}\n```\n\n**2. Insert into SharedStore (in `src/app.rs`):**\n\nA good place to insert your shared data is the `after_context` hook in your `App`'s `Hooks` implementation.\n\n```rust\n// In src/app.rs\n\nuse crate::MyClonableService; // Import your structs\nuse crate::MyNonClonableService;\n\npub struct App;\n#[async_trait]\nimpl Hooks for App {\n    // ... other Hooks methods (app_name, boot, etc.) ...\n\n    async fn after_context(mut ctx: AppContext) -> Result<AppContext> {\n        // Create instances of your services/data\n        let clonable_service = MyClonableService {\n            api_key: \"key-cloned-12345\".to_string(),\n        };\n        let non_clonable_service = MyNonClonableService {\n            api_key: \"key-ref-67890\".to_string(),\n        };\n\n        // Insert them into the shared store\n        ctx.shared_store.insert(clonable_service);\n        ctx.shared_store.insert(non_clonable_service);\n\n        Ok(ctx)\n    }\n\n    // ... rest of Hooks implementation ...\n}\n```\n\n**3. Retrieve from SharedStore (in Controllers):**\n\nYou have two main ways to retrieve data in your controllers:\n\n- **Using the `SharedStore(var)` Extractor (for `Clone`-able types):**\n  This is the most convenient way if your type implements `Clone`. The extractor retrieves and _clones_ the data for you.\n\n  ```rust\n  // In src/controllers/some_controller.rs\n  use loco_rs::prelude::*;\n  use crate::app::MyClonableService; // Or wherever it's defined\n\n  #[debug_handler]\n  pub async fn index(\n      // Extracts and clones MyClonableService into `service`\n      SharedStore(service): SharedStore<MyClonableService>,\n  ) -> impl IntoResponse {\n      tracing::info!(\"Using Cloned Service API Key: {}\", service.api_key);\n      format::empty()\n  }\n  ```\n\n- **Using `ctx.shared_store.get_ref()` (for Non-`Clone`-able types or avoiding clones):**\n  Use this method when your type doesn't implement `Clone` or when you want to avoid the performance cost of cloning. It gives you a reference (`RefGuard<T>`) to the data.\n\n  ```rust\n  // In src/controllers/some_controller.rs\n  use loco_rs::prelude::*;\n  use crate::app::MyNonClonableService; // Or wherever it's defined\n\n  #[debug_handler]\n  pub async fn index(\n      State(ctx): State<AppContext>, // Need the AppContext state\n  ) -> Result<impl IntoResponse> {\n      // Get a reference to the non-clonable service\n      let service_ref = ctx.shared_store.get_ref::<MyNonClonableService>()\n          .ok_or_else(|| {\n              tracing::error!(\"MyNonClonableService not found in shared store\");\n              Error::InternalServerError // Or a more specific error\n          })?;\n\n      // Access fields via the reference guard\n      tracing::info!(\"Using Non-Cloned Service API Key: {}\", service_ref.api_key);\n      format::empty()\n  }\n  ```\n\n**Summary:**\n\n- Use `SharedStore` in `AppContext` to share custom services or data.\n- Insert data during app setup (e.g., `after_context` in `src/app.rs`).\n- Use the `SharedStore(var)` extractor for convenient access to `Clone`-able types (clones the data).\n- Use `ctx.shared_store.get_ref::<T>()` to get a reference to non-`Clone`-able types or to avoid cloning for performance reasons.\n"
  },
  {
    "path": "docs-site/content/docs/extras/upgrades.md",
    "content": "+++\ntitle = \"Upgrades\"\ndescription = \"\"\ndate = 2021-05-01T18:20:00+00:00\nupdated = 2021-05-01T18:20:00+00:00\ndraft = false\nweight = 4\nsort_by = \"weight\"\ntemplate = \"docs/page.html\"\n\n[extra]\nlead = \"\"\ntoc = true\ntop = false\nflair =[]\n+++\n\n## What to do when a new Loco version is out?\n\n- Create a clean branch in your code repo.\n- Update the Loco version in your main `Cargo.toml`\n- Consult with the [CHANGELOG](https://github.com/loco-rs/loco/blob/master/CHANGELOG.md) to find breaking changes and refactorings you should do (if any).\n- Run `cargo loco doctor` inside your project to verify that your app and environment is compatible with the new version\n\nAs always, if anything turns wrong, [open an issue](https://github.com/loco-rs/loco/issues) and ask for help.\n\n## Major Loco dependencies\n\nLoco is built on top of great libraries. It's wise to be mindful of their versions in new releases of Loco, and their individual changelogs.\n\nThese are the major ones:\n\n- [SeaORM](https://www.sea-ql.org/SeaORM), [CHANGELOG](https://github.com/SeaQL/sea-orm/blob/master/CHANGELOG.md)\n- [Axum](https://github.com/tokio-rs/axum), [CHANGELOG](https://github.com/tokio-rs/axum/blob/main/axum/CHANGELOG.md)\n\n## Upgrade from 0.15.x to 0.16.x\n\n### Use `AppContext` instead of `Config` in `init_logger` in the `Hooks` trait\n\nPR: [#1418](https://github.com/loco-rs/loco/pull/1418)\n\nIf you are supplying an implementation of `init_logger` in your `impl` of the `Hooks` trait in order to set up your own logging, you will need to make the following change:\n\n```diff\n- fn init_logger(config: &config::Config, env: &Environment) -> Result<bool> {\n+ fn init_logger(ctx: &AppContext) -> Result<bool> {\n```\n\nAny code in your `init_logger` implementation that makes use of the `config` can access it through `ctx.config`. In addition, you will also be able to access anything else in the `AppContext`, such as the new `shared_store`. The `env` parameter is also removed, as that is accessible from the `AppContext` as `ctx.environment`.\n\n### Swap to validators builtin email validation\n\nPR: [#1359](https://github.com/loco-rs/loco/pull/1359)\n\nSwap from using the loco custom email validator, to the builtin email validator from `validator`.\n\n```diff\n- #[validate(custom (function = \"validation::is_valid_email\"))]\n+ #[validate(email(message = \"invalid email\"))]\n  pub email: String,\n```\n\n### Job system\n\nPR: [#1384](https://github.com/loco-rs/loco/pull/1384)\nPR: [#1396](https://github.com/loco-rs/loco/pull/1396)\n\nTwo major changes have been made to the background job system:\n\n1. The Redis provider is no longer Sidekiq-compatible and uses a custom implementation\n2. All providers (Redis, PostgreSQL, SQLite) now support tag-based job filtering\n\n#### What Changed\n\n##### Removing Sidekiq Compatibility\n\nThe Redis background job system has been completely refactored, replacing the Sidekiq-compatible implementation with a new custom implementation. This provides greater flexibility and improved performance, but means:\n\n- Jobs pushed from older Loco versions (pre-0.16) will not be recognized or processed\n- The Redis data structures have changed entirely\n- There is no automatic migration path for existing queued jobs\n\n##### Adding Job Filtering\n\nA new tag-based job filtering system has been added to all background worker providers:\n\n- Workers can now specify which tags they're interested in processing\n- Jobs can be tagged when enqueued\n- Workers with no tags only process untagged jobs, while tagged workers process jobs with matching tags\n- The same API is used across all providers\n\n#### How to Upgrade\n\nTo upgrade to the new job system:\n\n1. **Process existing jobs**:\n\n   - Make sure all jobs in your queue are processed/completed before upgrading\n\n2. **Clean up old data**:\n\n   - For Redis: Flush the Redis database used for jobs (`FLUSHDB` command)\n   - For PostgreSQL: Drop the job queue tables\n   - For SQLite: Delete the job queue tables\n\n3. **Update Loco**:\n   - Update to Loco 0.16+\n   - Loco will automatically create new job tables with the correct schema on first run\n\n### Generic Cache\n\nPR: [#1385](https://github.com/loco-rs/loco/pull/1385)\n\nThe cache API has been refactored to support storing and retrieving any serializable type, not just strings. This is a breaking change that requires updates to your code:\n\n#### Breaking Changes:\n\n1. **Type Parameters Required**: All cache methods now require explicit type parameters\n2. **Method Signatures**: Some method signatures have changed to support generics\n3. **Object Serialization**: Any type you store must implement `Serialize` and `Deserialize` from serde\n\n#### Migration Guide:\n\n**Before:**\n\n```rust\n// Get a string value from cache\nlet value = cache.get(\"key\").await?;\n\n// Insert or get with callback\nlet value = app_ctx.cache.get_or_insert(\"key\", async {\n    Ok(\"value\".to_string())\n}).await.unwrap();\n\n// Insert or get with expiry\nlet value = app_ctx.cache.get_or_insert_with_expiry(\"key\", Duration::from_secs(300), async {\n    Ok(\"value\".to_string())\n}).await.unwrap();\n```\n\n**After:**\n\n```rust\n// Get a string value from cache - specify the type\nlet value = cache.get::<String>(\"key\").await?;\n\n// Direct insert with any serializable type\ncache.insert(\"key\", &\"value\".to_string()).await?;\n\n// Insert or get with callback - specify return type\nlet value = app_ctx.cache.get_or_insert::<String, _>(\"key\", async {\n    Ok(\"value\".to_string())\n}).await.unwrap();\n\n// Store complex types\n#[derive(Serialize, Deserialize)]\nstruct User {\n    name: String,\n    age: u32,\n}\n\nlet user = app_ctx.cache.get_or_insert_with_expiry::<User, _>(\n    \"user:1\",\n    Duration::from_secs(300),\n    async {\n        Ok(User { name: \"Alice\".to_string(), age: 30 })\n    }\n).await.unwrap();\n```\n\n#### Implementing for Custom Types:\n\nFor your custom types to work with the cache, ensure they implement `Serialize` and `Deserialize`:\n\n```rust\nuse serde::{Serialize, Deserialize};\n\n#[derive(Serialize, Deserialize)]\nstruct MyType {\n    // fields...\n}\n```\n\n### Authentication Error Handling\n\nAuthentication error handling has been improved to better distinguish between actual authorization failures and system errors:\n\n1. **System errors now return 500**: Database errors during authentication now return Internal Server Error (500) instead of Unauthorized (401)\n2. **Improved error logging**: Authentication errors are now logged with detailed messages using `tracing::error`\n3. **Message changes**: Generic error messages have been updated from \"other error: '{e}'\" to \"could not authorize\"\n\n#### Migration Guide:\n\nIf you have code that relies on database errors during authentication returning 401 status codes, you'll need to update your error handling. Any code expecting a 401 for database connectivity issues should now handle 500 responses as well.\n\nClient applications should be prepared to handle both 401 and 500 status codes during authentication failures, with 401 indicating authorization problems and 500 indicating system errors.\n\n### Server side rendering\nWe had some changes in Tera template. go to `src/initializers/view_engine.rs` and replace the `after_routes` function with:\n```rust\nasync fn after_routes(&self, router: AxumRouter, _ctx: &AppContext) -> Result<AxumRouter> {\n        let tera_engine = if std::path::Path::new(I18N_DIR).exists() {\n            let arc = std::sync::Arc::new(\n                ArcLoader::builder(&I18N_DIR, unic_langid::langid!(\"en-US\"))\n                    .shared_resources(Some(&[I18N_SHARED.into()]))\n                    .customize(|bundle| bundle.set_use_isolating(false))\n                    .build()\n                    .map_err(|e| Error::string(&e.to_string()))?,\n            );\n            info!(\"locales loaded\");\n\n            engines::TeraView::build()?.post_process(move |tera| {\n                tera.register_function(\"t\", FluentLoader::new(arc.clone()));\n                Ok(())\n            })?\n        } else {\n            engines::TeraView::build()?\n        };\n\n        Ok(router.layer(Extension(ViewEngine::from(tera_engine))))\n    }\n```\n\n## Upgrade from 0.14.x to 0.15.x\n\n### Upgrade validator crate\n\nPR: [#1199](https://github.com/loco-rs/loco/pull/1199)\n\nUpdate the `validator` crate version in your `Cargo.toml`:\n\nFrom\n\n```\nvalidator = { version = \"0.19\" }\n```\n\nTo\n\n```\nvalidator = { version = \"0.20\" }\n```\n\n### User claims\n\nPR: [#1159](https://github.com/loco-rs/loco/pull/1159)\n\n- Flattened (De)Serialization of Custom User Claims:\n  The `claims` field in `UserClaims` has changed from `Option<Value>` to `Map<String, Value>`.\n\n- Mandatory Map Value in `generate_token` function:\n  When calling `generate_token`, the `Map<String, Value>` argument is now required. If you are not using custom claims, pass an empty map (`serde_json::Map::new()`).\n\n- Updated generate_token Signature:\n  The `generate_token` function now takes `expiration` as a value instead of a reference.\n\n### Pagination Response\n\nPR: [#1197](https://github.com/loco-rs/loco/pull/1197)\n\nThe pagination response now includes the `total_items` field, providing the total number of items available.\n\n```JSON\n{\"results\":[],\"pagination\":{\"page\":0,\"page_size\":0,\"total_pages\":0,\"total_items\":0}}\n```\n\n### Explicit id in migrations\n\nPR: [#1268](https://github.com/loco-rs/loco/pull/1268)\n\nMigrations using `create_table` now require `(\"id\", ColType::PkAuto)`, new migrations will have this field automatically added.\n\n```diff\n  async fn up(&self, m: &SchemaManager) -> Result<(), DbErr> {\n        create_table(m, \"movies\",\n            &[\n+           (\"id\", ColType::PkAuto),\n            (\"title\", ColType::StringNull),\n            ],\n            &[\n            (\"user\", \"\"),\n            ]\n        ).await\n    }\n```\n\n## Upgrade from 0.13.x to 0.14.x\n\n### Upgrading from Axum 0.7 to 0.8\n\nPR: [#1130](https://github.com/loco-rs/loco/pull/1130)\nThe upgrade to Axum 0.8 introduces a breaking change. For more details, refer to the [announcement](https://tokio.rs/blog/2025-01-01-announcing-axum-0-8-0).\n\n#### Steps to Upgrade\n\n- In your `Cargo.toml`, update the Axum version from `0.7.5` to `0.8.1`.\n- Replace use `axum::async_trait`; with use `async_trait::async_trait;`. For more information, see [here](https://tokio.rs/blog/2025-01-01-announcing-axum-0-8-0#async_trait-removal).\n- The URL parameter syntax has changed. Refer to [this section](https://tokio.rs/blog/2025-01-01-announcing-axum-0-8-0#path-parameter-syntax-changes) for the updated syntax. The new path parameter format is:\n  The path parameter syntax has changed from `/:single` and `/*many` to `/{single}` and `/{*many}`.\n\n### Extending the `boot` Function Hook\n\nPR: [#1143](https://github.com/loco-rs/loco/pull/1143)\n\nThe `boot` hook function now accepts an additional Config parameter. The function signature has changed from:\n\nFrom\n\n```rust\nasync fn boot(mode: StartMode, environment: &Environment) -> Result<BootResult> {\n     create_app::<Self, Migrator>(mode, environment).await\n}\n```\n\nTo:\n\n```rust\nasync fn boot(mode: StartMode, environment: &Environment, config: Config) -> Result<BootResult> {\n     create_app::<Self, Migrator>(mode, environment, config).await\n}\n```\n\nMake sure to import the `Config` type as needed.\n\n### Upgrade validator crate\n\nPR: [#993](https://github.com/loco-rs/loco/pull/993)\n\nUpdate the `validator` crate version in your `Cargo.toml`:\n\nFrom\n\n```\nvalidator = { version = \"0.18\" }\n```\n\nTo\n\n```\nvalidator = { version = \"0.19\" }\n```\n\n### Extend truncate and seed hooks\n\nPR: [#1158](https://github.com/loco-rs/loco/pull/1158)\n\nThe `truncate` and `seed` functions now receive `AppContext` instead of `DatabaseConnection` as their argument.\n\nFrom\n\n```rust\nasync fn truncate(db: &DatabaseConnection) -> Result<()> {}\nasync fn seed(db: &DatabaseConnection, base: &Path) -> Result<()> {}\n```\n\nTo\n\n```rust\nasync fn truncate(ctx: &AppContext) -> Result<()> {}\nasync fn seed(_ctx: &AppContext, base: &Path) -> Result<()> {}\n```\n\nImpact on Testing:\n\nTesting code involving the seed function must also be updated accordingly.\n\nfrom:\n\n```rust\nasync fn load_page() {\n    request::<App, _, _>(|request, ctx| async move {\n        seed::<App>(&ctx.db).await.unwrap();\n        ...\n    })\n    .await;\n}\n```\n\nto\n\n```rust\nasync fn load_page() {\n    request::<App, _, _>(|request, ctx| async move {\n        seed::<App>(&ctx).await.unwrap();\n        ...\n    })\n    .await;\n}\n```\n"
  },
  {
    "path": "docs-site/content/docs/extras/websocket.md",
    "content": "+++\ntitle = \"Websocket\"\ndescription = \"\"\ndate = 2024-01-21T18:20:00+00:00\nupdated = 2024-01-21T18:20:00+00:00\ndraft = false\nweight = 2\nsort_by = \"weight\"\ntemplate = \"docs/page.html\"\n\n[extra]\nlead = \"\"\ntoc = true\ntop = false\nflair =[]\n+++\n\n## Chat Room Example\nFor a simple example of a chat room implementation with [socketioxide](https://github.com/Totodore/socketioxide), refer to this [link](https://github.com/loco-rs/chat-rooms).\n"
  },
  {
    "path": "docs-site/content/docs/getting-started/_index.md",
    "content": "+++\ntitle = \"Getting Started\"\ndescription = \"\"\ntemplate = \"docs/section.html\"\nsort_by = \"weight\"\nweight = 1\ndraft = false\n+++\n"
  },
  {
    "path": "docs-site/content/docs/getting-started/axum-users.md",
    "content": "+++\ntitle = \"Axum vs Loco\"\ndescription = \"Shows how to move from Axum to Loco\"\ndate = 2023-12-01T19:30:00+00:00\nupdated = 2023-12-01T19:30:00+00:00\ndraft = false\nweight = 5\nsort_by = \"weight\"\ntemplate = \"docs/page.html\"\n\n[extra]\ntoc = true\ntop = false\nflair =[]\n+++\n\n<div class=\"infobox\">\n<b>NOTE: Loco is based on Axum, it is \"Axum with batteries included\"</b>, and is very easy to move your Axum code to Loco.\n</div>\n\nWe will study [realworld-axum-sqlx](https://github.com/launchbadge/realworld-axum-sqlx) which is an Axum based app, that attempts to describe a real world project, using API, real database, and real world scenarios as well as real world operability requirements such as configuration and logging.\n\nPicking `realworld-axum-sqlx` apart piece by piece **we will show that by moving it from Axum to Loco, most of the code is already written for you**, you get better best practices, better dev experience, integrated testing, code generation, and build apps faster.\n\n**You can use this breakdown** to understand how to move your own Axum based app to Loco as well. For any questions, reach out [in discussions](https://github.com/loco-rs/loco/discussions) or join our [discord by clicking the green invite button](https://github.com/loco-rs/loco)\n\n## `main`\n\nWhen working with Axum, you have to have your own `main` function which sets up every component of your app, gets your routers, adds middleware, sets context, and finally, eventually, goes and sets up a `listen` on a socket.\n\nThis is a lot of manual, error prone work. \n\nIn Loco you:\n\n* Toggle on/off your desired middleware in configuration\n* Use `cargo loco start`, no need for a `main` file at all\n* In production, you get a compiled binary named `your_app` which you run\n\n\n### Moving to Loco\n\n* Set up your required middleware in Loco `config/`\n\n```yaml\nserver:\n  middlewares:\n    limit_payload:\n      body_limit: 5mb\n  # .. more middleware below ..\n```\n\n* Set your serving port in Loco `config/`\n\n```yaml\nserver:\n  port: 5150\n```\n\n### Verdict\n\n* **No code to write**, you don't need to hand-code a main function unless you have to\n* **Best practices off the shelf**, you get a main file best practices uniform, shared across all your Loco apps\n* **Easy to change**, if you want to remove/add middleware to test things out, you can just flip a switch in configuration, no rebuild\n\n\n## Env\n\nThe realworld axum codebase uses [dotenv](https://github.com/launchbadge/realworld-axum-sqlx/blob/main/.env.sample), which needs explicit loading in `main`:\n\n```rust\n dotenv::dotenv().ok();\n```\n\nAnd a `.env` file to be available, maintained and loaded:\n\n```\nDATABASE_URL=postgresql://postgres:{password}@localhost/realworld_axum_sqlx\nHMAC_KEY={random-string}\nRUST_LOG=realworld_axum_sqlx=debug,tower_http=debug\n```\n\nThis is a **sample** file which you get with the project, which you have to manually copy and edit, which is more often than not very error prone.\n\n### Moving to Loco\n\nLoco: use your standard `config/[stage].yaml` configuration, and load specific values from environment using `get_env`\n\n\n```yaml\n# config/development.yaml\n\n# Web server configuration\nserver:\n  # Port on which the server will listen. the server binding is 0.0.0.0:{PORT}\n  port:  {{/* get_env(name=\"NODE_PORT\", default=5150) */}}\n```\n\nThis configuration is strongly typed, contains most-used values like database URL, logger levels and filtering and more. No need to guess or reinvent the wheel.\n\n### Verdict\n\n* **No coding needed**, when moving to Loco you write less code\n* **Less moving parts**, when using Axum only, you have to have configuration in addition to env vars, this is something you get for free with Loco\n\n## Database\n\nUsing Axum only, you typically have to set up your connection, pool, and set it up to be available for your routes, here's the code which you put in your `main.rs` typically:\n\n```rust\n    let db = PgPoolOptions::new()\n        .max_connections(50)\n        .connect(&config.database_url)\n        .await\n        .context(\"could not connect to database_url\")?;\n```\n\nThen you have to hand-wire this connection\n```rust\n .layer(AddExtensionLayer::new(ApiContext {\n                config: Arc::new(config),\n                db,\n            }))\n```\n\n### Moving to Loco\n\nIn Loco you just set your values for the pool in your `config/` folder. We already pick up best effort default values so you don't have to do it, but if you want to, this is how it looks like:\n\n\n```yaml\ndatabase:\n  enable_logging: false\n  connect_timeout: 500\n  idle_timeout: 500\n  min_connections: 1\n  max_connections: 1\n```\n\n### Verdict\n\n* **No code to write** - save yourself the dangers of picking the right values for your db pool, or misconfiguring it\n* **Change is easy** - often you want to try different values under different loads in production, with Axum only, you have to recompile, redeploy. With Loco you can set a config and restart the process.\n\n\n## Logging\n\nAll around your app, you'll have to manually code a logging story. Which do you pick? `tracing` or `slog`? Is it logging or tracing? What is better?\n\nHere's what exists in the real-world-axum project. In serving:\n\n```rust\n  // Enables logging. Use `RUST_LOG=tower_http=debug`\n  .layer(TraceLayer::new_for_http()),\n```\n\nAnd in `main`:\n\n```rust\n    // Initialize the logger.\n    env_logger::init();\n```\n\nAnd ad-hoc logging in various points:\n\n```rust\n  log::error!(\"SQLx error: {:?}\", e);\n```\n\n### Moving to Loco\n\nIn Loco, we've already answered these hard questions and provide multi-tier logging and tracing:\n\n* Inside the framework, internally\n* Configured in the router\n* Low level DB logging and tracing\n* All of Loco's components such as tasks, background jobs, etc. all use the same facility\n\nAnd we picked `tracing` so that any and every Rust library can \"stream\" into your log uniformly. \n\nBut we also made sure to create smart filters so you don't get bombarded with libraries you don't know, by default.\n\nYou can configure your logger in `config/`\n\n```yaml\nlogger:\n  enable: true\n  pretty_backtrace: true\n  level: debug\n  format: compact\n```\n\n### Verdict\n\n* **No code to write** - no set up code, no decision to make. We made the best decision for you so you can write more code for your app.\n* **Build faster** - you get traces for only what you want. You get error backtraces which are colorful, contextual, and with zero noise which makes it easier to debug stuff. You can change formats and levels for production.\n* **Change is easy** - often you want to try different values under different loads in production, with Axum only, you have to recompile, redeploy. With Loco you can set a config and restart the process.\n\n## Routing\n\nMoving routes from Axum to Loco is actually drop-in. Loco uses the native Axum router.\n\nIf you want to have facilities like route listing and information, you can use the native Loco router, which translates to an Axum router, or you can use your own Axum router.\n\n\n### Moving to Loco\n\nIf you want 1:1 complete copy-paste experience, just copy your Axum routes, and plug your router in Loco's `after_routes()` hook:\n\n```rust\n  async fn after_routes(router: AxumRouter, _ctx: &AppContext) -> Result<AxumRouter> {\n      // use AxumRouter to mount your routes and return an AxumRouter\n  }\n\n```\n\nIf you want Loco to understand the metadata information about your routes (which can come in handy later), write your `routes()` function in each of your controllers in this way:\n\n\n```rust\n// this is what people usually do using Axum only\npub fn router() -> Router {\n  Router::new()\n        .route(\"/auth/register\", post(create_user))\n        .route(\"/auth/login\", post(login_user))\n}\n\n// this is how it looks like using Loco (notice we use `Routes` and `add`)\npub fn routes() -> Routes {\n  Routes::new()\n      .add(\"/auth/register\", post(create_user))\n      .add(\"/auth/login\", post(login_user))\n}\n```\n\n### Verdict\n\n* **A drop-in compatibility** - Loco uses Axum and keeps all of its building blocks intact so that you can just use your own existing Axum code with no efforts.\n* **Route metadata for free** - one gap that Axum routers has is the ability to describe the currently configured routes, which can be used for listing or automatic OpenAPI schema generation. Loco has a small metadata layer to support this. If you use `Routes` you get it for free, while all of the different signatures remain compatible with Axum router.\n"
  },
  {
    "path": "docs-site/content/docs/getting-started/guide.md",
    "content": "+++\ntitle = \"The Loco Guide\"\ndate = 2021-05-01T08:00:00+00:00\nupdated = 2021-05-01T08:00:00+00:00\ndraft = false\nweight = 3\nsort_by = \"weight\"\ntemplate = \"docs/page.html\"\n\n[extra]\ntoc = true\ntop = false\nflair =[]\n+++\n\n## Guide Assumptions\n\nThis is a \"long way round\" tutorial. It is long and indepth on purpose, it shows you how to build things manually **and** automatically using generators, so that you learn the skills to build and also how things work.\n\n\n### What's with the name?\n\nThe name `Loco` comes from **loco**motive, as a tribute to Rails, and `loco` is easier to type than `locomotive` :-). Also, in some languages it means \"crazy\" but that was not the original intention (or, is it crazy to build a Rails on Rust? only time will tell!).\n\n### How much Rust do I need to know?\n\nYou need to be familiar with Rust to a beginner but not more than moderate-beginner level. You need to know how to build, test, and run Rust projects, have used some popular libraries such as `clap`, `regex`, `tokio`, `axum` or other web framework, nothing too fancy. There are no crazy lifetime twisters or complex / too magical, macros in Loco that you need to know how they work.\n\n\n### What is Loco?\n\nLoco is strongly inspired by Rails. If you know Rails _and_ Rust, you'll feel at home. If you only know Rails and new to Rust, you'll find Loco refreshing. We do not assume you know Rails.\n\n<div class=\"infobox\">\nWe think Rails is so great, that this guide is strongly inspired from the <a href=\"https://guides.rubyonrails.org/getting_started.html\">Rails guide, too</a>\n</div>\n\nLoco is a Web or API framework for Rust. It's also a productivity suite for developers: it contains everything you need while building a hobby or your next startup. It's also strongly inspired by Rails.\n\n- **You have a variant of the MVC model**, which removes the paradox of option. You deal with building your app, not making academic decisions for what abstractions to use.\n- **Fat models, slim controllers**. Models should contain most of your logic and business implementation, controllers should just be a lightweight router that understands HTTP and moves parameters around.\n- **Command line driven** to keep your momentum and flow. Generate stuff over copying and pasting or coding from scratch.\n- **Every task is \"infrastructure-ready\"**, just plug in your code and wire it in: controllers, models, views, tasks, background jobs, mailers, and more.\n- **Convention over configuration**: decisions are already done for you -- the folder structure matter, configuration shape and values matter, and the way an app is wired matter to how an app operates and for you to be the most effective.\n\n## Creating a New Loco App\n\nYou can follow this guide for a step-by-step \"bottom up\" learning, or you can jump and go with the [tour](@/docs/getting-started/tour/index.md) instead for a quicker \"top down\" intro.\n\n### Installing\n\n<!-- <snip id=\"quick-installation-command\" inject_from=\"yaml\" template=\"sh\"> -->\n```sh\ncargo install loco\ncargo install sea-orm-cli # Only when DB is needed\n```\n<!-- </snip> -->\n\n\n### Creating a new Loco app\n\nNow you can create your new app (choose \"SaaS app\" for built-in authentication).\n\n<!-- <snip id=\"loco-cli-new-from-template\" inject_from=\"yaml\" template=\"sh\"> -->\n```sh\n❯ loco new\n✔ ❯ App name? · myapp\n✔ ❯ What would you like to build? · Saas App with client side rendering\n✔ ❯ Select a DB Provider · Sqlite\n✔ ❯ Select your background worker type · Async (in-process tokio async tasks)\n\n🚂 Loco app generated successfully in:\nmyapp/\n\n- assets: You've selected `clientside` for your asset serving configuration.\n\nNext step, build your frontend:\n  $ cd frontend/\n  $ npm install && npm run build\n```\n<!-- </snip> -->\n\n\n\nHere's a rundown of what Loco creates for you by default:\n\n| File/Folder    | Purpose                                                                                                                                                           |\n| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `src/`         | Contains controllers, models, views, tasks and more                                                                                                               |\n| `app.rs`       | Main component registration point. Wire the important bits here.                                                                                                  |\n| `lib.rs`       | Various rust-specific exports of your components.                                                                                                                 |\n| `bin/`         | Has your `main.rs` file, you don't need to worry about it                                                                                                         |\n| `controllers/` | Contains controllers, all controllers are exported via `mod.rs`                                                                                                   |\n| `models/`      | Contains models, `models/_entities` contains auto-generated SeaORM models, and `models/*.rs` contains your model extension logic, which are exported via `mod.rs` |\n| `views/`       | Contains JSON-based views. Structs which can `serde` and output as JSON through the API.                                                                          |\n| `workers/`     | Has your background workers.                                                                                                                                      |\n| `mailers/`     | Mailer logic and templates, for sending emails.                                                                                                                   |\n| `fixtures/`    | Contains data and automatic fixture loading logic.                                                                                                                |\n| `tasks/`       | Contains your day to day business-oriented tasks such as sending emails, producing business reports, db maintenance, etc.                                         |\n| `tests/`       | Your app-wide tests: models, requests, etc.                                                                                                                       |\n| `config/`      | A stage-based configuration folder: development, test, production                                                                                                 |\n\n## Hello, Loco!\n\nLet's get some responses quickly. For this, we need to start up the server.\n\nYou can now switch to `myapp`:\n\n```sh\n$ cd myapp\n```\n\n### Starting the server\n\n<!-- <snip id=\"starting-the-server-command\" inject_from=\"yaml\" template=\"sh\"> -->\n```sh\ncargo loco start\n```\n<!-- </snip> -->\n\nAnd now, let's see that it's alive:\n\n```sh\n$ curl localhost:5150/_ping\n{\"ok\":true}\n```\n\nThe built in `_ping` route will tell your load balancer everything is up.\n\nLet's see that all services that are required are up:\n\n```sh\n$ curl localhost:5150/_health\n{\"ok\":true}\n```\n\n<div class=\"infobox\">\nThe built in <code>_health</code> route will tell you that you have configured your app properly: it can establish a connection to your Database and Redis instances successfully.\n</div>\n\n### Say \"Hello\", Loco\n\nLet's add a quick _hello_ response to our service.\n\n```sh\n$ cargo loco generate controller guide --api\nadded: \"src/controllers/guide.rs\"\ninjected: \"src/controllers/mod.rs\"\ninjected: \"src/app.rs\"\nadded: \"tests/requests/guide.rs\"\ninjected: \"tests/requests/mod.rs\"\n```\n\nThis is the generated controller body:\n\n```rust\n#![allow(clippy::missing_errors_doc)]\n#![allow(clippy::unnecessary_struct_initialization)]\n#![allow(clippy::unused_async)]\nuse loco_rs::prelude::*;\n\n#[debug_handler]\npub async fn index(State(_ctx): State<AppContext>) -> Result<Response> {\n    format::empty()\n}\n\npub fn routes() -> Routes {\n    Routes::new()\n        .prefix(\"api/guides/\")\n        .add(\"/\", get(index))\n}\n```\n\n\nChange the `index` handler body:\n\n```rust\n// replace\n    format::empty()\n// with this\n    format::text(\"hello\")\n```\n\nStart the server:\n\n<!-- <snip id=\"starting-the-server-command\" inject_from=\"yaml\" template=\"sh\"> -->\n```sh\ncargo loco start\n```\n<!-- </snip> -->\n\nNow, let's test it out:\n\n```sh\n$ curl localhost:5150/api/guides\nhello\n```\n\nLoco has powerful generators, which will make you 10x productive and drive your momentum when building apps.\n\nIf you'd like to be entertained for a moment, let's \"learn the hard way\" and add a new controller manually as well.\n\nAdd a file called `home.rs`, and line `pub mod home;` in `mod.rs`:\n\n```\nsrc/\n  controllers/\n    auth.rs\n    home.rs      <--- add this file\n    users.rs\n    mod.rs       <--- 'pub mod home;' the module here\n```\n\nNext, set up a _hello_ route, this is the contents of `home.rs`:\n\n```rust\n// src/controllers/home.rs\nuse loco_rs::prelude::*;\n\n// _ctx contains your database connection, as well as other app resource that you'll need\nasync fn hello(State(_ctx): State<AppContext>) -> Result<Response> {\n    format::text(\"ola, mundo\")\n}\n\npub fn routes() -> Routes {\n    Routes::new().prefix(\"home\").add(\"/hello\", get(hello))\n}\n```\n\nFinally, register this new controller routes in `app.rs`:\n\n```rust\nsrc/\n  controllers/\n  models/\n  ..\n  app.rs   <---- look here\n```\n\nAdd the following in `routes()`:\n\n```rust\n// in src/app.rs\n#[async_trait]\nimpl Hooks for App {\n    ..\n    fn routes() -> AppRoutes {\n        AppRoutes::with_default_routes()\n            .add_route(controllers::guide::routes())\n            .add_route(controllers::auth::routes())\n            .add_route(controllers::home::routes()) // <--- add this\n    }\n```\n\nThat's it. Kill the server and bring it up again:\n\n<!-- <snip id=\"starting-the-server-command\" inject_from=\"yaml\" template=\"sh\"> -->\n```sh\ncargo loco start\n```\n<!-- </snip> -->\n\nAnd hit `/home/hello`:\n\n```sh\n$ curl localhost:5150/home/hello\nola, mundo\n```\n\nYou can take a look at all of your routes with:\n\n```\n$ cargo loco routes\n  ..\n  ..\n[POST] /api/auth/login\n[POST] /api/auth/register\n[POST] /api/auth/reset\n[POST] /api/auth/verify\n[GET] /home/hello      <---- this is our new route!\n  ..\n  ..\n$\n```\n\n<div class=\"infobox\">\nThe <em>SaaS Starter</em> keeps routes under <code>/api</code> because it is client-side ready and we are using the <code>--api</code> option in scaffolding. <br/>\nWhen using client-side routing like React Router, we want to separate backend routes from client routes: the browser will use <code>/home</code> but not <code>/api/home</code> which is the backend route, and you can call <code>/api/home</code> from the client with no worries. Nevertheless, the routes: <code>/_health</code> and <code>/_ping</code> are exceptions, they stay at the root.\n</div>\n\n## MVC and You\n\n**Traditional MVC (Model-View-Controller) originated in desktop UI programming paradigms.** However, its applicability to web services led to its rapid adoption. MVC's golden era was around the early 2010s, and since then, many other paradigms and architectures have emerged.\n\n**MVC is still a very strong principle and architecture to follow for simplifying projects**, and this is what Loco follows too.\n\nAlthough web services and APIs don't have a concept of a _view_ because they do not generate HTML or UI responses, **we claim _stable_, _safe_ services and APIs indeed has a notion of a view** -- and that is the serialized data, its shape, its compatibility and its version.\n\n```\n// a typical loco app contains all parts of MVC\n\nsrc/\n  controllers/\n    users.rs\n    mod.rs\n  models/\n    _entities/\n      users.rs\n      mod.rs\n    users.rs\n    mod.rs\n  views/\n    users.rs\n    mod.rs\n```\n\n**This is an important _cognitive_ principle**. And the principle claims that you can only create safe, compatible API responses if you treat those as a separate, independently governed _thing_ -- hence the 'V' in MVC, in Loco.\n\n<div class=\"infobox\">\nModels in Loco carry the same semantics as in Rails: <b>fat models, slim controllers</b>. This means that every time you want to build something -- <em>you reach out to a model</em>.\n</div>\n\n### Generating a model\n\nA model in Loco represents data *and* functionality. Typically the data is stored in your database. Most, if not all, business processes of your applications would be coded on the model (as an Active Record) or as an orchestration of a few models.\n\nLet's create a new model called `Article`:\n\n```sh\n$ cargo loco generate model article title:string content:text\n\nadded: \"migration/src/m20231202_173012_articles.rs\"\ninjected: \"migration/src/lib.rs\"\ninjected: \"migration/src/lib.rs\"\nadded: \"tests/models/articles.rs\"\ninjected: \"tests/models/mod.rs\"\n```\n\n### Database migrations\n\n**Keeping your schema honest is done with migrations**. A migration is a singular change to your database structure: it can contain complete table additions, modifications, or index creation.\n\n```rust\n// this was generated into `migrations/` from the command:\n//\n// $ cargo loco generate model article title:string content:text\n//\n// it is automatically applied by Loco's migrator framework.\n// you can also apply it manually using the command:\n//\n// $ cargo loco db migrate\n//\n#[async_trait::async_trait]\nimpl MigrationTrait for Migration {\n    async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {\n        manager\n            .create_table(\n                table_auto_tz(Articles::Table)\n                    .col(pk_auto(Articles::Id))\n                    .col(string_null(Articles::Title))\n                    .col(text(Articles::Content))\n                    .to_owned(),\n            )\n            .await\n    }\n\n    async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {\n        manager\n            .drop_table(Table::drop().table(Articles::Table).to_owned())\n            .await\n    }\n}\n```\n\nYou can recreate a complete database **by applying migrations in-series onto a fresh database** -- this is done automatically by Loco's migrator (which is derived from SeaORM).\n\nWhen generating a new model, Loco will:\n\n- Generate a new \"up\" database migration\n- Apply the migration\n- Reflect the entities from database structure and generate back your `_entities` code\n\nYou will find your new model as an entity, synchronized from your database structure in `models/_entities/`:\n\n```\nsrc/models/\n├── _entities\n│   ├── articles.rs  <-- sync'd from db schema, do not edit\n│   ├── mod.rs\n│   ├── prelude.rs\n│   └── users.rs\n├── articles.rs   <-- generated for you, your logic goes here.\n├── mod.rs\n└── users.rs\n```\n\n### Using `playground` to interact with the database\n\nYour `examples/` folder contains:\n\n- `playground.rs` - a place to try out and experiment with your models and app logic.\n\nLet's fetch data using your models, using `playground.rs`:\n\n```rust\n// located in examples/playground.rs\n// use this file to experiment with stuff\nuse loco_rs::{cli::playground, prelude::*};\n// to refer to articles::ActiveModel, your imports should look like this:\nuse myapp::{app::App, models::_entities::articles};\n\n#[tokio::main]\nasync fn main() -> loco_rs::Result<()> {\n    let ctx = playground::<App>().await?;\n\n    // add this:\n    let res = articles::Entity::find().all(&ctx.db).await.unwrap();\n    println!(\"{:?}\", res);\n\n    Ok(())\n}\n\n```\n\n### Return a list of posts\n\nIn the example, we use the following to return a list:\n\n```rust\nlet res = articles::Entity::find().all(&ctx.db).await.unwrap();\n```\n\nTo see how to run more queries, go to the [SeaORM docs](https://www.sea-ql.org/SeaORM/docs/next/basic-crud/select/).\n\nTo execute your playground, run:\n\n```rust\n$ cargo playground\n[]\n```\n\nNow, let's insert one item:\n\n```rust\nasync fn main() -> loco_rs::Result<()> {\n    let ctx = playground::<App>().await?;\n\n    // add this:\n    let active_model: articles::ActiveModel = articles::ActiveModel {\n        title: Set(Some(\"how to build apps in 3 steps\".to_string())),\n        content: Set(Some(\"use Loco: https://loco.rs\".to_string())),\n        ..Default::default()\n    };\n    active_model.insert(&ctx.db).await.unwrap();\n\n    let res = articles::Entity::find().all(&ctx.db).await.unwrap();\n    println!(\"{:?}\", res);\n\n    Ok(())\n}\n```\n\nAnd run the playground again:\n\n```sh\n$ cargo playground\n[Model { created_at: ..., updated_at: ..., id: 1, title: Some(\"how to build apps in 3 steps\"), content: Some(\"use Loco: https://loco.rs\") }]\n```\n\nWe're now ready to plug this into an `articles` controller. First, generate a new controller:\n\n```sh\n$ cargo loco generate controller articles --api\nadded: \"src/controllers/articles.rs\"\ninjected: \"src/controllers/mod.rs\"\ninjected: \"src/app.rs\"\nadded: \"tests/requests/articles.rs\"\ninjected: \"tests/requests/mod.rs\"\n```\n\nEdit `src/controllers/articles.rs`:\n\n```rust\n#![allow(clippy::unused_async)]\nuse loco_rs::prelude::*;\n\nuse crate::models::_entities::articles;\n\npub async fn list(State(ctx): State<AppContext>) -> Result<Response> {\n    let res = articles::Entity::find().all(&ctx.db).await?;\n    format::json(res)\n}\n\npub fn routes() -> Routes {\n    Routes::new().prefix(\"api/articles\").add(\"/\", get(list))\n}\n```\n\nNow, start the app:\n\n<!-- <snip id=\"starting-the-server-command\" inject_from=\"yaml\" template=\"sh\"> -->\n```sh\ncargo loco start\n```\n<!-- </snip> -->\n\nAnd make a request:\n\n```sh\n$ curl localhost:5150/api/articles\n[{\"created_at\":\"...\",\"updated_at\":\"...\",\"id\":1,\"title\":\"how to build apps in 3 steps\",\"content\":\"use Loco: https://loco.rs\"}]\n```\n\n## Building a CRUD API\n\nNext we'll see how to get a single article, delete, and edit a single article. Getting an article by ID is done using the `Path` extractor from `axum`.\n\nReplace the contents of `articles.rs` with this:\n\n```rust\n// this is src/controllers/articles.rs\n\n#![allow(clippy::unused_async)]\nuse loco_rs::prelude::*;\nuse serde::{Deserialize, Serialize};\n\nuse crate::models::_entities::articles::{ActiveModel, Entity, Model};\n\n#[derive(Clone, Debug, Serialize, Deserialize)]\npub struct Params {\n    pub title: Option<String>,\n    pub content: Option<String>,\n}\n\nimpl Params {\n    fn update(&self, item: &mut ActiveModel) {\n        item.title = Set(self.title.clone());\n        item.content = Set(self.content.clone());\n    }\n}\n\nasync fn load_item(ctx: &AppContext, id: i32) -> Result<Model> {\n    let item = Entity::find_by_id(id).one(&ctx.db).await?;\n    item.ok_or_else(|| Error::NotFound)\n}\n\npub async fn list(State(ctx): State<AppContext>) -> Result<Response> {\n    format::json(Entity::find().all(&ctx.db).await?)\n}\n\npub async fn add(State(ctx): State<AppContext>, Json(params): Json<Params>) -> Result<Response> {\n    let mut item: ActiveModel = Default::default();\n    params.update(&mut item);\n    let item = item.insert(&ctx.db).await?;\n    format::json(item)\n}\n\npub async fn update(\n    Path(id): Path<i32>,\n    State(ctx): State<AppContext>,\n    Json(params): Json<Params>,\n) -> Result<Response> {\n    let item = load_item(&ctx, id).await?;\n    let mut item = item.into_active_model();\n    params.update(&mut item);\n    let item = item.update(&ctx.db).await?;\n    format::json(item)\n}\n\npub async fn remove(Path(id): Path<i32>, State(ctx): State<AppContext>) -> Result<Response> {\n    load_item(&ctx, id).await?.delete(&ctx.db).await?;\n    format::empty()\n}\n\npub async fn get_one(Path(id): Path<i32>, State(ctx): State<AppContext>) -> Result<Response> {\n    format::json(load_item(&ctx, id).await?)\n}\n\npub fn routes() -> Routes {\n    Routes::new()\n        .prefix(\"api/articles\")\n        .add(\"/\", get(list))\n        .add(\"/\", post(add))\n        .add(\"/{id}\", get(get_one))\n        .add(\"/{id}\", delete(remove))\n        .add(\"/{id}\", patch(update))\n}\n```\n\nA few items to note:\n\n- `Params` is a strongly typed required params data holder, and is similar in concept to Rails' _strongparams_, just safer.\n- `Path(id): Path<i32>` extracts the `:id` component from a URL.\n- Order of extractors is important and follows `axum`'s documentation (parameters, state, body).\n- It's always better to create a `load_item` helper function and use it in all singular-item routes.\n- While `use loco_rs::prelude::*` brings in anything you need to build a controller, you should note to import `crate::models::_entities::articles::{ActiveModel, Entity, Model}` as well as `Serialize, Deserialize` for params.\n\n\n<div class=\"infobox\">\nThe order of the extractors is important, as changing the order of them can lead to compilation errors. Adding the <code>#[debug_handler]</code> macro to handlers can help by printing out better error messages. More information about extractors can be found in the <a href=\"https://docs.rs/axum/latest/axum/extract/index.html#the-order-of-extractors\">axum documentation</a>.\n</div>\n\n\nYou can now test that it works, start the app:\n\n<!-- <snip id=\"starting-the-server-command\" inject_from=\"yaml\" template=\"sh\"> -->\n```sh\ncargo loco start\n```\n<!-- </snip> -->\n\nAdd a new article:\n\n```sh\n$ curl -X POST -H \"Content-Type: application/json\" -d '{\n  \"title\": \"Your Title\",\n  \"content\": \"Your Content xxx\"\n}' localhost:5150/api/articles\n{\"created_at\":\"...\",\"updated_at\":\"...\",\"id\":2,\"title\":\"Your Title\",\"content\":\"Your Content xxx\"}\n```\n\nGet a list:\n\n```sh\n$ curl localhost:5150/api/articles\n[{\"created_at\":\"...\",\"updated_at\":\"...\",\"id\":1,\"title\":\"how to build apps in 3 steps\",\"content\":\"use Loco: https://loco.rs\"},{\"created_at\":\"...\",\"updated_at\":\"...\",\"id\":2,\"title\":\"Your Title\",\"content\":\"Your Content xxx\"}\n```\n\n### Adding a second model\n\nLet's add another model, this time: `Comment`. We want to create a relation - a comment belongs to a post, and each post can have multiple comments.\n\nInstead of coding the model and controller by hand, we're going to create a **comment scaffold** which will generate a fully working CRUD API comments. We're also going to use the special `references` type:\n\n```sh\n$ cargo loco generate scaffold comment content:text article:references --api\n```\n\n<div class=\"infobox\">\nThe special <code>&lt;other_model&gt;:references:&lt;column_name&gt;</code> is also available. For when you want to have a different name for your column.\n</div>\n\nIf you peek into the new migration, you'll discover a new database relation in the articles table:\n\n```rust\n      ..\n      ..\n  .col(integer(Comments::ArticleId))\n  .foreign_key(\n      ForeignKey::create()\n          .name(\"fk-comments-articles\")\n          .from(Comments::Table, Comments::ArticleId)\n          .to(Articles::Table, Articles::Id)\n          .on_delete(ForeignKeyAction::Cascade)\n          .on_update(ForeignKeyAction::Cascade),\n  )\n      ..\n      ..\n```\n\n\nNow, lets modify our API in the following way:\n\n1. Comments can be added through a shallow route: `POST comments/`\n2. Comments can only be fetched in a nested route (forces a Post to exist): `GET posts/1/comments`\n3. Comments cannot be updated, fetched singular, or deleted\n\nIn `src/controllers/comments.rs`, remove unneeded routes and functions:\n\n```rust\npub fn routes() -> Routes {\n    Routes::new()\n        .prefix(\"api/comments\")\n        .add(\"/\", post(add))\n        // .add(\"/\", get(list))\n        // .add(\"/{id}\", get(get_one))\n        // .add(\"/{id}\", delete(remove))\n        // .add(\"/{id}\", patch(update))\n}\n```\n\nAlso adjust the Params & update functions in `src/controllers/comments.rs`, by updating the scaffolded code marked with `<- add this`\n\n```rust\npub struct Params {\n    pub content: Option<String>,\n    pub article_id: i32, // <- add this\n}\n\nimpl Params {\n    fn update(&self, item: &mut ActiveModel) {\n        item.content = Set(self.content.clone());\n        item.article_id = Set(self.article_id.clone()); // <- add this\n    }\n}\n```\n\nNow we need to fetch a relation in `src/controllers/articles.rs`. Add the following route:\n\n```rust\npub fn routes() -> Routes {\n  // ..\n  // ..\n  .add(\"/{id}/comments\", get(comments))\n}\n```\n\nAnd implement the relation fetching:\n\n```rust\n// to refer to comments::Entity, your imports should look like this:\nuse crate::models::_entities::{\n    articles::{ActiveModel, Entity, Model},\n    comments,\n};\n\npub async fn comments(\n    Path(id): Path<i32>,\n    State(ctx): State<AppContext>,\n) -> Result<Response> {\n    let item = load_item(&ctx, id).await?;\n    let comments = item.find_related(comments::Entity).all(&ctx.db).await?;\n    format::json(comments)\n}\n```\n\n<div class=\"infobox\">\nThis is called \"lazy loading\", where we fetch the item first and later its associated relation. Don't worry - there is also a way to eagerly load comments along with an article.\n</div>\n\nNow start the app again:\n\n<!-- <snip id=\"starting-the-server-command\" inject_from=\"yaml\" template=\"sh\"> -->\n```sh\ncargo loco start\n```\n<!-- </snip> -->\n\nAdd a comment to Article `1`:\n\n```sh\n$ curl -X POST -H \"Content-Type: application/json\" -d '{\n  \"content\": \"this rocks\",\n  \"article_id\": 1\n}' localhost:5150/api/comments\n{\"created_at\":\"...\",\"updated_at\":\"...\",\"id\":4,\"content\":\"this rocks\",\"article_id\":1}\n```\n\nAnd, fetch the relation:\n\n```sh\n$ curl localhost:5150/api/articles/1/comments\n[{\"created_at\":\"...\",\"updated_at\":\"...\",\"id\":4,\"content\":\"this rocks\",\"article_id\":1}]\n```\n\nThis ends our comprehensive _Guide to Loco_. If you made it this far, hurray!.\n\n## Tasks: export data report\n\nReal world apps require handling real world situations. Say some of your users or customers require some kind of a report.\n\nYou can:\n\n- Connect to your production database, issue ad-hoc SQL queries. Or use some kind of DB tool. _This is unsafe, insecure, prone to errors, and cannot be automated_.\n- Export your data to something like Redshift, or Google, and issue a query there. _This is a waste of resource, insecure, cannot be tested properly, and slow_.\n- Build an admin. _This is time-consuming, and waste_.\n- **Or build an adhoc task in Rust, which is quick to write, type safe, guarded by the compiler, fast, environment-aware, testable, and secure.**\n\nThis is where `cargo loco task` comes in.\n\nFirst, run `cargo loco task` to see current tasks:\n\n```sh\n$ cargo loco task\nseed_data\t\t[Task for seeding data]\n```\n\nGenerate a new task `user_report`\n\n```sh\n$ cargo loco generate task user_report\n\nadded: \"src/tasks/user_report.rs\"\ninjected: \"src/tasks/mod.rs\"\ninjected: \"src/app.rs\"\nadded: \"tests/tasks/user_report.rs\"\ninjected: \"tests/tasks/mod.rs\"\n```\n\nIn `src/tasks/user_report.rs` you'll see the task that was generated for you. Replace it with following:\n\n```rust\n// find it in `src/tasks/user_report.rs`\n\nuse loco_rs::prelude::*;\nuse loco_rs::task::Vars;\n\nuse crate::models::users;\n\npub struct UserReport;\n\n#[async_trait]\nimpl Task for UserReport {\n    fn task(&self) -> TaskInfo {\n      // description that appears on the CLI\n        TaskInfo {\n            name: \"user_report\".to_string(),\n            detail: \"output a user report\".to_string(),\n        }\n    }\n\n    // variables through the CLI:\n    // `$ cargo loco task name:foobar count:2`\n    // will appear as {\"name\":\"foobar\", \"count\":2} in `vars`\n    async fn run(&self, app_context: &AppContext, vars: &Vars) -> Result<()> {\n        let users = users::Entity::find().all(&app_context.db).await?;\n        println!(\"args: {vars:?}\");\n        println!(\"!!! user_report: listing users !!!\");\n        println!(\"------------------------\");\n        for user in &users {\n            println!(\"user: {}\", user.email);\n        }\n        println!(\"done: {} users\", users.len());\n        Ok(())\n    }\n}\n```\n\nYou can modify this task as you see fit. Access the models with `app_context`, or any other environmental resources, and fetch\nvariables that were given through the CLI with `vars`.\n\nRunning this task is done with:\n\n```rust\n$ cargo loco task user_report var1:val1 var2:val2 ...\n\nargs: Vars { cli: {\"var1\": \"val1\", \"var2\": \"val2\"} }\n!!! user_report: listing users !!!\n------------------------\ndone: 0 users\n```\nIf you have not added a user before, the report will be empty.\n\nTo add a user check out chapter [Registering a New User](/docs/getting-started/tour/#registering-a-new-user) of [A Quick Tour with Loco](/docs/getting-started/tour/).\n\nRemember: this is environmental, so you write the task once, and then execute in development or production as you wish. Tasks are compiled into the main app binary.\n\n## Authentication: authenticating your requests\n\nIf you chose the `SaaS App` starter, you should have a fully configured authentication module baked into the app.\nLet's see how to require authentication when **adding comments**.\n\nGo back to `src/controllers/comments.rs` and take a look at the `add` function:\n\n```rust\npub async fn add(State(ctx): State<AppContext>, Json(params): Json<Params>) -> Result<Response> {\n    let mut item: ActiveModel = Default::default();\n    params.update(&mut item);\n    let item = item.insert(&ctx.db).await?;\n    format::json(item)\n}\n```\n\nTo require authentication, we need to modify the function signature in this way:\n\n```rust\nasync fn add(\n    auth: auth::JWT,\n    State(ctx): State<AppContext>,\n    Json(params): Json<Params>,\n) -> Result<Response> {\n    // we only want to make sure it exists\n    let _current_user = crate::models::users::Model::find_by_pid(&ctx.db, &auth.claims.pid).await?;\n\n    // next, update\n    // homework/bonus: make a comment _actually_ belong to user (user_id)\n    let mut item: ActiveModel = Default::default();\n    params.update(&mut item);\n    let item = item.insert(&ctx.db).await?;\n    format::json(item)\n}\n```\n"
  },
  {
    "path": "docs-site/content/docs/getting-started/starters.md",
    "content": "+++\ntitle = \"Starters\"\ndate = 2021-12-19T08:00:00+00:00\nupdated = 2021-12-19T08:00:00+00:00\ndraft = false\nweight = 4\nsort_by = \"weight\"\ntemplate = \"docs/page.html\"\n\n[extra]\ntoc = true\ntop = false\nflair =[]\n+++\n\nSimplify your project setup with Loco's predefined boilerplates, designed to make your development journey smoother. To get started, install our CLI and choose the template that suits your needs.\n\n<!-- <snip id=\"quick-installation-command\" inject_from=\"yaml\" template=\"sh\"> -->\n```sh\ncargo install loco\ncargo install sea-orm-cli # Only when DB is needed\n```\n<!-- </snip> -->\n\nCreate a starter:\n\n<!-- <snip id=\"loco-cli-new-from-template\" inject_from=\"yaml\" template=\"sh\"> -->\n```sh\n❯ loco new\n✔ ❯ App name? · myapp\n✔ ❯ What would you like to build? · Saas App with client side rendering\n✔ ❯ Select a DB Provider · Sqlite\n✔ ❯ Select your background worker type · Async (in-process tokio async tasks)\n\n🚂 Loco app generated successfully in:\nmyapp/\n\n- assets: You've selected `clientside` for your asset serving configuration.\n\nNext step, build your frontend:\n  $ cd frontend/\n  $ npm install && npm run build\n```\n<!-- </snip> -->\n\n## Available Starters\n\n### Command Line Options\n\nPrint the command line options:\n\n```console\n$ loco new --help\nCreate a new Loco app\n\nUsage: loco[EXE] new [OPTIONS]\n\nOptions:\n  -p, --path <PATH>          Local path to generate into [default: .]\n  -v, --verbose <VERBOSE>    Verbosity level [default: ERROR]\n  -n, --name <NAME>          App name\n  -t, --template <TEMPLATE>  Starter template\n      --db <DB>              DB Provider [possible values: sqlite, postgres]\n      --bg <BG>              Background worker configuration [possible values: async, queue, blocking]\n      --assets <ASSETS>      Assets serving configuration [possible values: serverside, clientside]\n  -h, --help                 Print help\n  -V, --version              Print version\n```\n\nExample starter with a SQLite database, async background worker, and server side assets:\n\n```sh\nloco new --db sqlite --bg async --assets serverside\n```\n\n### SaaS Starter\n\nThe SaaS starter is an all-included set up for projects requiring both a UI and a REST API. For the UI this starter supports a client-side app or classic server-side templates (or a combination).\n\n**UI**\n\n- Frontend starter built on React and Vite (easy to replace with your preferred framework).\n- Static middleware that point on your frontend build and includes a fallback index. Alternatively you can configure it for static assets for server-side templates.\n- The Tera view engine configured for server-side templates, including i18n configuration. Templates and i18n assets live in `assets/`.\n\n**Rest API**\n\n- `_ping`, `_health` and `_readiness` endpoints to check service health. See all endpoint with the following command `cargo loco routes`\n- Users table and authentication middleware.\n- User model with authentication logic and user registration.\n- Forgot password API flow.\n- Mailer that sends welcome emails and handles forgot password requests.\n\n#### Configuring assets for serverside templates\n\nThe SaaS starter comes preconfigured for frontend client-side assets. If you want to use server-side template rendering which includes assets such as pictures and styles, you can configure the asset middleware for it:\n\nIn your `config/development.yaml`, uncomment the server-side config, and comment the client-side config.\n\n```yaml\n    # server-side static assets config\n    # for use with the view_engine in initializers/view_engine.rs\n    #\n    static:\n      enable: true\n      must_exist: true\n      precompressed: false\n      folder:\n        uri: \"/static\"\n        path: \"assets/static\"\n      fallback: \"assets/static/404.html\"\n    fallback:\n      enable: false\n    # client side app static config\n    # static:\n    #   enable: true\n    #   must_exist: true\n    #   precompressed: false\n    #   folder:\n    #     uri: \"/\"\n    #     path: \"frontend/dist\"\n    #   fallback: \"frontend/dist/index.html\"\n    # fallback:\n    #   enable: false\n```\n\n\n### Rest API Starter\n\nChoose the Rest API starter if you only need a REST API without a frontend. If you change your mind later and decide to serve a frontend, simply enable the `static` middleware and point the configuration to your `frontend` distribution folder.\n\n### Lightweight Service Starter\n\nFocused on controllers and views (response schema), the Lightweight Service starter is minimalistic. If you require a REST API service without a database, frontend, workers, or other features that Loco provides, this is the ideal choice for you!\n"
  },
  {
    "path": "docs-site/content/docs/getting-started/tour/index.md",
    "content": "+++\ntitle = \"A Quick Tour\"\ndate = 2021-05-01T08:00:00+00:00\nupdated = 2021-05-01T08:00:00+00:00\ndraft = false\nweight = 2\nsort_by = \"weight\"\ntemplate = \"docs/page.html\"\n\n[extra]\ntoc = true\ntop = false\nflair =[]\n+++\n\n\n<img style=\"width:100%; max-width:640px\" src=\"tour.png\"/>\n<br/>\n<br/>\n<br/>\nLet's create a blog backend on Loco in just a few minutes. First install `loco` and `sea-orm-cli`:\n\n<!-- <snip id=\"quick-installation-command\" inject_from=\"yaml\" template=\"sh\"> -->\n```sh\ncargo install loco\ncargo install sea-orm-cli # Only when DB is needed\n```\n<!-- </snip> -->\n\n\n Now you can create your new app (choose \"`SaaS` app\"). Select SaaS app with client side rendering:\n\n<!-- <snip id=\"loco-cli-new-from-template\" inject_from=\"yaml\" template=\"sh\"> -->\n```sh\n❯ loco new\n✔ ❯ App name? · myapp\n✔ ❯ What would you like to build? · Saas App with client side rendering\n✔ ❯ Select a DB Provider · Sqlite\n✔ ❯ Select your background worker type · Async (in-process tokio async tasks)\n\n🚂 Loco app generated successfully in:\nmyapp/\n\n- assets: You've selected `clientside` for your asset serving configuration.\n\nNext step, build your frontend:\n  $ cd frontend/\n  $ npm install && npm run build\n```\n<!-- </snip> -->\n\nYou'll have:\n\n* `sqlite` for database. Learn about database providers in [Sqlite vs Postgres](@/docs/the-app/models.md#sqlite-vs-postgres) in the _models_ section.\n* `async` for background workers. Learn about workers configuration [async vs queue](@/docs/processing/workers.md#async-vs-queue) in the _workers_ section.\n* client-side asset serving configuration. This means your backend will serve as API and will also serve your static client-side content.\n\n\nNow `cd` into your `myapp` and start your app by running `cargo loco start`:\n \n \n <div class=\"infobox\">\n If you have the client-side asset serving option configured, make sure you build your frontend before starting the server. This can be done by changing into the frontend directory (`cd frontend`) and running `pnpm install` and `pnpm build`.\n </div>\n\n<!-- <snip id=\"starting-the-server-command-with-output\" inject_from=\"yaml\" template=\"sh\"> -->\n```sh\n$ cargo loco start\n\n                      ▄     ▀\n                                ▀  ▄\n                  ▄       ▀     ▄  ▄ ▄▀\n                                    ▄ ▀▄▄\n                        ▄     ▀    ▀  ▀▄▀█▄\n                                          ▀█▄\n▄▄▄▄▄▄▄  ▄▄▄▄▄▄▄▄▄   ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄ ▀▀█\n██████  █████   ███ █████   ███ █████   ███ ▀█\n██████  █████   ███ █████   ▀▀▀ █████   ███ ▄█▄\n██████  █████   ███ █████       █████   ███ ████▄\n██████  █████   ███ █████   ▄▄▄ █████   ███ █████\n██████  █████   ███  ████   ███ █████   ███ ████▀\n  ▀▀▀██▄ ▀▀▀▀▀▀▀▀▀▀  ▀▀▀▀▀▀▀▀▀▀  ▀▀▀▀▀▀▀▀▀▀ ██▀\n      ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀\n                https://loco.rs\n\nlistening on port 5150\n```\n<!-- </snip> -->\n\n\n<div class=\"infobox\">\nYou don't have to run things through `cargo` but in development it's highly\nrecommended. If you build `--release`, your binary contains everything\nincluding your code and `cargo` or Rust is not needed. \n</div>\n\n## Adding a CRUD API\n\nWe have a base SaaS app with user authentication generated for us. Let's make it a blog backend by adding a `post` and a full CRUD API using `scaffold`:\n\n<div class=\"infobox\">\nYou can choose between generating an `api`, `html` or `htmx` scaffold using the respective `--api`, `--html`, and `--htmx` flags.\n</div>\n\nBecause we're building a backend with a client-side codebase for the client, we'll build an API using `--api`:\n\n```sh\n$ cargo loco generate scaffold post title:string content:text --api\n\n  :\n  :\nadded: \"src/controllers/post.rs\"\ninjected: \"src/controllers/mod.rs\"\ninjected: \"src/app.rs\"\nadded: \"tests/requests/post.rs\"\ninjected: \"tests/requests/mod.rs\"\n* Migration for `post` added! You can now apply it with `$ cargo loco db migrate`.\n* A test for model `posts` was added. Run with `cargo test`.\n* Controller `post` was added successfully.\n* Tests for controller `post` was added successfully. Run `cargo test`.\n```\n\nYour database have been migrated and model, entities, and a full CRUD controller have been generated automatically.\n\nStart your app again:\n<!-- <snip id=\"starting-the-server-command-with-output\" inject_from=\"yaml\" template=\"sh\"> -->\n```sh\n$ cargo loco start\n\n                      ▄     ▀\n                                ▀  ▄\n                  ▄       ▀     ▄  ▄ ▄▀\n                                    ▄ ▀▄▄\n                        ▄     ▀    ▀  ▀▄▀█▄\n                                          ▀█▄\n▄▄▄▄▄▄▄  ▄▄▄▄▄▄▄▄▄   ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄ ▀▀█\n██████  █████   ███ █████   ███ █████   ███ ▀█\n██████  █████   ███ █████   ▀▀▀ █████   ███ ▄█▄\n██████  █████   ███ █████       █████   ███ ████▄\n██████  █████   ███ █████   ▄▄▄ █████   ███ █████\n██████  █████   ███  ████   ███ █████   ███ ████▀\n  ▀▀▀██▄ ▀▀▀▀▀▀▀▀▀▀  ▀▀▀▀▀▀▀▀▀▀  ▀▀▀▀▀▀▀▀▀▀ ██▀\n      ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀\n                https://loco.rs\n\nlistening on port 5150\n```\n<!-- </snip> -->\n\n<div class=\"infobox\"> \nDepending on which scaffold template option you chose (`--api`, `--html`, `--htmx`), the steps for creating a scaffolded resource will change. With the `--api` flag or the `--htmx` flag you can use the below example. But with the `--html` flag, it is recommended you do the post creation steps in your browser.\n  \nIf you want to use `curl` to test the `--html` scaffold, you will need to send your requests with the Content-Type `application/x-www-form-urlencoded` and the body as `title=Your+Title&content=Your+Content` by default. This can be changed to allow `application/json` as a `Content-Type` in the code if desired.\n</div>\n\nNext, try adding a `post` with `curl`:\n\n```sh\n$ curl -X POST -H \"Content-Type: application/json\" -d '{\n  \"title\": \"Your Title\",\n  \"content\": \"Your Content xxx\"\n}' localhost:5150/api/posts\n```\n\nYou can list your posts:\n\n```sh\n$ curl localhost:5150/api/posts\n```\n\nFor those counting -- the commands for creating a blog backend were:\n\n1. `cargo install loco`\n2. `cargo install sea-orm-cli`\n3. `loco new`\n4. `cargo loco generate scaffold post title:string content:text --api`\n\nDone! enjoy your ride with `loco` 🚂\n\n## Checking Out SaaS Authentication\n\nYour generated app contains a fully working authentication suite, based on JWTs.\n\n### Registering a New User\n\nThe `/api/auth/register` endpoint creates a new user in the database with an `email_verification_token` for account verification. A welcome email is sent to the user with a verification link.\n\n```sh\n$ curl --location 'localhost:5150/api/auth/register' \\\n     --header 'Content-Type: application/json' \\\n     --data-raw '{\n         \"name\": \"Loco user\",\n         \"email\": \"user@loco.rs\",\n         \"password\": \"12341234\"\n     }'\n```\n\nFor security reasons, if the user is already registered, no new user is created, and a 200 status is returned without exposing user email details.\n\n### Login\n\nAfter registering a new user, use the following request to log in:\n\n```sh\n$ curl --location 'localhost:5150/api/auth/login' \\\n     --header 'Content-Type: application/json' \\\n     --data-raw '{\n         \"email\": \"user@loco.rs\",\n         \"password\": \"12341234\"\n     }'\n```\n\nThe response includes a JWT token for authentication, user ID, name, and verification status.\n\n```sh\n{\n    \"token\": \"...\",\n    \"pid\": \"2b20f998-b11e-4aeb-96d7-beca7671abda\",\n    \"name\": \"Loco user\",\n    \"claims\": null\n    \"is_verified\": false\n}\n```\n\nIn your client-side app, you save this JWT token and make following requests with it using _bearer token_ (see below) in order for those to be authenticated.\n\n### Get current user\n\nThis endpoint is protected by auth middleware. We will use the token we got earlier to perform a request with the _bearer token_ technique (replace `TOKEN` with the JWT token you got earlier):\n\n```sh\n$ curl --location --request GET 'localhost:5150/api/auth/current' \\\n     --header 'Content-Type: application/json' \\\n     --header 'Authorization: Bearer TOKEN'\n```\n\nThat should be your first authenticated request!.\n\nCheck out the source code for `controllers/auth.rs` to see how to use the authentication middleware in your own controllers.\n"
  },
  {
    "path": "docs-site/content/docs/infrastructure/_index.md",
    "content": "+++\ntitle = \"Infrastructure\"\ndescription = \"\"\ntemplate = \"docs/section.html\"\nsort_by = \"weight\"\nweight = 4\ndraft = false\n+++\n"
  },
  {
    "path": "docs-site/content/docs/infrastructure/cache.md",
    "content": "+++\ntitle = \"Cache\"\ndescription = \"\"\ndate = 2024-02-07T08:00:00+00:00\nupdated = 2025-04-22T08:00:00+00:00\ndraft = false\nweight = 2\nsort_by = \"weight\"\ntemplate = \"docs/page.html\"\n\n[extra]\nlead = \"\"\ntoc = true\ntop = false\nflair =[]\n+++\n\n`Loco` provides a cache layer to improve application performance by storing frequently accessed data.\n\n## Supported Cache Drivers\n\nLoco supports several cache drivers out of the box:\n\n1. **Null Cache**: A no-op cache that doesn't actually store anything (default)\n2. **In-Memory Cache**: A local in-memory cache using the `moka` crate\n3. **Redis Cache**: A distributed cache using Redis\n\n## Default Behavior\n\nBy default, `Loco` initializes a `Null` cache driver. The Null driver implements the cache interface but doesn't actually store any data:\n\n- `get()` operations always return `None`\n- Other operations like `insert()`, `remove()`, etc. return errors with a message indicating the operation is not supported\n\nIf you use the cache functionality without configuring a proper cache driver, many operations will result in errors. It's recommended to configure a real cache driver for production use.\n\n## Configuring Cache Drivers\n\nYou can configure your preferred cache driver in your application's configuration files (e.g., `config/development.yaml`).\n\n### Configuration Examples\n\n#### Null Cache (Default)\n\n```yaml\ncache:\n  kind: Null\n```\n\n#### In-Memory Cache\nfeature `cache_inmem` enable by default\n```yaml\ncache:\n  kind: InMem\n  max_capacity: 33554432 # 32MiB (default if not specified)\n```\n\n#### Redis Cache\nfeature `cache_redis` should be enabled\n```yaml\ncache:\n  kind: Redis\n  uri: \"redis://localhost:6379\"\n  max_size: 10 # Maximum number of connections in the pool\n```\n\nIf no cache configuration is provided, the `Null` cache will be used by default.\n\n## Using the Cache\n\nAll items are cached as serialized values with string keys.\n\n```rust\nuse std::time::Duration;\nuse loco_rs::cache;\nuse serde::{Serialize, Deserialize};\n\n#[derive(Serialize, Deserialize)]\nstruct User {\n    name: String,\n    age: u32,\n}\n\nasync fn test_cache(ctx: AppContext) -> Result<()> {\n    // Insert a simple string value\n    ctx.cache.insert(\"string_key\", \"simple value\").await?;\n\n    // Insert a structured value\n    let user = User { name: \"Alice\".to_string(), age: 30 };\n    ctx.cache.insert(\"user:1\", &user).await?;\n\n    // Insert with expiration\n    ctx.cache.insert_with_expiry(\"expiring_key\", \"temporary value\", Duration::from_secs(300)).await?;\n\n    // Retrieve a string value\n    let string_value = ctx.cache.get::<String>(\"string_key\").await?;\n\n    // Retrieve a structured value\n    let user = ctx.cache.get::<User>(\"user:1\").await?;\n\n    // Remove a value\n    ctx.cache.remove(\"string_key\").await?;\n\n    // Check if a key exists\n    let exists = ctx.cache.contains_key(\"user:1\").await?;\n\n    // Get or insert (retrieve if exists, otherwise compute and store)\n    let lazy_value = ctx.cache.get_or_insert::<String, _>(\"lazy_key\", async {\n        Ok(\"computed value\".to_string())\n    }).await?;\n\n    Ok(())\n}\n```\n\nSee the [Cache API](https://docs.rs/loco-rs/latest/loco_rs/cache/struct.Cache.html) docs for more examples.\n"
  },
  {
    "path": "docs-site/content/docs/infrastructure/data.md",
    "content": "+++\ntitle = \"Data\"\ndescription = \"\"\ndate = 2024-02-07T08:00:00+00:00\nupdated = 2024-02-07T08:00:00+00:00\ndraft = false\nweight = 4\nsort_by = \"weight\"\ntemplate = \"docs/page.html\"\n\n[extra]\nlead = \"\"\ntoc = true\ntop = false\nflair =[]\n+++\n\n`Loco` provides a simple static data loader facility. This can be useful for the following cases:\n\n* You need access to read-only data that has to be loaded from a JSON file\n* You download data from external sources periodically, and want to use it in your process (to refresh you typically restart the process or read from disk directly)\n\n\nExamples:\n\n* Machine learning model hyperparameters (that are updated from time to time)\n* IP banlist\n* Calendar-related events\n* Stock data\n* Security policies\n* Per-container policies or configuration\n\n## Creating a new data loader\n\nUse the `data` generator:\n\n```\n$ cargo loco g data stocks\nadded: \"data/stocks/data.json\"\nadded: \"src/data/stocks.rs\"\ninjected: \"src/data/mod.rs\"\n* Data loader `Stocks` was added successfully.\n```\n\nThe actual data should be placed in the new `data/` folder (next to `src/`). Similar to how configuration is placed in `config/`. Here, the JSON data file is named `data/stocks/data.json`.\n\nThe data _module_ is in the `src/data/stocks.rs` module that was added, and creates a new `crate::data::stocks` namespace available statically from anywhere in your code.\n\nRemember, to load the data your app _binary_ needs to see a `data/` folder next to it. If you want to customize the name of this folder you can set the `LOCO_DATA` environment variable.\n\n## Shape your data structure\n\nYour `src/data/stocks.rs` file contains an initial definition for the data which was automatically generated:\n\n```\n#[derive(Debug, Clone, Serialize, Deserialize, Default)]\npub struct Stocks {\n    pub is_loaded: bool,\n}\n```\n\nWhen you put your real data in `data/stocks/data.json` you should define the _shape_ of your data in the `Stocks` struct to match (you can do this automatically with a tool like [quicktype](https://quicktype.io/)). You can use any `serde`-friendly data type.\n\n\n## Using your static data\n\nUse `data::stocks::get()` from anywhere to access the data which is loaded **once** for the duration of the life of your process (this will use an in-memory image of your data). You can call `get()` as many times as you want and pay no special performance fee for it.\n\nUse `data::stocks::read()`  to read directly from disk (note: this will spend IO time reading for every call).\n\n## Updating the process data\n\nBecause this data is loaded **once** for the duration of the life of your process, you need to restart your process to effectively update it. \n\nFor the `data` subsystem we assume that the use cases around these types of data is massively read many more times than it is updated (but it is updated from time to time), so it is a read-heavy use case, and data that is _frequently_ updated in any case needs a different storage paradigm (cache, database, etc.). The in-memory copy of your data will have the best read access performance possible, like any other static data.\n\nIn cases you do need to update this data, restarting a Loco process is _fast_, and is similar in concept to deploying a new version, but not deploying new code which saves time and effort.\n\nYou can also use the `read()` function to read from disk, and cache it somewhere centrally (you can use the Loco `cache` system).\n"
  },
  {
    "path": "docs-site/content/docs/infrastructure/deployment.md",
    "content": "+++\ntitle = \"Deployment\"\ndate = 2021-05-01T08:00:00+00:00\nupdated = 2021-05-01T08:00:00+00:00\ndraft = false\nweight = 3\nsort_by = \"weight\"\ntemplate = \"docs/page.html\"\n\n[extra]\ntoc = true\ntop = false\nflair =[]\n+++\n\nDeployment is super simple in Loco, and this is why this guide is super short. Although **most of the time in development you are using `cargo`** when deploying, you use the **binary that was compiled**, there is no need for `cargo` or Rust on the target server.\n\n## How to Deploy\nFirst, check your Cargo.toml to see your application name:\n```toml\n[package]\nname = \"myapp\" # This is your binary name\nversion = \"0.1.0\"\n```\n\nbuild your production binary for your relevant server architecture:\n\n<!-- <snip id=\"build-command\" inject_from=\"yaml\" template=\"sh\"> -->\n```sh\ncargo build --release\n```\n<!-- </snip>-->\n\nAnd copy your binary along with your `config/` folder to the server. You can then run `myapp start` on your server.\n\n```sh\n# The binary is located in ./target/release/ after building\n./target/release/myapp start\n```\n\nThat's it!\n\nWe took special care that **all of your work** is embbedded in a **single** binary, so you need nothing on the server other than that.\n\n## Review your production config\n\nThere are a few configuration sections that are important to review and set accordingly when deploying to production:\n\n- Logger:\n\n<!-- <snip id=\"configuration-logger\" inject_from=\"code\" template=\"yaml\"> -->\n```yaml\n# Application logging configuration\nlogger:\n  # Enable or disable logging.\n  enable: true\n  # Enable pretty backtrace (sets RUST_BACKTRACE=1)\n  pretty_backtrace: true\n  # Log level, options: trace, debug, info, warn or error.\n  level: debug\n  # Define the logging format. options: compact, pretty or json\n  format: compact\n  # By default the logger has filtering only logs that came from your code or logs that came from `loco` framework. to see all third party libraries\n  # Uncomment the line below to override to see all third party libraries you can enable this config and override the logger filters.\n  # override_filter: trace\n```\n<!-- </snip>-->\n \n\n- Server:\n<!-- <snip id=\"configuration-server\" inject_from=\"code\" template=\"yaml\"> -->\n```yaml\nserver:\n  # Port on which the server will listen. the server binding is 0.0.0.0:{PORT}\n  port: {{ get_env(name=\"NODE_PORT\", default=5150) }}\n  # The UI hostname or IP address that mailers will point to.\n  host: http://localhost\n```\n<!-- </snip>-->\n\n\n- Database:\n<!-- <snip id=\"configuration-database\" inject_from=\"code\" template=\"yaml\"> -->\n```yaml\ndatabase:\n  # Database connection URI\n  uri: {{get_env(name=\"DATABASE_URL\", default=\"postgres://loco:loco@localhost:5432/loco_app\")}}\n  # When enabled, the sql query will be logged.\n  enable_logging: false\n  # Set the timeout duration when acquiring a connection.\n  connect_timeout: 500\n  # Set the idle duration before closing a connection.\n  idle_timeout: 500\n  # Minimum number of connections for a pool.\n  min_connections: 1\n  # Maximum number of connections for a pool.\n  max_connections: 1\n  # Run migration up when application loaded\n  auto_migrate: true\n  # Truncate database when application loaded. This is a dangerous operation, make sure that you using this flag only on dev environments or test mode\n  dangerously_truncate: false\n  # Recreating schema when application loaded.  This is a dangerous operation, make sure that you using this flag only on dev environments or test mode\n  dangerously_recreate: false\n```\n<!-- </snip>-->\n\n\n- Mailer:\n<!-- <snip id=\"configuration-mailer\" inject_from=\"code\" template=\"yaml\"> -->\n```yaml\nmailer:\n  # SMTP mailer configuration.\n  smtp:\n    # Enable/Disable smtp mailer.\n    enable: true\n    # SMTP server host. e.x localhost, smtp.gmail.com\n    host: {{ get_env(name=\"MAILER_HOST\", default=\"localhost\") }}\n    # SMTP server port\n    port: 1025\n    # Use secure connection (SSL/TLS).\n    secure: false\n    # auth:\n    #   user:\n    #   password:\n```\n<!-- </snip>-->\n\n- Queue:\n<!-- <snip id=\"configuration-queue\" inject_from=\"code\" template=\"yaml\"> -->\n```yaml\nqueue:\n  kind: Redis\n  # Redis connection URI\n  uri: {{ get_env(name=\"REDIS_URL\", default=\"redis://127.0.0.1\") }}\n  # Dangerously flush all data in Redis on startup. dangerous operation, make sure that you using this flag only on dev environments or test mode\n  dangerously_flush: false\n```\n<!-- </snip>-->\n\n- JWT secret:\n<!-- <snip id=\"configuration-auth\" inject_from=\"code\" template=\"yaml\"> -->\n```yaml\nauth:\n  # JWT authentication\n  jwt:\n    # Secret key for token generation and verification\n    secret: PqRwLF2rhHe8J22oBeHy\n    # Token expiration time in seconds\n    expiration: 604800 # 7 days\n```\n<!-- </snip>-->\n\n## Running `loco doctor`\n\nYou can run `loco doctor` in your server to check the connection health of your environment. \n\n```sh\n$ myapp doctor --production\n```\n\n## Generate\n\nLoco offers a deployment template enabling the creation of a deployment infrastructure.\n\n```sh\n$ cargo loco generate deployment --help\nGenerate a deployment infrastructure\n\nUsage: myapp-cli generate deployment [OPTIONS] <KIND>\n\nArguments:\n  <KIND>  [possible values: docker, nginx]\n```\n\n<!-- <snip id=\"generate-deployment-command\" inject_from=\"yaml\" template=\"sh\"> -->\n\n```sh\ncargo loco generate deployment docker\n\nadded: \"Dockerfile\"\nadded: \".dockerignore\"\n* Dockerfile generated successfully.\n* Dockerignore generated successfully\n```\n\n<!-- </snip>-->\n\nDeployment Options:\n\n1. Docker:\n\n- Generates a Dockerfile ready for building and deploying.\n- Creates a .dockerignore file.\n\n2. Nginx:\n\n- Generates a nginx configuration file for reverse proxying.\n\nChoose the option that best fits your deployment needs. Happy deploying!\n\nIf you have a preference for deploying on a different cloud, feel free to open a pull request. Your contributions are more than welcome!\n"
  },
  {
    "path": "docs-site/content/docs/infrastructure/storage.md",
    "content": "+++\ntitle = \"Storage\"\ndescription = \"\"\ndate = 2024-02-07T08:00:00+00:00\nupdated = 2024-02-07T08:00:00+00:00\ndraft = false\nweight = 1\nsort_by = \"weight\"\ntemplate = \"docs/page.html\"\n\n[extra]\nlead = \"\"\ntoc = true\ntop = false\nflair =[]\n+++\n\nIn Loco Storage, we facilitate working with files through multiple operations. Storage can be in-memory, on disk, or use cloud services such as AWS S3, GCP, and Azure.\n\nLoco supports simple storage operations and advanced features like mirroring data or backup strategies with different failure modes.\n\nBy default, in-memory and disk storage come out of the box. To work with cloud providers, you should specify the following features:\n- `storage_aws_s3`\n- `storage_azure`\n- `storage_gcp`\n- `all_storage`\n\nBy default loco initialize a `Null` provider, meaning any work with the storage will return an error. \n\n## Setup\n\nAdd the `after_context` function as a Hook in the `app.rs` file and import the `storage` module from `loco_rs`.\n\n```rust\nuse loco_rs::storage;\n\nasync fn after_context(ctx: AppContext) -> Result<AppContext> {\n    Ok(ctx)\n}\n```\n\nThis hook returns a Storage instance that holds all storage configurations, covered in the next sections. This Storage instance is stored as part of the application context and is available in controllers, endpoints, task workers, and more.\n\n## Glossary\n|          |   |\n| -        | - |\n| `StorageDriver` | Trait implementation something that does storage  |\n| `Storage`| Abstraction implementation for managing one or more storage drivers. |\n| `Strategy`| Trait implementing various strategies for Storage, such as mirror or backup. |\n| `FailureMode`| Implemented within each Strategy, determining how to handle operations in case of failures. |\n\n### Initialize Storage\n\nStorage can be configured with a single driver or multiple drivers.\n\n#### Single Store\n\nIn this example, we initialize the in-memory driver and create a new storage with the single function.\n\n```rust\nuse loco_rs::storage;\n\nasync fn after_context(ctx: AppContext) -> Result<AppContext> {\n    Ok(AppContext {\n        storage: Storage::single(storage::drivers::mem::new()).into(),\n        ..ctx\n    })\n}\n```\n\n### Multiple Drivers\n\nFor advanced usage, you can set up multiple drivers and apply smart strategies that come out of the box. Each strategy has its own set of failure modes that you can decide how to handle.\n\nCreating multiple drivers:\n\n```rust\nuse crate::storage::{drivers, Storage};\n\nlet aws_1 = drivers::aws::new(\"users\");\nlet azure = drivers::azure::new(\"users\");\nlet aws_2 = drivers::aws::new(\"users-mirror\");\n```\n\n#### Mirror Strategy:\nYou can keep multiple services in sync by defining a mirror service. A mirror service **replicates** uploads, deletes, rename and copy across two or more subordinate services. The download behavior redundantly retrieves data, meaning if the file retrieval fails from the primary, the first file found in the secondaries is returned.\n\n#### Behaviour\n\nAfter creating the three store instances, we need to create the mirror strategy instance and define the failure mode. The mirror strategy expects the primary store and a list of secondary stores, along with failure mode options:\n- `MirrorAll`: All secondary storages must succeed. If one fails, the operation continues to the rest but returns an error.\n- `AllowMirrorFailure`: The operation does not return an error when one or more mirror operations fail.\n\nThe failure mode is relevant for upload, delete, move, and copy.\n\nExample:\n```rust\n\n// Define the mirror strategy by setting the primary store and secondary stores by names.\nlet strategy = Box::new(MirrorStrategy::new(\n    \"store_1\",\n    Some(vec![\"store_2\".to_string(), \"store_3\".to_string()]),\n    FailureMode::MirrorAll,\n)) as Box<dyn StorageStrategy>;\n\n// Create the storage with the store mapping and the strategy.\n let storage = Storage::new(\n    BTreeMap::from([\n        (\"store_1\".to_string(), aws_1),\n        (\"store_2\".to_string(), azure),\n        (\"store_3\".to_string(), aws_2),\n    ]),\n    strategy.into(),\n);\n```\n\n### Backup Strategy:\n\nYou can back up your operations across multiple storages and control the failure mode policy.\n\nAfter creating the three store instances, we need to create the backup strategy instance and define the failure mode. The backup strategy expects the primary store and a list of secondary stores, along with failure mode options:\n- `BackupAll`: All secondary storages must succeed. If one fails, the operation continues to the rest but returns an error.\n- `AllowBackupFailure`: The operation does not return an error when one or more backup operations fail.\n- `AtLeastOneFailure`: At least one operation should pass.\n- `CountFailure`: The given number of backups should pass.\n\nThe failure mode is relevant for upload, delete, move, and copy. The download always retrieves the file from the primary.\n\nExample:\n```rust\n\n// Define the backup strategy by setting the primary store and secondary stores by names.\nlet strategy: Box<dyn StorageStrategy> = Box::new(BackupStrategy::new(\n    \"store_1\",\n    Some(vec![\"store_2\".to_string(), \"store_3\".to_string()]),\n    FailureMode::AllowBackupFailure,\n)) as Box<dyn StorageStrategy>;\n\nlet storage = Storage::new(\n    BTreeMap::from([\n        (\"store_1\".to_string(), store_1),\n        (\"store_2\".to_string(), store_2),\n        (\"store_3\".to_string(), store_3),\n    ]),\n    strategy.into(),\n);\n```\n\n## Create Your Own Strategy\n\nIn case you have a specific strategy, you can easily create it by implementing the StorageStrategy and implementing all store functionality.\n\n## Usage In Controller\n\nFollow this example, make sure you enable `multipart` feature in axum crate.\n\n```rust\nuse loco_rs::prelude::*;\n\nasync fn upload_file(\n    State(ctx): State<AppContext>,\n    mut multipart: Multipart,\n) -> Result<Response> {\n    let mut file = None;\n    while let Some(field) = multipart.next_field().await.map_err(|err| {\n        tracing::error!(error = ?err,\"could not readd multipart\");\n        Error::BadRequest(\"could not readd multipart\".into())\n    })? {\n        let file_name = match field.file_name() {\n            Some(file_name) => file_name.to_string(),\n            _ => return Err(Error::BadRequest(\"file name not found\".into())),\n        };\n\n        let content = field.bytes().await.map_err(|err| {\n            tracing::error!(error = ?err,\"could not readd bytes\");\n            Error::BadRequest(\"could not readd bytes\".into())\n        })?;\n\n        let path = PathBuf::from(\"folder\").join(file_name);\n        ctx.storage.as_ref().upload(path.as_path(), &content).await?;\n\n        file = Some(path);\n    }\n\n    file.map_or_else(not_found, |path| {\n        format::json(views::upload::Response::new(path.as_path()))\n    })\n}\n```\n# Testing\n\nBy testing file storage in your controller you can follow this example:\n\n```rust\nuse loco_rs::testing::prelude::*;\n\n#[tokio::test]\n#[serial]\nasync fn can_register() {\n    request::<App, _, _>(|request, ctx| async move {\n        let file_content = \"loco file upload\";\n        let file_part = Part::bytes(file_content.as_bytes()).file_name(\"loco.txt\");\n\n        let multipart_form = MultipartForm::new().add_part(\"file\", file_part);\n\n        let response = request.post(\"/upload/file\").multipart(multipart_form).await;\n\n        response.assert_status_ok();\n\n        let res: views::upload::Response = serde_json::from_str(&response.text()).unwrap();\n\n        let stored_file: String = ctx.storage.as_ref().download(&res.path).await.unwrap();\n\n        assert_eq!(stored_file, file_content);\n    })\n    .await;\n}\n```\n\n"
  },
  {
    "path": "docs-site/content/docs/processing/_index.md",
    "content": "+++\ntitle = \"Processing\"\ndescription = \"\"\ntemplate = \"docs/section.html\"\nsort_by = \"weight\"\nweight = 3\ndraft = false\n+++\n"
  },
  {
    "path": "docs-site/content/docs/processing/mailers.md",
    "content": "+++\ntitle = \"Mailers\"\ndescription = \"\"\ndate = 2021-05-01T18:10:00+00:00\nupdated = 2021-05-01T18:10:00+00:00\ndraft = false\nweight = 2\nsort_by = \"weight\"\ntemplate = \"docs/page.html\"\n\n[extra]\nlead = \"\"\ntoc = true\ntop = false\nflair =[]\n+++\n\nA mailer will deliver emails in the background using the existing `loco` background worker infrastructure. It will all be seamless for you.\n\n# Sending email\n\nTo use an existing mailer, mostly in your controller:\n\n```rust\nuse crate::{\n    mailers::auth::AuthMailer,\n}\n\n// in your controllers/auth.rs\nasync fn register(\n    State(ctx): State<AppContext>,\n    Json(params): Json<RegisterParams>,\n) -> Result<Response> {\n    // .. register a user ..\n    AuthMailer::send_welcome(&ctx, &user.email).await.unwrap();\n}\n```\n\nThis will enqueue a mail delivery job. The action is instant because the delivery will be performed later in the background.\n\n## Configuration\n\nConfiguration for mailers is done in the `config/[stage].yaml` file. Here is the default configuration:\n\n```yaml\n# Mailer Configuration.\nmailer:\n  # SMTP mailer configuration.\n  smtp:\n    # Enable/Disable smtp mailer.\n    enable: true\n    # SMTP server host. e.x localhost, smtp.gmail.com\n    host: {{/* get_env(name=\"MAILER_HOST\", default=\"localhost\") */}}\n    # SMTP server port\n    port: 1025\n    # Use secure connection (SSL/TLS).\n    secure: false\n    # auth:\n    #   user:\n    #   password:\n```\n\nMailer is done by sending emails to a SMTP server. An example configuration for using sendgrid (choosing the SMTP relay option):\n\n```yaml\n# Mailer Configuration.\nmailer:\n  # SMTP mailer configuration.\n  smtp:\n    # Enable/Disable smtp mailer.\n    enable: true\n    # SMTP server host. e.x localhost, smtp.gmail.com\n    host: {{/* get_env(name=\"MAILER_HOST\", default=\"smtp.sendgrid.net\") */}}\n    # SMTP server port\n    port: 587\n    # Use secure connection (SSL/TLS).\n    secure: true\n    auth:\n      user: \"apikey\"\n      password: \"your-sendgrid-api-key\"\n```\n\n### Default Email Address\n\nOther than specifying email addresses for every email sending task, you can override a default email address per-mailer.\n\nFirst, override the `opts` function in the `Mailer` trait, in this example for an `AuthMailer`:\n\n```rust\nimpl Mailer for AuthMailer {\n    fn opts() -> MailerOpts {\n        MailerOpts {\n            from: // set your from email,\n            ..Default::default()\n        }\n    }\n}\n```\n\n### Using a mail catcher in development\n\nYou can use an app like `MailHog` or `mailtutan` (written in Rust):\n\n```\n$ cargo install mailtutan\n$ mailtutan\nlistening on smtp://0.0.0.0:1025\nlistening on http://0.0.0.0:1080\n```\n\nThis will bring up a local smtp server and a nice UI on `http://localhost:1080` that \"catches\" and shows emails as they are received.\n\nAnd then put this in your `development.yaml`:\n\n```yaml\n# Mailer Configuration.\nmailer:\n  # SMTP mailer configuration.\n  smtp:\n    # Enable/Disable smtp mailer.\n    enable: true\n    # SMTP server host. e.x localhost, smtp.gmail.com\n    host: localhost\n    # SMTP server port\n    port: 1025\n    # Use secure connection (SSL/TLS).\n    secure: false\n```\n\nNow your mailer workers will send email to the SMTP server at `localhost`.\n\n## Adding a mailer\n\nYou can generate a mailer:\n\n```sh\ncargo loco generate mailer <mailer name>\n```\n\nOr, you can define it manually if you like to see how things work. In `mailers/auth.rs`, add:\n\n```rust\nstatic welcome: Dir<'_> = include_dir!(\"src/mailers/auth/welcome\");\nimpl AuthMailer {\n    /// Sending welcome email the the given user\n    ///\n    /// # Errors\n    ///\n    /// When email sending is failed\n    pub async fn send_welcome(ctx: &AppContext, _user_id: &str) -> Result<()> {\n        Self::mail_template(\n            ctx,\n            &welcome,\n            Args {\n                to: \"foo@example.com\".to_string(),\n                locals: json!({\n                  \"name\": \"joe\"\n                }),\n                ..Default::default()\n            },\n        )\n        .await?;\n        Ok(())\n    }\n}\n```\n\nEach mailer has an opinionated, predefined folder structure:\n\n```\nsrc/\n  mailers/\n    auth/\n      welcome/      <-- all the parts of an email, all templates\n        subject.t\n        html.t\n        text.t\n    auth.rs         <-- mailer definition\n```\n\n### Running a mailer\nThe mailer operates as a background worker, which means you need to run the worker separately to process the jobs. The default startup command `cargo loco start` does not initiate the worker, so you need to run it separately:\n\nTo run the worker, use the following command:\n```bash\ncargo loco start --worker\n```\n\nTo run both the server and the worker simultaneously, use the following command:\n```bash\ncargo loco start --server-and-worker\n```\n\n# Testing\n\nTesting emails sent as part of your workflow can be a complex task, requiring validation of various scenarios such as email verification during user registration and checking user password emails. The primary goal is to streamline the testing process by examining the number of emails sent in the workflow, reviewing email content, and allowing for data snapshots.\n\nIn `Loco`, we have introduced a stub test email feature. Essentially, emails are not actually sent; instead, we collect information on the number of emails and their contents as part of the testing context.\n\n## Configuration\n\nTo enable the stub in your tests, add the following field to the configuration under the mailer section in your YAML file:\n\n```yaml\nmailer:\n  stub: true\n```\n\nNote: If your email sender operates within a [worker](@/docs/processing/workers.md) process, ensure that the worker mode is set to ForegroundBlocking.\n\nOnce you have configured the stub, proceed to your unit tests and follow the example below:\n\n## Writing a test\n\nTest Description:\n\n- Create an HTTP request to the endpoint responsible for sending emails as part of your code.\n- Retrieve the mailer instance from the context and call the deliveries() function, which contains information about the number of sent emails and their content.\n\n```rust\nuse loco_rs::testing::prelude::*;\n\n#[tokio::test]\n#[serial]\nasync fn can_register() {\n    configure_insta!();\n\n    request::<App, Migrator, _, _>(|request, ctx| async move {\n        // Create a request for user registration.\n\n        // Now you can call the context mailer and use the deliveries function.\n        with_settings!({\n            filters => cleanup_email()\n        }, {\n            assert_debug_snapshot!(ctx.mailer.unwrap().deliveries());\n        });\n    })\n    .await;\n}\n```\n\n"
  },
  {
    "path": "docs-site/content/docs/processing/scheduler.md",
    "content": "+++\ntitle = \"Scheduler\"\ndescription = \"\"\ndate = 2024-11-09T18:10:00+00:00\nupdated = 2025-06-03T14:10:00+00:00\ndraft = false\nweight = 5\nsort_by = \"weight\"\ntemplate = \"docs/page.html\"\n\n[extra]\nlead = \"\"\ntoc = true\ntop = false\nflair =[]\n+++\n\nLoco simplifies the traditional, often cumbersome `crontab` system, making it easier and more elegant to schedule cron jobs. The scheduler job can execute either a shell script command or run a registered [task](@/docs/processing/task.md).\n\n## Setting Up\n\nScheduler jobs can be configured via a your YAML scheduler setup file or as part of an environment YAML file.\n\n### 1. Your Dedicated File\n\nUsing a dedicated file provides a centralized place to configure all your scheduler jobs, making it easier to manage and maintain. You can start by generating a template file using the Loco generator command:\n\n```sh\ncargo loco generate scheduler\n```\n\nThis command creates a `scheduler.yaml` file under the `config` folder. You can then configure your jobs within this file.\n\nTo use this dedicated file:\n\n- When running the scheduler as a separate process, you'll use the `--config` flag with the `cargo loco scheduler` command (see \"Verifying the Configuration\" and \"Running the Scheduler\" sections).\n- If you want the main application (started with `cargo loco start --all`) to use this dedicated file, you'll need to set the `SCHEDULER_CONFIG` environment variable (see \"Running the Scheduler\" section for details).\n\n### 2. Environment Configuration File\n\nYou can also configure scheduler jobs per environment by adding the scheduler section to your environment's YAML configuration file:\n\n<!-- <snip id=\"configuration-scheduler\" inject_from=\"code\" template=\"yaml\"> -->\n\n```yaml\nscheduler:\n  # Location of shipping the command stdout and stderr.\n  output: stdout\n  # A list of jobs to be scheduled.\n  jobs:\n    # The name of the job.\n    write_content:\n      # by default false meaning executing the the run value as a task. if true execute the run value as shell command\n      shell: true\n      # command to run\n      run: \"echo loco >> ./scheduler.txt\"\n      # The cron expression that defines the job's schedule.\n      schedule: run every 1 second\n      output: silent\n      tags: [\"base\", \"infra\"]\n\n    run_task:\n      run: \"foo\"\n      schedule: \"at 10:00 am\"\n      run_on_start: true\n\n    list_if_users:\n      run: \"user_report\"\n      shell: true\n      schedule: \"* 2 * * * *\"\n      tags: [\"base\", \"users\"]\n```\n\n<!-- </snip> -->\n\n## The `SCHEDULER_CONFIG` Environment Variable\n\nLoco uses the `SCHEDULER_CONFIG` environment variable to locate your scheduler configuration file (e.g., `scheduler.yaml`) when it's not embedded directly within your environment's main configuration file (like `development.yaml`).\n\nThis is particularly important when:\n\n- You have a dedicated `scheduler.yaml` file (e.g., generated by `cargo loco generate scheduler`).\n- You are running the scheduler as part of the main application using `cargo loco start --all`.\n\nIn such cases, you need to set `SCHEDULER_CONFIG` to the path of your scheduler file. For example:\n`export SCHEDULER_CONFIG=config/scheduler.yaml`\n\n## Scheduler Configuration\n\nThe scheduler configuration consists of the following elements:\n\n- `scheduler.output` (Optional): Sets the default output location for all jobs.\n  - `stdout:` Output to the console (default).\n  - `silent:` Suppress all output.\n- `scheduler.jobs:` A object of jobs to be scheduled, the object key describe the job name. Each job has:\n\n  - `schedule`: The cron expression that defines the job's schedule.\n    The cron get an english that convert to cron syntax or cron syntax itself.\n\n    ##### **_English to cron_**\n\n    - Examples:\n    - every 15 seconds\n    - run every minute\n    - fire every day at 4:00 pm\n    - at 10:00 am\n    - run at midnight on the 1st and 15th of the month\n    - On Sunday at 12:00\n    - 7pm every Thursday\n    - midnight on Tuesdays\n\n    ##### **_Cron Syntax format:_**\n\n    The cronjob should be UTC based\n\n    ```sh\n    sec   min   hour   day of month   month   day of week   year\n    *     *     *      *              *       *             *\n    ```\n\n  - `run_on_start`: By default, `false`. If set to `true`, the job will also run at the start of the scheduler.\n    - `shell`: by default `false` meaning executing the the `run` value as a task. if `true` execute the `run` value as shell command\n    - `run`: Cronjob command to run.\n      - `Task:` The task name (with variables e.x `[TASK_NAME] KEY:VAl`. follow [here](@/docs/processing/task.md) to see task arguments ). Note that the `shell` field should be false.\n      - `Shell`: Run a shell command (e.x `\"echo loco >> ./scheduler.txt\"`). Note that the `shell` field should be true.\n    - `tags` (Optional): A list of tags to categorize and manage the job.\n    - `output` (Optional): Overrides the global `scheduler.output` for this job.\n\n## Verifying the Configuration\n\nAfter setting up your jobs, you can verify the configuration to ensure everything is correct.\n\n### 1. When using a dedicated file:\n\nRun the following command to list the jobs from your scheduler file:\n\n<!-- <snip id=\"scheduler-list-from-file-command\" inject_from=\"yaml\"  template=\"sh\"> -->\n\n```sh\ncargo loco scheduler --config config/scheduler.yaml --list\n```\n\n<!-- </snip> -->\n\n### 2. When using environment-based configuration:\n\nTo list jobs from the environment configuration, run:\n\n<!-- <snip id=\"scheduler-list-from-env-setting-command\" inject_from=\"yaml\"  template=\"sh\"> -->\n\n```sh\nLOCO_ENV=production cargo loco scheduler --list\n```\n\n<!-- </snip> -->\n\nThis command loads the scheduler configuration from the `scheduler:` block within your `config/production.yaml` file and lists the defined jobs.\n\n## Running the Scheduler\n\nOnce the configuration is verified, you can run the scheduler. There are two primary ways to do this:\n\n### 1. As a Dedicated Process\n\nThis approach runs the scheduler independently of your main web server or background workers. It's useful if you want to manage its lifecycle separately.\n\n- **Using a dedicated configuration file** (e.g., `config/scheduler.yaml`):\n  Pass the path to your configuration file using the `--config` flag.\n\n  ```sh\n  cargo loco scheduler --config config/scheduler.yaml\n  ```\n\n- **Using scheduler configuration from an environment file** (e.g., `scheduler:` block in `config/development.yaml`):\n  Ensure your `LOCO_ENV` is set correctly (or it defaults to `development`), then run:\n  ```sh\n  cargo loco scheduler\n  ```\n  Or, to specify an environment:\n  ```sh\n  LOCO_ENV=production cargo loco scheduler\n  ```\n\n### 2. Integrated with the Main Application (using `cargo loco start --all`)\n\nThis mode starts the server, background worker(s), and the scheduler together in the same process.\n\n- Command:\n\n  ```sh\n  cargo loco start --all\n  ```\n\n- **Loading Configuration for `start --all`:**\n\n  - **From environment configuration file**: If your scheduler settings are defined within your active environment's YAML file (e.g., under the `scheduler:` key in `config/development.yaml`), `cargo loco start --all` will automatically pick them up.\n  - **From a dedicated scheduler file**: If you have a dedicated `scheduler.yaml` file (e.g., one generated by `cargo loco generate scheduler`), you **must** inform Loco where to find this file by setting the `SCHEDULER_CONFIG` environment variable.\n\n    ```sh\n    SCHEDULER_CONFIG=config/scheduler.yaml cargo loco start --all\n    ```\n\n    _This is a common point of confusion if missed. Setting `SCHEDULER_CONFIG` ensures that `cargo loco start --all` knows which scheduler configuration to load when it's not embedded in the main environment configuration file. This was highlighted in a community discussion regarding scheduler initialization failures._\n\nThe scheduler will continuously execute jobs based on their schedule until a shutdown signal (e.g., `Ctrl+C`) is received. When a signal is received, it gracefully terminates all running tasks and shuts down safely.\n\n### Important Notes:\n\n- When a job is running, `Loco` spawns it in a new process, and all environment variables will propagate to the new job process.\n- For tasks, ensure you run the scheduler with a valid environment by using the `--environment` flag or setting the `LOCO_ENV` environment variable. This ensures the correct environment and configuration are loaded for the task.\n- You can pass variables to tasks by using the vars object in the task configuration.\n\n## Running a Single Scheduled Job by Name\n\nTo run a specific scheduler job by its name, use the --name flag. This will execute a single job with the provided name.\n\n<!-- <snip id=\"scheduler-run-job-by-name-command\" inject_from=\"yaml\"  template=\"sh\"> -->\n\n```sh\nLOCO_ENV=production cargo loco scheduler --name 'JOB_NAME'\n```\n\n<!-- </snip> -->\n\nThis command will locate the job named `\"Run command\"` in your scheduler.yaml file and run it.\n\n## Running Scheduled Jobs by Tag\n\nYou can also run multiple jobs that share the same tag. Tags are useful for grouping related jobs together. For example, you might have several jobs that perform different types of maintenance tasks—such as database cleanup, cache invalidation, and log rotation—that you want to run together. Assigning them the same tag, like `maintenance`, allows you to execute them all at once.\n\n<!-- <snip id=\"scheduler-run-job-by-tag-command\" inject_from=\"yaml\"  template=\"sh\"> -->\n\n```sh\nLOCO_ENV=production cargo loco scheduler --tag 'maintenance'\n```\n\n<!-- </snip> -->\n\nThis command runs all jobs that have been tagged with `maintenance`, ensuring that all related jobs are executed in one go.\n"
  },
  {
    "path": "docs-site/content/docs/processing/task.md",
    "content": "+++\ntitle = \"Tasks\"\ndescription = \"\"\ndate = 2021-05-01T18:10:00+00:00\nupdated = 2021-05-01T18:10:00+00:00\ndraft = false\nweight = 4\nsort_by = \"weight\"\ntemplate = \"docs/page.html\"\n\n[extra]\nlead = \"\"\ntoc = true\ntop = false\nflair =[]\n+++\n\nTasks in `Loco` serve as ad-hoc functionalities to handle specific aspects of your application. Whether you need to fix data, send emails, delete a user, or update a customer order, creating a dedicated task for each scenario provides a flexible and efficient solution. You can run tasks manually or by [scheduling the task](@/docs/processing/scheduler.md).\n\nCreating tasks is worthwhile for several reasons:\n- **Automation of Manual Work:** Tasks automate manual processes, streamlining repetitive actions.\n- **Utilization of Familiar Components:** Leverage your app's models, libraries, and existing logic within tasks.\n- **Elimination of UI Development:** Tasks don't require building user interfaces, focusing solely on backend operations.\n- **Potential for UI Automation:** If necessary, tasks can be automated with a UI by integrating with job-running tools like Jenkins.\n\nEach task is designed to parse command-line arguments into flags, utilizing the yargs-parsed output of your CLI.\n\n## Creating a Task with the CLI Generator\n\n`Loco` provides a convenient code generator to simplify the creation of a starter task connected to your project. Use the following command to generate a task:\n\nGenerate the task:\n\n<!-- <snip id=\"generate-task-help-command\" inject_from=\"yaml\" action=\"exec\" template=\"sh\"> -->\n```sh\nGenerate a Task based on the given name\n\nUsage: demo_app-cli generate task [OPTIONS] <NAME>\n\nArguments:\n  <NAME>  Name of the thing to generate\n\nOptions:\n  -e, --environment <ENVIRONMENT>  Specify the environment [default: development]\n  -h, --help                       Print help\n  -V, --version                    Print version\n```\n<!-- </snip> -->\n\n## Running a Task\n\nExecute the task you created in the previous step using the following command:\n\n<!-- <snip id=\"run-task-command\" inject_from=\"yaml\" template=\"sh\"> -->\n```sh\ncargo loco task <TASK_NAME>\n```\n<!-- </snip> -->\n\n### Running a Task with Parameters\n\nTo pass parameters to a task, add a list of key:value to the command\n```sh\n[PARAMS]...  Task params (e.g. <`my_task`> foo:bar baz:qux)`\n```\n```sh\ncargo loco task <TASK_NAME> [PARAMS]...\n```\n\nThen use that value using [`cli_arg`](https://docs.rs/loco-rs/latest/loco_rs/task/struct.Vars.html#method.cli_arg) in the `run` method of the task\n```rust\nasync fn run(&self, app_context: &AppContext, vars: &task::Vars) -> Result<()> {\n    let foo = vars.cli_arg(\"foo\");\n    Ok(())\n}\n```\n\n## Listing All Tasks\n\nTo view a list of all tasks that have been executed, use the following command:\n\n<!-- <snip id=\"list-tasks-command\" inject_from=\"yaml\" template=\"sh\"> -->\n```sh\ncargo loco task\n```\n<!-- </snip> -->\n\n\n## Creating a Task manually\n\nIf you prefer a manual approach to creating tasks in `Loco`, you can follow these steps:\n\n#### 1. Create a Task File\n\nStart by creating a new file under the path `src/tasks`. For example, let's create a file named `example.rs`:\n\n<!-- <snip id=\"task-code-example\" inject_from=\"code\" template=\"rust\"> -->\n```rust\nuse loco_rs::prelude::*;\n\npub struct Foo;\n#[async_trait]\nimpl Task for Foo {\n    fn task(&self) -> TaskInfo {\n        TaskInfo {\n            name: \"foo\".to_string(),\n            detail: \"run foo task\".to_string(),\n        }\n    }\n    async fn run(&self, _app_context: &AppContext, _vars: &task::Vars) -> Result<()> {\n        Ok(())\n    }\n}\n```\n<!-- </snip> -->\n\n#### 2. Load the File in mod.rs\n\nNext, ensure that you load the newly created task file in the `mod.rs` file within the `src/tasks` folder.\n\n#### 3. Register the Task in App Hooks\n\nIn your App hook implementation (e.g., App struct), register the task in the register_tasks function:\n\n```rust\n// src/app.rs\n\npub struct App;\n#[async_trait]\nimpl Hooks for App {\n    ...\n\n    fn register_tasks(tasks: &mut Tasks) {\n        tasks.register(tasks::example::ExampleTask);\n    }\n\n    ...\n}\n```\n\nThese steps ensure that your manually created task, such as ExampleTask, is integrated into Loco's task management system.\n"
  },
  {
    "path": "docs-site/content/docs/processing/workers.md",
    "content": "+++\ntitle = \"Workers\"\ndescription = \"\"\ndate = 2021-05-01T18:10:00+00:00\nupdated = 2021-05-01T18:10:00+00:00\ndraft = false\nweight = 1\nsort_by = \"weight\"\ntemplate = \"docs/page.html\"\n\n[extra]\nlead = \"\"\ntoc = true\ntop = false\nflair =[]\n+++\n\nLoco provides the following options for background jobs:\n\n- Redis backed\n- Postgres backed\n- SQLite backed\n- Tokio-async based (same-process, evented thread based background jobs)\n\nYou enqueue and perform jobs without knowledge of the actual background queue implementation, similar to Rails' _ActiveJob_, so you can switch with a simple change of configuration and no code change.\n\n## Async vs Queue\n\nWhen you generated a new app, you might have selected the default `async` configuration for workers. This means workers spin off jobs in Tokio's async pool, which gives you proper background processes in the same running server.\n\nYou might want to configure jobs to run in a separate process backed by a queue, in order to distribute the load across servers.\n\nFirst, switch to `BackgroundQueue`:\n\n```yaml\n# Worker Configuration\nworkers:\n  # specifies the worker mode. Options:\n  #   - BackgroundQueue - Workers operate asynchronously in the background, processing queued.\n  #   - ForegroundBlocking - Workers operate in the foreground and block until tasks are completed.\n  #   - BackgroundAsync - Workers operate asynchronously in the background, processing tasks with async capabilities.\n  mode: BackgroundQueue\n```\n\nThen, configure a Redis based queue backend:\n\n```yaml\nqueue:\n  kind: Redis\n  # Redis connection URI.\n  uri: \"{{ get_env(name=\"REDIS_URL\", default=\"redis://127.0.0.1\") }}\"\n  # Dangerously flush all data.\n  dangerously_flush: false\n  # represents the number of tasks a worker can handle simultaneously.\n  num_workers: 2\n```\n\nOr a Postgres based queue backend:\n\n```yaml\nqueue:\n  kind: Postgres\n  # Postgres Queue connection URI.\n  uri: \"{{ get_env(name=\"PGQ_URL\", default=\"postgres://localhost:5432/mydb\") }}\"\n  # Dangerously flush all data.\n  dangerously_flush: false\n  # represents the number of tasks a worker can handle simultaneously.\n  num_workers: 2\n```\n\nOr a SQLite based queue backend:\n\n```yaml\nqueue:\n  kind: Sqlite\n  # SQLite Queue connection URI.\n  uri: \"{{ get_env(name=\"SQLTQ_URL\", default=\"sqlite://loco_development.sqlite?\n  mode=rwc\") }}\"\n  # Dangerously flush all data.\n  dangerously_flush: false\n  # represents the number of tasks a worker can handle simultaneously.\n  num_workers: 2\n```\n\n## Running the worker process\n\nYou can run in two ways, depending on which setting you chose for background workers:\n\n```\nUsage: demo_app start [OPTIONS]\n\nOptions:\n  -w, --worker [<WORKER>...]       Start worker. Optionally provide tags to run specific jobs (e.g. --worker=tag1,tag2)\n  -s, --server-and-worker          start same-process server and worker\n```\n\nChoose `--worker` when you configured a real Redis queue and you want a process for doing just background jobs. You can use a single process per server. In this case, you can run your main Web or API server using just `cargo loco start`.\n\n```sh\n$ cargo loco start --worker # starts a standalone worker job executing process\n$ cargo loco start # starts a standalone API service or Web server, no workers\n```\n\nChoose `-s` when you configured `async` background workers, and jobs will execute as part of the current running server process.\n\nFor example, running `--server-and-worker`:\n\n```sh\n$ cargo loco start --server-and-worker # both API service and workers will execute\n```\n\n### Worker Tag Filtering\n\nLoco supports tag-based job filtering, allowing you to create specialized workers that only process specific types of jobs. This is particularly useful for distributing workloads or creating dedicated workers for resource-intensive tasks.\n\nWhen starting a worker, you can specify which tags it should process:\n\n```sh\n# Start a worker that only processes jobs with no tags\n$ cargo loco start --worker\n\n# Start a worker that only processes jobs with the \"email\" tag\n$ cargo loco start --worker email\n\n# Start a worker that processes jobs with either \"report\" or \"analytics\" tags\n$ cargo loco start --worker report,analytics\n```\n\nImportant notes about tag-based processing:\n\n1. Workers with no tags (`cargo loco start --worker`) will only process jobs that have no tags\n2. Workers with tags will only process jobs that have at least one matching tag\n3. The `--all` and `--server-and-worker` modes don't support filtering by tags and will only process untagged jobs\n4. Tags are case-sensitive\n\n## Creating background jobs in code\n\nTo use a worker, we mainly think about adding a job to the queue, so you `use` the worker and perform later:\n\n```rust\n    // .. in your controller ..\n    DownloadWorker::perform_later(\n        &ctx,\n        DownloadWorkerArgs {\n            user_guid: \"foo\".to_string(),\n        },\n    )\n    .await\n```\n\nUnlike Rails and Ruby, with Rust you can enjoy _strongly typed_ job arguments which gets serialized and pushed into the queue.\n\n### Assigning Tags to Jobs\n\nWhen enqueueing a job, you can optionally assign tags to it. The job will then only be processed by workers that match at least one of its tags:\n\n```rust\n    // To create a job with a tag, define the tags in your worker:\n    struct DownloadWorker;\n\n    #[async_trait]\n    impl BackgroundWorker<DownloadWorkerArgs> for DownloadWorker {\n        // Define tags for this worker\n        fn tags() -> Vec<String> {\n            vec![\"download\".to_string(), \"network\".to_string()]\n        }\n\n        // ... other implementation details\n    }\n\n    // When you call perform_later, the job will automatically be tagged\n    DownloadWorker::perform_later(&ctx, args).await?;\n```\n\n### Using shared state from a worker\n\nSee [How to have global state](@/docs/the-app/controller.md#global-app-wide-state), but generally you use a single shared state by using something like `lazy_static` and then simply refer to it from the worker.\n\nIf this state can be serializable, _strongly prefer_ to pass it through the `WorkerArgs`.\n\n## Creating a new worker\n\nAdding a worker meaning coding the background job logic to take the _arguments_ and perform a job. We also need to let `loco` know about it and register it into the global job processor.\n\nAdd a worker to `workers/`:\n\n```rust\n#[async_trait]\nimpl BackgroundWorker<DownloadWorkerArgs> for DownloadWorker {\n    fn build(ctx: &AppContext) -> Self {\n        Self { ctx: ctx.clone() }\n    }\n\n    // Optional: Define tags for this worker\n    fn tags() -> Vec<String> {\n        vec![\"download\".to_string()]\n    }\n\n    async fn perform(&self, args: DownloadWorkerArgs) -> Result<()> {\n        println!(\"================================================\");\n        println!(\"Sending payment report to user {}\", args.user_guid);\n\n        // TODO: Some actual work goes here...\n\n        println!(\"================================================\");\n        Ok(())\n    }\n}\n```\n\nAnd register it in `app.rs`:\n\n```rust\n#[async_trait]\nimpl Hooks for App {\n//..\n    async fn connect_workers(ctx: &AppContext, queue: &Queue) -> Result<()> {\n        queue.register(DownloadWorker::build(ctx)).await?;\n        Ok(())\n    }\n// ..\n}\n```\n\n### The `BackgroundWorker` Trait\n\nThe `BackgroundWorker` trait is the core interface for defining background workers in Loco. It provides several methods:\n\n- `build(ctx: &AppContext) -> Self`: Creates a new instance of the worker with the provided application context.\n- `perform(&self, args: A) -> Result<()>`: The main method that executes the job's logic with the provided arguments.\n- `queue() -> Option<String>`: Optional method to specify a custom queue for the worker (returns `None` by default).\n- `tags() -> Vec<String>`: Optional method to specify tags for this worker (returns an empty vector by default).\n- `class_name() -> String`: Returns the worker's class name (automatically derived from the struct name).\n- `perform_later(ctx: &AppContext, args: A) -> Result<()>`: Static method to enqueue a job to be performed later.\n\n### Generate a Worker\n\nTo automatically add a worker using `loco generate`, execute the following command:\n\n```sh\ncargo loco generate worker report_worker\n```\n\nThe worker generator creates a worker file associated with your app and generates a test template file, enabling you to verify your worker.\n\n## Configuring Workers\n\nIn your `config/<environment>.yaml` you can specify the worker mode. BackgroundAsync and BackgroundQueue will process jobs in a non-blocking manner, while ForegroundBlocking will process jobs in a blocking manner.\n\nThe main difference between BackgroundAsync and BackgroundQueue is that the latter will use Redis/Postgres/SQLite to store the jobs, while the former does not require Redis/Postgres/SQLite and will use async in memory within the same process.\n\n```yaml\n# Worker Configuration\nworkers:\n  # specifies the worker mode. Options:\n  #   - BackgroundQueue - Workers operate asynchronously in the background, processing queued.\n  #   - ForegroundBlocking - Workers operate in the foreground and block until tasks are completed.\n  #   - BackgroundAsync - Workers operate asynchronously in the background, processing tasks with async capabilities.\n  mode: BackgroundQueue\n```\n\n## Manage a Workers From UI\n\nYou can manage the jobs queue with the [Loco admin job project](https://github.com/loco-rs/admin-jobs).\n![<img style=\"width:100%; max-width:640px\" src=\"tour.png\"/>](https://github.com/loco-rs/admin-jobs/raw/main/media/screenshot.png)\n\n### Managing Job Queues via CLI\n\nThe job queue management feature provides a powerful and flexible way to handle the lifecycle of jobs in your application. It allows you to cancel, clean up, remove outdated jobs, export job details, and import jobs, ensuring efficient and organized job processing.\n\n## Features Overview\n\n- **Cancel Jobs**  \n  Provides the ability to cancel specific jobs by name, updating their status to `cancelled`. This is useful for stopping jobs that are no longer needed, relevant, or if you want to prevent them from being processed when a bug is detected.\n- **Clean Up Jobs**  \n  Enables the removal of jobs that have already been completed or cancelled. This helps maintain a clean and efficient job queue by eliminating unnecessary entries.\n- **Purge Outdated Jobs**  \n  Allows you to delete jobs based on their age, measured in days. This is particularly useful for maintaining a lean job queue by removing older, irrelevant jobs.  \n  **Note**: You can use the `--dump` option to export job details to a file, manually modify the job parameters in the exported file, and then use the `import` feature to reintroduce the updated jobs into the system.\n- **Export Job Details**  \n  Supports exporting the details of all jobs to a specified location in file format. This feature is valuable for backups, audits, or further analysis.\n- **Import Jobs**  \n  Facilitates importing jobs from external files, making it easy to restore or add new jobs to the system. This ensures seamless integration of external job data into your application's workflow.\n\nTo access the job management commands, use the following CLI structure:\n\n<!-- <snip id=\"jobs-help-command\" inject_from=\"yaml\" action=\"exec\" template=\"sh\"> -->\n\n```sh\nManaging jobs queue\n\nUsage: demo_app-cli jobs [OPTIONS] <COMMAND>\n\nCommands:\n  cancel  Cancels jobs with the specified names, setting their status to `cancelled`\n  tidy    Deletes jobs that are either completed or cancelled\n  purge   Deletes jobs based on their age in days\n  dump    Saves the details of all jobs to files in the specified folder\n  import  Imports jobs from a file\n  help    Print this message or the help of the given subcommand(s)\n\nOptions:\n  -e, --environment <ENVIRONMENT>  Specify the environment [default: development]\n  -h, --help                       Print help\n  -V, --version                    Print version\n```\n\n<!-- </snip> -->\n\n## Testing a Worker\n\nYou can easily test your worker background jobs using `Loco`. Ensure that your worker is set to the `ForegroundBlocking` mode, which blocks the job, ensuring it runs synchronously. When testing the worker, the test will wait until your worker is completed, allowing you to verify if the worker accomplished its intended tasks.\n\nIt's recommended to implement tests in the `tests/workers` directory to consolidate all your worker tests in one place.\n\nAdditionally, you can leverage the [worker generator](@/docs/processing/workers.md#generate-a-worker), which automatically creates tests, saving you time on configuring tests in the library.\n\nHere's an example of how the test should be structured:\n\n```rust\nuse loco_rs::testing::prelude::*;\n\n#[tokio::test]\n#[serial]\nasync fn test_run_report_worker_worker() {\n    // Set up the test environment\n    let boot = boot_test::<App, Migrator>().await.unwrap();\n\n    // Execute the worker in 'ForegroundBlocking' mode, preventing it from running asynchronously\n    assert!(\n        ReportWorkerWorker::perform_later(&boot.app_context, ReportWorkerWorkerArgs {})\n            .await\n            .is_ok()\n    );\n\n    // Include additional assert validations after the execution of the worker\n}\n\n```\n\n### Understanding `class_name()`\n\nThe `class_name()` function in the `BackgroundWorker` trait is used to determine the unique identifier for your worker in the job queue. By default, it:\n\n1. Takes the struct name (e.g., `DownloadWorker`)\n2. Strips any module paths (e.g., `my_app::workers::DownloadWorker` becomes just `DownloadWorker`)\n3. Converts it to UpperCamelCase format\n\nThis is important because when a job is enqueued, it needs a string identifier to match with the appropriate worker when it's time for processing. This function automatically generates that identifier for you, but you can override it if you need a custom naming scheme.\n\n```rust\n// Example of how class_name works:\nstruct download_worker;\nimpl BackgroundWorker<Args> for download_worker {\n    // class_name() would return \"DownloadWorker\"\n    // No need to override this unless you need custom naming\n}\n```\n\nAnd register it in `app.rs`:\n\n```rust\n#[async_trait]\nimpl Hooks for App {\n//..\n    async fn connect_workers(ctx: &AppContext, queue: &Queue) -> Result<()> {\n        queue.register(DownloadWorker::build(ctx)).await?;\n        Ok(())\n    }\n// ..\n}\n```\n"
  },
  {
    "path": "docs-site/content/docs/resources/_index.md",
    "content": "+++\ntitle = \"Resources\"\ndescription = \"\"\ntemplate = \"docs/section.html\"\nsort_by = \"weight\"\nweight = 6\ndraft = false\n+++\n"
  },
  {
    "path": "docs-site/content/docs/resources/around-the-web.md",
    "content": "+++\ntitle = \"Around the Web\"\ndescription = \"\"\ndate = 2024-01-21T19:00:00+00:00\nupdated = 2024-01-21T19:00:00+00:00\ndraft = false\nweight = 1\nsort_by = \"weight\"\ntemplate = \"docs/page.html\"\n\n[extra]\nlead = \"\"\ntoc = true\ntop = false\nflair =[]\n+++\n\nCheck out Loco resources and links from around the Web.\n\n## Blogs\n\n- [Introducing Loco: the \"Rust on Rails\"](https://blog.rng0.io/introducing-loco/) - by [@jondot](https://x.com/jondot)\n- [Loco is a New Framework for Rust Inspired by Rails](https://www.infoq.com/news/2024/02/loco-new-framework-rust-rails/) - by [infoq.com](https://infoq.com)\n- [Going in cold inside the locomotive](https://vanhalt.com/post/loco-rs/) by [@vanhalt](https://twitter.com/vanhalt)\n- [Getting Started with Loco & SeaORM](https://www.sea-ql.org/blog/2024-05-28-getting-started-with-loco-seaorm/) by [Billy Chan (SeaORM)](https://github.com/billy1624)\n\n## Videos\n\n- [A Legendary Web Framework is Reborn... in Rust](https://www.youtube.com/watch?v=7utPutDORb4) - by [Code to the Moon](https://www.youtube.com/@codetothemoon)\n\n## Ecosystem\n\n- [rhai-loco](https://docs.rs/rhai-loco/latest/rhai_loco/) - This crate adds [Rhai](https://rhai.rs) script support to [Loco](https://loco.rs)\n- [loco-oauth2](https://github.com/yinho999/loco-oauth2) - Loco OAuth2 is a simple OAuth2 initializer for the Loco API. It is designed to be a tiny and easy-to-use library for implementing OAuth2 in your application.\n\n## App Examples\n\n- [Chat rooms](https://github.com/loco-rs/chat-rooms) - With opening a web socket\n- [Todo list](https://github.com/loco-rs/todo-list) - working with rest API\n"
  },
  {
    "path": "docs-site/content/docs/resources/faq.md",
    "content": "+++\ntitle = \"FAQ\"\ndescription = \"Answers to frequently asked questions.\"\ndate = 2021-05-01T19:30:00+00:00\nupdated = 2021-05-01T19:30:00+00:00\ndraft = false\nweight = 2\nsort_by = \"weight\"\ntemplate = \"docs/page.html\"\n\n[extra]\ntoc = true\ntop = false\nflair =[]\n+++\n\n<details>\n<summary>How can I automatically reload code?</summary>\n\nTry [cargo watchexec](https://crates.io/crates/watchexec):\n\n```\n$ watchexec --notify -r -- cargo loco start\n```\n\nOr [bacon](https://github.com/Canop/bacon)\n\n```\n$ bacon run\n```\n\n</details>\n<br/>\n<details>\n<summary>Do I have to have `cargo` to run tasks or other things?</summary>\nYou don't have to run things through `cargo` but in development it's highly recommended. If you build `--release`, your binary contains everything including your code and `cargo` or Rust is not needed.\n</details>\n\n<br/>\n\n<details>\n<summary>Is this production ready?</summary>\n\nLoco is still in its beginning, but its roots are not. It's almost a rewrite of `Hyperstackjs.io`, and Hyperstack is based on an internal Rails-like framework which is production ready.\n\nMost of Loco is glue code around Axum, SeaORM, and other stable frameworks, so you can consider that.\n\nAt this stage, at version 0.1.x, we would recommend to _adopt and report issues_ if they arise.\n\n</details>\n\n<br/>\n<details>\n<summary>Adding Custom Middleware in Loco</summary>\nLoco is compatible with Axum middlewares. Simply implement `FromRequestParts` in your custom struct and integrate it within your controller.\n</details>\n\n<br/>\n\n<details>\n<summary>Injecting Custom State or Layers in Loco?</summary>\nYes, you can achieve this by implementing `Hooks::after_routes`. This hook receive Axum routers that Loco has already built, allowing you to seamlessly add any available Axum functions that suit your needs.\n\nIf you need your routes or (404) fallback handler to be affected by loco's middleware, you can add them in `Hooks::before_routes` which is called before the middleware is installed.\n</details>\n\n<br/>\n"
  },
  {
    "path": "docs-site/content/docs/the-app/_index.md",
    "content": "+++\ntitle = \"The App\"\ndescription = \"\"\ntemplate = \"docs/section.html\"\nsort_by = \"weight\"\nweight = 2\ndraft = false\n+++\n"
  },
  {
    "path": "docs-site/content/docs/the-app/controller.md",
    "content": "+++\ntitle = \"Controllers\"\ndescription = \"\"\ndate = 2021-05-01T18:10:00+00:00\nupdated = 2021-05-01T18:10:00+00:00\ndraft = false\nweight = 5\nsort_by = \"weight\"\ntemplate = \"docs/page.html\"\n\n[extra]\nlead = \"\"\ntoc = true\ntop = false\nflair =[]\n+++\n\n`Loco` is a framework that wraps around [axum](https://crates.io/crates/axum), offering a straightforward approach to manage routes, middlewares, authentication, and more right out of the box. At any point, you can leverage the powerful axum Router and extend it with your custom middlewares and routes.\n\n# Controllers and Routing\n\n## Adding a controller\n\nProvides a convenient code generator to simplify the creation of a starter controller connected to your project. Additionally, a test file is generated, enabling easy testing of your controller.\n\nGenerate a controller:\n\n```sh\ncargo loco generate controller [OPTIONS] <CONTROLLER_NAME>\n```\n\nAfter generating the controller, navigate to the created file in `src/controllers` to view the controller endpoints. You can also check the testing (in folder tests/requests) documentation for testing this controller.\n\n### Displaying active routes\n\nTo view a list of all your registered controllers, execute the following command:\n\n```sh\n$ cargo loco routes\n\n[GET] /_health\n[GET] /_ping\n[GET] /_readiness\n[POST] /auth/forgot\n[POST] /auth/login\n[POST] /auth/register\n[POST] /auth/reset\n[POST] /auth/verify\n[GET] /auth/current\n```\n\nThis command will provide you with a comprehensive overview of the controllers currently registered in your system.\n\n## AppRoutes\n\n`AppRoutes` is a core component of the `Loco` framework that helps you manage and organize your application's routes. It provides a convenient way to add, prefix, and collect routes from different controllers.\n\n### Features\n\n- **Add Routes**: Easily add routes from different controllers.\n- **Prefix Routes**: Apply a common prefix to a group of routes.\n- **Collect Routes**: Gather all routes into a single collection for further processing.\n\n### Examples\n\n#### Adding Routes\n\nYou can add routes from different controllers to `AppRoutes`:\n\n```rust\nuse loco_rs::controller::AppRoutes;\nuse loco_rs::prelude::*;\nuse axum::routing::get;\n\nfn routes(_ctx: &AppContext) -> AppRoutes {\n  AppRoutes::empty()\n          .add_route(Routes::new().add(\"/\", get(home_handler)))\n          .add_route(Routes::new().add(\"/about\", get(about_handler)))\n}\n```\n\n### Prefixing Routes\n\nApply a common prefix to a group of routes:\n\n```rust\nuse loco_rs::controller::AppRoutes;\nuse loco_rs::prelude::*;\nuse axum::routing::get;\n\nfn routes(_ctx: &AppContext) -> AppRoutes {\n    AppRoutes::empty()\n        .prefix(\"/api\")\n        .add_route(Routes::new().add(\"/users\", get(users_handler)))\n        .add_route(Routes::new().add(\"/posts\", get(posts_handler)))\n}\n```\n\n### Nesting Routes\n\nAppRoutes allows you to nest routes, making it easier to organize and manage complex route hierarchies.\nThis is particularly useful when you have a set of related routes that share a common prefix.\n\n```rust\n use loco_rs::controller::AppRoutes;\nuse loco_rs::prelude::*;\nuse axum::routing::get;\n\nfn routes(_ctx: &AppContext) -> AppRoutes {\n  let route = Routes::new().add(\"/\", get(|| async { \"notes\" }));\n  AppRoutes::with_default_routes()\n        .prefix(\"api\")\n        .add_route(controllers::auth::routes())\n        .nest_prefix(\"v1\")\n        .nest_route(\"/notes\", route)\n}\n```\n\n## Adding state\n\nYour app context and state is held in `AppContext` and is what Loco provides and sets up for you. There are cases where you'd want to load custom data,\nlogic, or entities when the app starts and be available to use in all controllers.\n\nYou could do that by using Axum's `Extension`. Here's an example for loading an LLM model, which is a time consuming task, and then providing it to a controller endpoint, where its already loaded, and fresh for use.\n\nFirst, add a lifecycle hook in `src/app.rs`:\n\n```rust\n    // in src/app.rs, in your Hooks trait impl override the `after_routes` hook:\n\n    async fn after_routes(router: axum::Router, _ctx: &AppContext) -> Result<axum::Router> {\n        // cache should reside at: ~/.cache/huggingface/hub\n        println!(\"loading model\");\n        let model = Llama::builder()\n            .with_source(LlamaSource::llama_7b_code())\n            .build()\n            .unwrap();\n        println!(\"model ready\");\n        let st = Arc::new(RwLock::new(model));\n\n        Ok(router.layer(Extension(st)))\n    }\n```\n\nNext, consume this state extension anywhere you like. Here's an example controller endpoint:\n\n```rust\nasync fn candle_llm(Extension(m): Extension<Arc<RwLock<Llama>>>) -> impl IntoResponse {\n    // use `m` from your state extension\n    let prompt = \"write binary search\";\n    ...\n}\n```\n\n## Global app-wide state\n\nSometimes you might want state that can be shared between controllers, workers, and other areas of your app.\n\nYou can review the example [shared-global-state](https://github.com/loco-rs/shared-global-state) app to see how to integrate `libvips`, which is a C based image manipulation library. `libvips` requires an odd thing from the developer: to keep a single instance of it loaded per app process. We do this by keeping a [single `lazy_static` field](https://github.com/loco-rs/shared-global-state/blob/main/src/app.rs#L27-L34), and referring to it from different places in the app.\n\nThis means you can shape them as a \"regular\" Rust struct that takes a state as a field. Then refer to that field in perform.\n\n[Here's how the worker is initialized](https://github.com/loco-rs/shared-global-state/blob/main/src/workers/downloader.rs#L19) with the global `vips` instance in the `shared-global-state` example.\n\nNote that by-design _sharing state between controllers and workers have no meaning_, because even though you may choose to run workers in the same process as controllers initially (and share state) -- you'd want to quickly switch to proper workers backed by queue and running in a standalone workers process as you scale horizontally, and so workers should by-design have no shared state with controllers, for your own good.\n\n### Shared state in tasks\n\nTasks don't really have a value for shared state, as they have a similar life as any exec'd binary. The process fires up, boots, creates all resources needed (connects to db, etc.), performs the task logic, and then the process terminates.\n\n## Routes in Controllers\n\nControllers define Loco routes capabilities. In the example below, a controller creates one GET endpoint and one POST endpoint:\n\n```rust\nuse axum::routing::{get, post};\nRoutes::new()\n    .add(\"/\", get(hello))\n    .add(\"/echo\", post(echo))\n```\n\nYou can also define a `prefix` for all routes in a controller using the `prefix` function.\n\n## Sending Responses\n\nResponse senders are in the `format` module. Here are a few ways to send responses from your routes:\n\n```rust\n\n// keep a best practice of returning a `Result<impl IntoResponse>` to be able to swap return types transparently\npub async fn list(...) -> Result<impl IntoResponse> // ..\n\n// use `json`, `html` or `text` for simple responses\nformat::json(item)\n\n\n// use `render` for a builder interface for more involved responses. you can still terminate with\n// `json`, `html`, or `text`\nformat::render()\n    .etag(\"foobar\")?\n    .json(Entity::find().all(&ctx.db).await?)\n```\n\n### Content type aware responses\n\nYou can opt-in into the responders mechanism, where a format type is detected\nand handed to you.\n\nUse the `Format` extractor for this:\n\n```rust\npub async fn get_one(\n    respond_to: RespondTo,\n    Path(id): Path<i32>,\n    State(ctx): State<AppContext>,\n) -> Result<Response> {\n    let res = load_item(&ctx, id).await?;\n    match respond_to {\n        RespondTo::Html => format::html(&format!(\"<html><body>{:?}</body></html>\", item.title)),\n        _ => format::json(item),\n    }\n}\n```\n\n### Custom errors\n\nHere is a case where you might want to both render differently based on\ndifferent formats AND ALSO, render differently based on kinds of errors you got.\n\n````rust\npub async fn get_one(\n    respond_to: RespondTo,\n    Path(id): Path<i32>,\n    State(ctx): State<AppContext>,\n) -> Result<Response> {\n    // having `load_item` is useful because inside the function you can call and use\n    // '?' to bubble up errors, then, in here, we centralize handling of errors.\n    // if you want to freely use code statements with no wrapping function, you can\n    // use the experimental `try` feature in Rust where you can do:\n    // ```\n    // let res = try {\n    //     ...\n    //     ...\n    // }\n    //\n    // match res { ..}\n    // ```\n    let res = load_item(&ctx, id).await;\n\n    match res {\n        // we're good, let's render the item based on content type\n        Ok(item) => match respond_to {\n            RespondTo::Html => format::html(&format!(\"<html><body>{:?}</body></html>\", item.title)),\n            _ => format::json(item),\n        },\n        // we have an opinion how to render out validation errors, only in HTML content\n        Err(Error::Model(ModelError::Validation(errors))) => match respond_to {\n            RespondTo::Html => {\n                format::html(&format!(\"<html><body>errors: {errors:?}</body></html>\"))\n            }\n            _ => bad_request(\"opaque message: cannot respond!\"),\n        },\n        // we have no clue what this is, let the framework render default errors\n        Err(err) => Err(err),\n    }\n}\n````\n\nHere, we also \"centralize\" our error handling by first wrapping the workflow in a function, and grabbing the result type.\n\nNext we create a 2 level match to:\n\n1. Match the result type\n2. Match the format type\n\nWhere we lack the knowledge for handling, we just return the error as-is and let the framework render out default errors.\n\n## Creating a Controller Manually\n\n#### 1. Create a Controller File\n\nStart by creating a new file under the path `src/controllers`. For example, let's create a file named `example.rs`.\n\n#### 2. Load the File in mod.rs\n\nEnsure that you load the newly created controller file in the `mod.rs` file within the `src/controllers` folder.\n\n#### 3. Register the Controller in App Hooks\n\nIn your App hook implementation (e.g., App struct), add your controller's `Routes` to `AppRoutes`:\n\n```rust\n// src/app.rs\n\npub struct App;\n#[async_trait]\nimpl Hooks for App {\n    fn routes() -> AppRoutes {\n        AppRoutes::with_default_routes().prefix(\"prefix\")\n            .add_route(controllers::example::routes())\n    }\n    ...\n}\n\n```\n\n## Default Endpoints\n\n### Health check endpoints\n\nThere are three default health check endpoints that are automatically registered in the application:\n\n- `_ping` and `_health`: Can be used by startup probe and liveness probe, they only confirm the server is running (simple 200 OK).\n- `_readiness`: Can be used by readiness probe, tt checks dependencies (DB, Cache, Storage).\n  - If you configure a queue, it will check if the queue is reachable.\n  - If you enable `with-db` feature, it'll also check the database connection.\n  - If you enable `cache_inmem` or `cache_redis` features, it'll also check the cache connection.\n\nWhy we separate these endpoints?\n\n- **Best practices**: Aligns with Kubernetes patterns to avoid removing healthy servers from rotation when dependencies fail temporarily.\n- **Load Balancer Clarity**: A Clear distinction helps load balancers make accurate routing decisions without conflating server and dependency health.\n- **Flexibility**: Splitting endpoints gives users more control to decide which checks to monitor based on their needs (e.g., prioritizing liveness for basic uptime or readiness for full system health).\n- **Debugging**: Separate endpoints make it easier to diagnose issues (e.g., server up but S3 down).\n\n# Middleware\n\nLoco comes with a set of built-in middleware out of the box. Some are enabled by default, while others need to be configured. Middleware registration is flexible and can be managed either through the `*.yaml` environment configuration or directly in the code.\n\n## The default stack\n\nYou get all the enabled middlewares run the following command\n\n<!-- <snip id=\"cli-middleware-list\" inject_from=\"yaml\" template=\"sh\"> -->\n\n```sh\ncargo loco middleware --config\n```\n\n<!-- </snip> -->\n\nThis is the stack in `development` mode:\n\n```sh\n$ cargo loco middleware --config\n\nlimit_payload          {\"body_limit\":{\"Limit\":1000000}}\ncors                   {\"enable\":true,\"allow_origins\":[\"any\"],\"allow_headers\":[\"*\"],\"allow_methods\":[\"*\"],\"expose_header\":[\"\"],\"max_age\":null,\"vary\":[\"origin\",\"access-control-request-method\",\"access-control-request-headers\"]}\ncatch_panic            {\"enable\":true}\netag                   {\"enable\":true}\nlogger                 {\"config\":{\"enable\":true},\"environment\":\"development\"}\nrequest_id             {\"enable\":true}\nfallback               {\"enable\":true,\"code\":200,\"file\":null,\"not_found\":null}\npowered_by             {\"ident\":\"loco.rs\"}\n\n\nremote_ip              (disabled)\ncompression            (disabled)\ntimeout                (disabled)\nstatic_assets          (disabled)\nsecure_headers         (disabled)\n```\n\n### Example: disable all middleware\n\nTake what ever is enabled, and use `enable: false` with the relevant field. If `middlewares:` section in `server` is missing, add it.\n\n```yaml\nserver:\n  middlewares:\n    cors:\n      enable: false\n    catch_panic:\n      enable: false\n    etag:\n      enable: false\n    logger:\n      enable: false\n    request_id:\n      enable: false\n    fallback:\n      enable: false\n```\n\nThe result:\n\n```sh\n$ cargo loco middleware --config\npowered_by             {\"ident\":\"loco.rs\"}\n\n\ncors                   (disabled)\ncatch_panic            (disabled)\netag                   (disabled)\nremote_ip              (disabled)\ncompression            (disabled)\ntimeout_request        (disabled)\nstatic                 (disabled)\nsecure_headers         (disabled)\nlogger                 (disabled)\nrequest_id             (disabled)\nfallback               (disabled)\n```\n\nYou can control the `powered_by` middleware by changing the value for `server.ident`:\n\n```yaml\nserver:\n  ident: my-server #(or empty string to disable)\n```\n\n### Example: add a non-default middleware\n\nLets add the _Remote IP_ middleware to the stack. This is done just by configuration:\n\n```yaml\nserver:\n  middlewares:\n    remote_ip:\n      enable: true\n```\n\nThe result:\n\n```sh\n$ cargo loco middleware --config\n\nlimit_payload          {\"body_limit\":{\"Limit\":1000000}}\ncors                   {\"enable\":true,\"allow_origins\":[\"any\"],\"allow_headers\":[\"*\"],\"allow_methods\":[\"*\"],\"expose_header\":[\"\"],\"max_age\":null,\"vary\":[\"origin\",\"access-control-request-method\",\"access-control-request-headers\"]}\ncatch_panic            {\"enable\":true}\netag                   {\"enable\":true}\nremote_ip              {\"enable\":true,\"trusted_proxies\":null}\nlogger                 {\"config\":{\"enable\":true},\"environment\":\"development\"}\nrequest_id             {\"enable\":true}\nfallback               {\"enable\":true,\"code\":200,\"file\":null,\"not_found\":null}\npowered_by             {\"ident\":\"loco.rs\"}\n```\n\n### Example: change a configuration for an enabled middleware\n\nLet's change the request body limit to `5mb`. When overriding a middleware configuration, rememeber to keep an `enable: true`:\n\n```yaml\nmiddlewares:\n  limit_payload:\n    body_limit: 5mb\n```\n\nThe result:\n\n```sh\n$ cargo loco middleware --config\n\nlimit_payload          {\"body_limit\":{\"Limit\":5000000}}\ncors                   {\"enable\":true,\"allow_origins\":[\"any\"],\"allow_headers\":[\"*\"],\"allow_methods\":[\"*\"],\"expose_headers\":[\"\"],\"max_age\":null,\"vary\":[\"origin\",\"access-control-request-method\",\"access-control-request-headers\"]}\ncatch_panic            {\"enable\":true}\netag                   {\"enable\":true}\nlogger                 {\"config\":{\"enable\":true},\"environment\":\"development\"}\nrequest_id             {\"enable\":true}\nfallback               {\"enable\":true,\"code\":200,\"file\":null,\"not_found\":null}\npowered_by             {\"ident\":\"loco.rs\"}\n\n\nremote_ip              (disabled)\ncompression            (disabled)\ntimeout_request        (disabled)\nstatic                 (disabled)\nsecure_headers         (disabled)\n```\n\n### Authentication\n\nIn the `Loco` framework, middleware plays a crucial role in authentication. `Loco` supports various authentication methods, including JSON Web Token (JWT) and API Key authentication. This section outlines how to configure and use authentication middleware in your application.\n\n#### JSON Web Token (JWT)\n\n##### Configuration\n\nBy default, Loco uses Bearer authentication for JWT. However, you can customize this behavior in the configuration file under the auth.jwt section.\n\n- _Bearer Authentication:_ Keep the configuration blank or explicitly set it as follows:\n\n  ```yaml\n  # Authentication Configuration\n  auth:\n    # JWT authentication\n    jwt:\n      location:\n        from: Bearer\n  ```\n\n- _Cookie Authentication:_ Configure the location from which to extract the token and specify the cookie name:\n\n  ```yaml\n  # Authentication Configuration\n  auth:\n    # JWT authentication\n    jwt:\n      location:\n        from: Cookie\n        name: token\n  ```\n\n- _Query Parameter Authentication:_ Specify the location and name of the query parameter:\n\n  ```yaml\n  # Authentication Configuration\n  auth:\n    # JWT authentication\n    jwt:\n      location:\n        from: Query\n        name: token\n  ```\n\n###### Multiple Location Authentication (Fallback Chain)\n\nYou can configure multiple authentication locations that will be tried in order until one succeeds. This is useful when you want to support multiple authentication methods simultaneously:\n\n```yaml\n# Authentication Configuration\nauth:\n  # JWT authentication\n  jwt:\n    location:\n      - from: Bearer # Try Authorization header first\n      - from: Cookie # If not found, try cookie\n        name: session_token\n      - from: Query # Finally try query parameter\n        name: api_token\n```\n\nWith this configuration:\n\n1. The system first checks for a Bearer token in the `Authorization` header\n2. If not found, it looks for a cookie named `session_token`\n3. If still not found, it checks for a query parameter named `api_token`\n4. If none are found, authentication fails with a descriptive error message\n\nThis approach provides flexibility for different client types:\n\n- Web browsers can use cookies\n- API clients can use Bearer tokens\n- Simple clients can use query parameters\n\n**Error Messages:** When authentication fails, the system provides clear feedback about which locations were attempted:\n\n```\nToken not found in any of the configured locations: [Bearer header, Cookie 'session_token', Query parameter 'api_token']\n```\n\n##### Usage\n\nIn your controller parameters, use `auth::JWT` for authentication. This triggers authentication validation based on the configured settings.\n\n```rust\nuse loco_rs::prelude::*;\n\nasync fn current(\n    auth: auth::JWT,\n    State(_ctx): State<AppContext>,\n) -> Result<Response> {\n    // Your implementation here\n}\n```\n\nAdditionally, you can fetch the current user by replacing auth::JWT with `auth::JWTWithUser<users::Model>`.\n\n#### API Key\n\nFor API Key authentication, use auth::ApiToken. This middleware validates the API key against the user database record and loads the corresponding user into the authentication parameter.\n\n```rust\nuse loco_rs::prelude::*;\n\nasync fn current(\n    auth: auth::ApiToken<users::Model>,\n    State(_ctx): State<AppContext>,\n) -> Result<Response> {\n    // Your implementation here\n}\n```\n\n## Catch Panic\n\nThis middleware catches panics that occur during request handling in the application. When a panic occurs, it logs the error and returns an internal server error response. This middleware helps ensure that the application can gracefully handle unexpected errors without crashing the server.\n\nTo disable the middleware edit the configuration as follows:\n\n```yaml\n#...\nmiddlewares:\n  catch_panic:\n    enable: false\n```\n\n## Limit Payload\n\nThe Limit Payload middleware restricts the maximum allowed size for HTTP request payloads. By default, it is enabled and configured with a 2MB limit.\n\nYou can customize or disable this behavior through your configuration file.\n\n### Set a custom limit\n\n```yaml\n#...\nmiddlewares:\n  limit_payload:\n    body_limit: 5mb\n```\n\n### Disable payload size limitation\n\nTo remove the restriction entirely, set `body_limit` to `disable`:\n\n```yaml\n#...\nmiddlewares:\n  limit_payload:\n    body_limit: disable\n```\n\n##### Usage\n\nIn your controller parameters, use `axum::body::Bytes`.\n\n```rust\nuse loco_rs::prelude::*;\n\nasync fn current(_body: axum::body::Bytes,) -> Result<Response> {\n    // Your implementation here\n}\n```\n\n## Timeout\n\nApplies a timeout to requests processed by the application. The middleware ensures that requests do not run beyond the specified timeout period, improving the overall performance and responsiveness of the application.\n\nIf a request exceeds the specified timeout duration, the middleware will return a `408 Request Timeout` status code to the client, indicating that the request took too long to process.\n\nTo enable the middleware edit the configuration as follows:\n\n```yaml\n#...\nmiddlewares:\n  timeout_request:\n    enable: false\n    timeout: 5000\n```\n\n## Logger\n\nProvides logging functionality for HTTP requests. Detailed information about each request, such as the HTTP method, URI, version, user agent, and an associated request ID. Additionally, it integrates the application's runtime environment into the log context, allowing environment-specific logging (e.g., \"development\", \"production\").\n\nTo disable the middleware edit the configuration as follows:\n\n```yaml\n#...\nmiddlewares:\n  logger:\n    enable: false\n```\n\n## Fallback\n\nWhen choosing the SaaS starter (or any starter that is not API-first), you get a default fallback behavior with the _Loco welcome screen_. This is a development-only mode where a `404` request shows you a nice and friendly page that tells you what happened and what to do next. This also takes preference over the static handler, so make sure to disable it if you want to have static content served.\n\nYou can disable or customize this behavior in your `development.yaml` file. You can set a few options:\n\n```yaml\n# the default pre-baked welcome screen\nfallback:\n  enable: true\n```\n\n```yaml\n# a different predefined 404 page\nfallback:\n  enable: true\n  file: assets/404.html\n```\n\n```yaml\n# a message, and customizing the status code to return 200 instead of 404\nfallback:\n  enable: true\n  code: 200\n  not_found: cannot find this resource\n```\n\nFor production, it's recommended to disable this.\n\n```yaml\n# disable. you can also remove the `fallback` section entirely to disable\nfallback:\n  enable: false\n```\n\n## Remote IP\n\nWhen your app is under a proxy or a load balancer (e.g. Nginx, ELB, etc.), it does not face the internet directly, which is why if you want to find out the connecting client IP, you'll get a socket which indicates an IP that is actually your load balancer instead.\n\nThe load balancer or proxy is responsible for doing the socket work against the real client IP, and then giving your app the load via the proxy back connection to your app.\n\nThis is why when your app has a concrete business need for getting the real client IP you need to use the de-facto standard proxies and load balancers use for handing you this information: the `X-Forwarded-For` header.\n\nLoco provides the `remote_ip` section for configuring the `RemoteIP` middleware:\n\n```yaml\nserver:\n  middleware:\n    # calculate remote IP based on `X-Forwarded-For` when behind a proxy or load balancer\n    # use RemoteIP(..) extractor to get the remote IP.\n    # without this middleware, you'll get the proxy IP instead.\n    # For more: https://github.com/rails/rails/blob/main/actionpack/lib/action_dispatch/middleware/remote_ip.rb\n    #\n    # NOTE! only enable when under a proxy, otherwise this can lead to IP spoofing vulnerabilities\n    # trust me, you'll know if you need this middleware.\n    remote_ip:\n      enable: true\n      # # replace the default trusted proxies:\n      # trusted_proxies:\n      # - ip range 1\n      # - ip range 2 ..\n    # Generating a unique request ID and enhancing logging with additional information such as the start and completion of request processing, latency, status code, and other request details.\n```\n\nThen, use the `RemoteIP` extractor to get the IP:\n\n```rust\n#[debug_handler]\npub async fn list(ip: RemoteIP, State(ctx): State<AppContext>) -> Result<Response> {\n    println!(\"remote ip {ip}\");\n    format::json(Entity::find().all(&ctx.db).await?)\n}\n```\n\nWhen using the `RemoteIP` middleware, take note of the security implications vs. your current architecture (as noted in the documentation and in the configuration section): if your app is NOT under a proxy, you can be prone to IP spoofing vulnerability because anyone can set headers to arbitrary values, and specifically, anyone can set the `X-Forwarded-For` header.\n\nThis middleware is not enabled by default. Usually, you _will know_ if you need this middleware and you will be aware of the security aspects of using it in the correct architecture. If you're not sure -- don't use it (keep `enable` to `false`).\n\n## Secure Headers\n\nLoco comes with default secure headers applied by the `secure_headers` middleware. This is similar to what is done in the Rails ecosystem with [secure_headers](https://github.com/github/secure_headers).\n\nIn your `server.middleware` YAML section you will find the `github` preset by default (which is what Github and Twitter recommend for secure headers).\n\n```yaml\nserver:\n  middleware:\n    # set secure headers\n    secure_headers:\n      preset: github\n```\n\nYou can also override select headers:\n\n```yaml\nserver:\n  middleware:\n    # set secure headers\n    secure_headers:\n      preset: github\n      overrides:\n        foo: bar\n```\n\nOr start from scratch:\n\n```yaml\nserver:\n  middleware:\n    # set secure headers\n    secure_headers:\n      preset: empty\n      overrides:\n        foo: bar\n```\n\nTo support `htmx`, You can add the following override, to allow some inline running of scripts:\n\n```yaml\nsecure_headers:\n  preset: github\n  overrides:\n    # this allows you to use HTMX, and has unsafe-inline. Remove or consider in production\n    \"Content-Security-Policy\": \"default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src 'unsafe-inline' 'self' https:; style-src 'self' https: 'unsafe-inline'\"\n```\n\n## Compression\n\n`Loco` leverages [CompressionLayer](https://docs.rs/tower-http/0.5.0/tower_http/compression/index.html) to enable a `one click` solution.\n\nTo enable response compression, based on `accept-encoding` request header, simply edit the configuration as follows:\n\n```yaml\n#...\nmiddlewares:\n  compression:\n    enable: true\n```\n\nDoing so will compress each response and set `content-encoding` response header accordingly.\n\n## Static Assets\n\nThe static assets middleware serves static files (e.g., images, CSS, JS) from a specified folder to the client. It also allows configuration of a fallback file to serve in case a requested file is not found, and can serve precompressed files if enabled.\n\n### Basic Configuration\n\n```yaml\n#...\nmiddlewares:\n  static_assets:\n    enable: true\n    folder:\n      uri: \"/static\"\n      path: \"assets/static\"\n    fallback: \"assets/static/404.html\"\n    must_exist: true\n```\n\n### Cache Control\n\nYou can configure cache control headers for static assets to optimize performance. By default, static assets are cached for 1 year (`max-age=31536000`).\n\n```yaml\n#...\nmiddlewares:\n  static_assets:\n    enable: true\n    cache_control: \"max-age=31536000, public\"  # 1 year cache\n    # or\n    cache_control: \"max-age=86400\"  # 1 day cache\n    # or\n    cache_control: \"no-cache\"  # No caching\n    # or\n    cache_control: null  # Disable caching entirely\n```\n\n### Precompressed Assets\n\n`Loco` leverages [ServeDir::precompressed_gzip](https://docs.rs/tower-http/latest/tower_http/services/struct.ServeDir.html#method.precompressed_gzip) to enable a `one click` solution of serving pre compressed assets.\n\nIf a static assets exists on the disk as a `.gz` file, `Loco` will serve it instead of compressing it on the fly.\n\n```yaml\n#...\nmiddlewares:\n  static_assets:\n    enable: true\n    precompressed: true\n```\n\n## CORS\n\nThis middleware enables Cross-Origin Resource Sharing (CORS) by allowing configurable origins, methods, and headers in HTTP requests.\nIt can be tailored to fit various application requirements, supporting permissive CORS or specific rules as defined in the middleware configuration.\n\n```yaml\n#...\nserver:\n  ...\n  middlewares:\n    ...\n    cors:\n      enable: true\n      # Set the value of the [`Access-Control-Allow-Origin`][mdn] header\n      # allow_origins:\n      #   - https://loco.rs\n      # Set the value of the [`Access-Control-Allow-Headers`][mdn] header\n      # allow_headers:\n      # - Content-Type\n      # Set the value of the [`Access-Control-Allow-Methods`][mdn] header\n      # allow_methods:\n      #   - POST\n      # expose_headers:\n      #   - X-CUSTOM-HEADER1\n      #   - X-CUSTOM-HEADER2\n      # Set the value of the [`Access-Control-Max-Age`][mdn] header in seconds\n      # max_age: 3600\n\n```\n\n## Handler and Route based middleware\n\n`Loco` also allow us to apply [layers](https://docs.rs/tower/latest/tower/trait.Layer.html) to specific handlers or\nroutes.\nFor more information on handler and route based middleware, refer to the [middleware](/docs/the-app/controller/#middleware)\ndocumentation.\n\n### Handler based middleware\n\nApply a layer to a specific handler using `layer` method.\n\n```rust\n// src/controllers/auth.rs\npub fn routes() -> Routes {\n    Routes::new()\n        .prefix(\"auth\")\n        .add(\"/register\", post(register).layer(middlewares::log::LogLayer::new()))\n}\n```\n\n### Route based middleware\n\nApply a layer to a specific route using `layer` method.\n\n```rust\n// src/main.rs\npub struct App;\n\n#[async_trait]\nimpl Hooks for App {\n    fn routes(_ctx: &AppContext) -> AppRoutes {\n        AppRoutes::with_default_routes()\n            .add_route(\n                controllers::auth::routes()\n                    .layer(middlewares::log::LogLayer::new()),\n            )\n    }\n}\n```\n\n# Request Validation\n\nRequest validation in Loco ensures that incoming data (JSON payloads, query parameters, or form data) conforms to rules before processing. You can validate in two ways:\n\n- With the derive-based `validator` crate ([documentation](https://github.com/Keats/validator))\n- By implementing Loco’s `ValidatorTrait` for custom validation logic (no external crate required)\n\nLoco’s validation extractors work with either approach, and the `WithMessage` variants return structured JSON errors suitable for client-side feedback.\n\n## Validation Extractors\n\n| Extractor                  | JSON | Content Type                        | Request Body                                                 |\n| -------------------------- | ---- | ----------------------------------- | ------------------------------------------------------------ |\n| `JsonValidate`             | ❌   | `application/json`                  | JSON (e.g., `{\"name\": \"John\", \"email\": \"john@example.com\"}`) |\n| `JsonValidateWithMessage`  | ✅   | `application/json`                  | JSON (e.g., `{\"name\": \"John\", \"email\": \"john@example.com\"}`) |\n| `QueryValidate`            | ❌   | Any                                 | Query string (e.g., `?name=John&email=john@example.com`)     |\n| `QueryValidateWithMessage` | ✅   | Any                                 | Query string (e.g., `?name=John&email=john@example.com`)     |\n| `FormValidate`             | ❌   | `application/x-www-form-urlencoded` | Form data                                                    |\n| `FormValidateWithMessage`  | ✅   | `application/x-www-form-urlencoded` | Form data                                                    |\n\n### Notes:\n\n- **Error Status**: HTTP status codes for invalid data or unsupported `Content-Type`.\n- **Structured JSON Errors**: Provided by `WithMessage` extractors for detailed error reporting.\n- **Supported Content Type**: Specifies the expected request `Content-Type`.\n- **Request Body**: Describes the expected format of the request data.\n\n## Implementing Request Validation\n\n### 1. Define Validation Rules\n\nYou can validate requests in two ways:\n\n- Using the `validator` crate (derive-based)\n- Implementing a custom validator via the `ValidatorTrait` (no external crate required)\n\nDefine a Rust struct with validation rules using the `serde` and `validator` crates.\n\n#### Example: `DataParams` Struct\n\n```rust\nuse serde::Deserialize;\nuse validator::Validate;\n\n#[derive(Debug, Deserialize, Validate)]\npub struct DataParams {\n    #[validate(length(min = 5, message = \"Name must be at least 5 characters long\"))]\n    pub name: String,\n    #[validate(email)]\n    pub email: String,\n}\n```\n\n- **Rules**:\n  - `name`: Requires at least 5 characters.\n  - `email`: Must be a valid email address.\n- The `Validate` macro enables automatic field validation.\n\nAlternatively, implement the `ValidatorTrait` for full control without using the `validator` crate:\n\n```rust\nuse loco_rs::prelude::*;\nuse serde::Deserialize;\nuse std::collections::{BTreeMap, HashMap};\n\n#[derive(Debug, Deserialize)]\npub struct CustomDataParams {\n    pub name: String,\n    pub email: String,\n}\n\nimpl ValidatorTrait for CustomDataParams {\n    fn validate(&self) -> Result<(), ModelValidationErrors> {\n        let mut errors: BTreeMap<String, Vec<ValidationError>> = BTreeMap::new();\n\n        if self.name.len() < 5 {\n            let mut params: HashMap<String, serde_json::Value> = HashMap::new();\n            params.insert(\"min\".to_string(), serde_json::json!(5));\n            params.insert(\"value\".to_string(), serde_json::json!(&self.name));\n            errors.insert(\n                \"name\".to_string(),\n                vec![ValidationError {\n                    code: \"length\".to_string(),\n                    message: Some(\"custom message\".to_string()),\n                    params,\n                }],\n            );\n        }\n\n        if !self.email.contains('@') {\n            let mut params: HashMap<String, serde_json::Value> = HashMap::new();\n            params.insert(\"value\".to_string(), serde_json::json!(&self.email));\n            errors.insert(\n                \"email\".to_string(),\n                vec![ValidationError {\n                    code: \"email\".to_string(),\n                    message: None,\n                    params,\n                }],\n            );\n        }\n\n        if errors.is_empty() {\n            Ok(())\n        } else {\n            Err(ModelValidationErrors { errors })\n        }\n    }\n}\n```\n\nNotes:\n\n- WithMessage extractors return structured errors; when implementing `ValidatorTrait`, use `ModelValidationErrors { errors: BTreeMap<String, Vec<ValidationError>> }` to match the expected shape.\n- `ValidationError` fields: `code: String`, `message: Option<String>`, `params: HashMap<String, serde_json::Value>`. Empty `params` are omitted from JSON automatically.\n- If you prefer derive-based validation, `validator::Validate` is automatically adapted to `ValidatorTrait` behind the scenes.\n\n### 2. Create Handlers with Validation\n\nLoco extractors validate data within HTTP handlers, proceeding with validated data or returning errors.\n\n#### Example 1: JSON Validation with `JsonValidate`\n\n```rust\nuse loco_rs::prelude::*;\n\n#[debug_handler]\npub async fn index(\n    State(_ctx): State<AppContext>,\n    JsonValidate(params): JsonValidate<DataParams>,\n) -> Result<Response> {\n    format::empty()\n}\n```\n\n- Deserializes and validates JSON payloads (e.g., `{\"name\": \"John\", \"email\": \"john@example.com\"}`).\n- Returns **400 Bad Request** on failure.\n\n#### Example 2: Query Validation with `QueryValidate`\n\n```rust\nuse loco_rs::prelude::*;\n\n#[debug_handler]\npub async fn index(\n    State(_ctx): State<AppContext>,\n    QueryValidate(params): QueryValidate<DataParams>,\n) -> Result<Response> {\n    format::empty()\n}\n```\n\n- Validates query parameters (e.g., `?name=John&email=john@example.com`).\n- Returns **400 Bad Request** on failure.\n\n#### Example 3: Form Validation with `FormValidateWithMessage`\n\n```rust\nuse loco_rs::prelude::*;\n\n#[debug_handler]\npub async fn index(\n    State(_ctx): State<AppContext>,\n    FormValidateWithMessage(params): FormValidateWithMessage<DataParams>,\n) -> Result<Response> {\n    format::empty()\n}\n```\n\n- Validates form data (e.g., `name=John&email=john@example.com`) with `application/x-www-form-urlencoded` content type.\n- Returns **400 Bad Request** on failure, with structured JSON errors.\n\n### 3. Structured Error Responses\n\n`WithMessage` extractors (e.g., `JsonValidateWithMessage`) provide detailed JSON error responses.\n\n#### Example Error Response\n\nFor invalid form data (`{\"name\": \"abc\", \"email\": \"invalid_email\"}`):\n\n```json\n{\n  \"errors\": {\n    \"name\": [\n      {\n        \"code\": \"length\",\n        \"message\": \"Name must be at least 5 characters long\",\n        \"params\": {\n          \"min\": 5,\n          \"value\": \"abc\"\n        }\n      }\n    ],\n    \"email\": [\n      {\n        \"code\": \"email\",\n        \"message\": null,\n        \"params\": {\n          \"value\": \"invalid_email\"\n        }\n      }\n    ]\n  }\n}\n```\n\n- **Structure**:\n  - `errors`: Maps fields to arrays of validation errors.\n  - Each error includes `code`, `message`, and `params` for context.\n\n## Key Considerations\n\n- **Extractor Selection**: Choose based on the request data type (JSON, query, or form).\n- **Error Handling**:\n  - Standard extractors return generic HTTP errors.\n  - `WithMessage` extractors provide structured JSON errors for client-side feedback.\n- **Further Reading**: Refer to the [validator crate documentation](https://github.com/Keats/validator) for advanced validation rules.\n\n# Pagination\n\nIn many scenarios, when querying data and returning responses to users, pagination is crucial. In `Loco`, we provide a straightforward method to paginate your data and maintain a consistent pagination response schema for your API responses.\n\nWe assume you have a `notes` entity and/or scaffold (replace this with any entity you like).\n\n## Using pagination\n\n```rust\nuse loco_rs::prelude::*;\n\nlet res = query::fetch_page(&ctx.db, notes::Entity::find(), &query::PaginationQuery::page(2)).await;\n```\n\n## Using pagination With Filter\n\n```rust\nuse loco_rs::prelude::*;\n\nlet pagination_query = query::PaginationQuery {\n    page_size: 100,\n    page: 1,\n};\n\nlet condition = query::condition().contains(notes::Column::Title, \"loco\");\nlet paginated_notes = query::paginate(\n    &ctx.db,\n    notes::Entity::find(),\n    Some(condition.build()),\n    &pagination_query,\n)\n.await?;\n```\n\n- Start by defining the entity you want to retrieve.\n- Create your query condition (in this case, filtering rows that contain \"loco\" in the title column).\n- Define the pagination parameters.\n- Call the paginate function.\n\n### Pagination view\n\nAfter creating getting the `paginated_notes` in the previous example, you can choose which fields from the model you want to return and keep the same pagination response in all your different data responses.\n\nDefine the data you're returning to the user in Loco views. If you're not familiar with views, refer to the [documentation](@/docs/the-app/views.md) for more context.\n\nCreate a notes view file in `src/view/notes` with the following code:\n\n```rust\nuse loco_rs::{\n    controller::views::pagination::{Pager, PagerMeta},\n    prelude::model::query::PaginatedResponse,\n};\nuse serde::{Deserialize, Serialize};\n\nuse crate::models::_entities::notes;\n\n#[derive(Debug, Deserialize, Serialize)]\npub struct ListResponse {\n    id: i32,\n    title: Option<String>,\n    content: Option<String>,\n}\n\n#[derive(Debug, Deserialize, Serialize)]\npub struct PaginationResponse {}\n\nimpl From<notes::Model> for ListResponse {\n    fn from(note: notes::Model) -> Self {\n        Self {\n            id: note.id.clone(),\n            title: note.title.clone(),\n            content: note.content,\n        }\n    }\n}\n\nimpl PaginationResponse {\n    #[must_use]\n    pub fn response(data: PageResponse<notes::Model>, pagination_query: &PaginationQuery) -> Pager<Vec<ListResponse>> {\n        Pager {\n            results: data\n                .page\n                .into_iter()\n                .map(ListResponse::from)\n                .collect::<Vec<ListResponse>>(),\n            info: PagerMeta {\n                page: pagination_query.page,\n                page_size: pagination_query.page_size,\n                total_pages: data.total_pages,\n                total_items: data.total_items,\n            },\n        }\n    }\n}\n```\n\n## Custom Extractors\n\nWhen it is necessary to validate request information contained in the request header, a custom extractor can be implemented for this purpose. For example, in a multi-tenant application that includes the current tenant identifier in the headers, the extractor should retrieve the value, verify its validity in the database, and ensure that the user is authorized to access it. To implement a custom extractor, it is required to implement one of the following traits: FromRequest or FromRequestParts.\n\n```rust\nuse axum::{\n    extract::FromRequestParts,\n    extract::FromRef,\n    http::{request::Parts, StatusCode},\n};\nuse loco_rs::prelude::*;\nuse sea_orm::{EntityTrait, DatabaseConnection};\n\nuse loco_rs::app::AppContext;\nuse crate::models::_entities::companies; // Adjust to your strucuture\n\n#[derive(Debug, Clone)]\npub struct CompanyContext(pub i32, pub String);\n\nimpl<S> FromRequestParts<S> for CompanyContext\nwhere\n    AppContext: FromRef<S>,\n    S: Send + Sync,\n{\n    type Rejection = (StatusCode, String);\n\n    async fn from_request_parts(\n        parts: &mut Parts,\n        state: &S\n    ) -> Result<Self, Self::Rejection> {\n\n        // 1. get header\n        let nickname = parts.headers\n            .get(\"x-my-company\")\n            .and_then(|h| h.to_str().ok())\n            .ok_or((\n                StatusCode::BAD_REQUEST,\n                \"Missing X-MY-COMPANY\".to_string(),\n            ))?\n            .to_string();\n\n        let ctx = AppContext::from_ref(state);\n        let db: &DatabaseConnection = &ctx.db;\n\n        // 3. Search tenant on database\n        let company = companies::Entity::find()\n            .filter(companies::Column::Uuid.eq(nickname.clone()))\n            .one(db)\n            .await\n            .map_err(|e| {\n                (\n                    StatusCode::INTERNAL_SERVER_ERROR,\n                    format!(\"DB error: {}\", e),\n                )\n            })?\n            .ok_or((\n                StatusCode::NOT_FOUND,\n                \"Company not found\".to_string(),\n            ))?;\n\n        Ok(CompanyContext(company.id, nickname))\n    }\n}\n\n```\n\nAfter that just added to your action.\n\n```rust\n#[debug_handler]\npub async fn add(\n    CompanyContext(company_id, nickname): CompanyContext,\n    State(ctx): State<AppContext>,\n    Json(params): Json<Params>,\n) -> Result<Response> {\n\n    // Action logic ...\n\n\n    format::json({ message: \"added!\" })\n}\n```\n\n<div class=\"infobox\">\nMore information about extractors can be found in the <a href=\"https://docs.rs/axum/latest/axum/extract/index.html#the-order-of-extractors\">axum documentation</a>.\n</div>\n\n# Testing\n\nWhen testing controllers, the goal is to call the router's controller endpoint and verify the HTTP response, including the status code, response content, headers, and more.\n\nTo initialize a test request, use `use loco_rs::testing::prelude::*;`, which prepares your app routers, providing the request instance and the application context.\n\nIn the following example, we have a POST endpoint that returns the data sent in the POST request.\n\n```rust\nuse loco_rs::testing::prelude::*;\n\n#[tokio::test]\n#[serial]\nasync fn can_print_echo() {\n    configure_insta!();\n\n    request::<App, _, _>(|request, _ctx| async move {\n        let response = request\n            .post(\"/example\")\n            .json(&serde_json::json!({\"site\": \"Loco\"}))\n            .await;\n\n        assert_debug_snapshot!((response.status_code(), response.text()));\n    })\n    .await;\n}\n```\n\nAs you can see initialize the testing request and using `request` instance calling /example endpoing.\nthe request returns a `Response` instance with the status code and the response test\n\n## Async\n\nWhen writing async tests with database data, it's important to ensure that one test does not affect the data used by other tests. Since async tests can run concurrently on the same database dataset, this can lead to unstable test results.\n\nInstead of using `request`, as described in the documentation for synchronous tests, use the `request_with_create_db` function. This function generates a random database schema name and ensures that the tables are deleted once the test is completed.\n\nNote: If you cancel the test run midway (e.g., by pressing `Ctrl + C`), the cleanup process will not execute, and the database tables will remain. In such cases, you will need to manually remove them.\n\n```rust\nuse loco_rs::testing::prelude::*;\n\n#[tokio::test]\nasync fn can_print_echo() {\n    configure_insta!();\n\n    request_with_create_db::<App, _, _>(|request, _ctx| async move {\n        let response = request\n            .post(\"/example\")\n            .json(&serde_json::json!({\"site\": \"Loco\"}))\n            .await;\n\n        assert_debug_snapshot!((response.status_code(), response.text()));\n    })\n    .await;\n}\n```\n\n## Authenticated Endpoints\n\nThe following example works for both JWT and API_KEY Authentication.\n\n```rust\nuse loco_rs::testing::prelude::*;\nuse super::prepare_data;\n\n#[tokio::test]\n#[serial]\nasync fn can_get_current_user() {\n    configure_insta!();\n\n    request::<App, _, _>(|request, ctx| async move {\n        // Initialize the user\n        let user = prepare_data::init_user_login(&request, &ctx).await;\n        let (auth_key, auth_value) = prepare_data::auth_header(&user.token);\n\n        // Then add the key to the request, usually in the header\n        let response = request\n            .get(\"/example\")\n            .add_header(auth_key, auth_value)\n            .await;\n\n        assert_eq!(\n            response.status_code(),\n            200,\n            \"Current request should succeed\"\n        );\n\n        assert_debug_snapshot!((response.status_code(), response.text()));\n    })\n    .await;\n}\n```\n"
  },
  {
    "path": "docs-site/content/docs/the-app/models.md",
    "content": "+++\ntitle = \"Models\"\ndescription = \"\"\ndate = 2021-05-01T18:10:00+00:00\nupdated = 2024-01-07T21:10:00+00:00\ndraft = false\nweight = 3\nsort_by = \"weight\"\ntemplate = \"docs/page.html\"\n\n[extra]\nlead = \"\"\ntoc = true\ntop = false\nflair =[]\n+++\n\nModels in `loco` mean entity classes that allow for easy database querying and writes, but also migrations and seeding.\n\n## Sqlite vs Postgres\n\nYou might have selected `sqlite` which is the default when you created your new app. Loco allows you to _seamlessly_ move between `sqlite` and `postgres`.\n\nIt is typical that you could use `sqlite` for development, and `postgres` for production. Some people prefer `postgres` all the way for both development and production because they use `pg` specific features. Some people use `sqlite` for production too, these days. Either way -- all valid choices.\n\nTo configure `postgres` instead of `sqlite`, go into your `config/development.yaml` (or `production.yaml`) and set this, assuming your app is named `myapp`:\n\n```yaml\ndatabase:\n  uri: \"{{ get_env(name=\"DATABASE_URL\", default=\"postgres://loco:loco@localhost:5432/myapp_development\") }}\"\n```\n\n<div class=\"infobox\">\nYour local postgres database should be with <code>loco:loco</code> and a db named <code>myapp_development</code>. For test and production, your DB should be named <code>myapp_test</code> and <code>myapp_production</code> respectively.\n</div>\n\nFor your convenience, here is a docker command to start up a Postgresql database server:\n\n<!-- <snip id=\"postgres-run-docker-command\" inject_from=\"yaml\" template=\"sh\"> -->\n\n```sh\ndocker run -d -p 5432:5432 \\\n  -e POSTGRES_USER=loco \\\n  -e POSTGRES_DB=myapp_development \\\n  -e POSTGRES_PASSWORD=\"loco\" \\\n  postgres:15.3-alpine\n```\n\n<!-- </snip> -->\n\nFinally you can also use the doctor command to validate your connection:\n\n<!-- <snip id=\"doctor-command\" inject_from=\"yaml template=\"sh\"> -->\n\n```sh\n$ cargo loco doctor\n    Finished dev [unoptimized + debuginfo] target(s) in 0.32s\n    Running `target/debug/myapp-cli doctor`\n✅ SeaORM CLI is installed\n✅ DB connection: success\n✅ Redis connection: success\n```\n\n<!-- </snip> -->\n\n## Fat models, slim controllers\n\n`loco` models **are designed after active record**. This means they're a central point in your universe, and every logic or operation your app has should be there.\n\nIt means that `User::create` creates a user **but also** `user.buy(product)` will buy a product.\n\nIf you agree with that direction you'll get these for free:\n\n- **Time-effective testing**, because testing your model tests most if not all of your logic and moving parts.\n- Ability to run complete app workflows **from _tasks_, or from workers and other places**.\n- Effectively **compose features** and use cases by combining models, and nothing else.\n- Essentially, **models become your app** and controllers are just one way to expose your app to the world.\n\nWe use [`SeaORM`](https://www.sea-ql.org/SeaORM/) as the main ORM behind our ActiveRecord abstraction.\n\n- _Why not Diesel?_ - although Diesel has better performance, its macros, and general approach felt incompatible with what we were trying to do\n- _Why not sqlx_ - SeaORM uses sqlx under the hood, so the plumbing is there for you to use `sqlx` raw if you wish.\n\n## Example model\n\nThe life of a `loco` model starts with a _migration_, then an _entity_ Rust code is generated for you automatically from the database structure:\n\n```\nsrc/\n  models/\n    _entities/   <--- autogenerated code\n      users.rs   <--- the bare entity and helper traits\n    users.rs  <--- your custom activerecord code\n```\n\nUsing the `users` activerecord would be just as you use it under SeaORM [see examples here](https://www.sea-ql.org/SeaORM/docs/next/basic-crud/select/)\n\nAdding functionality to the `users` activerecord is by _extension_:\n\n```rust\nimpl super::_entities::users::ActiveModel {\n    /// .\n    ///\n    /// # Errors\n    ///\n    /// .\n    pub fn foobar(&self) -> Result<(), DbErr> {\n        // implement and get back a `user.foobar()`\n    }\n}\n```\n\n# Crafting models\n\n## The model generator\n\nTo add a new model the model generator creates a migration, runs it, and then triggers an entities sync from your database schema which will hydrate and create your model entities.\n\n```\n$ cargo loco generate model posts title:string! content:text user:references\n```\n\nWhen a model is added via migration, the following default fields are provided:\n\n- `created_at` (ts!): This is a timestamp indicating when your model was created.\n- `updated_at` (ts!): This is a timestamp indicating when your model was updated.\n\nThese fields are ignored if you provide them in your migration command.\n\n### Controlling Timestamps\n\nBy default, all models include timestamp columns (`created_at` and `updated_at`). If you want to create a model without these timestamp columns, you can use the `--without-tz` flag:\n\n```sh\n# Generate model with timestamps (default behavior)\n$ cargo loco g model posts title:string content:text\n\n# Generate model without timestamps\n$ cargo loco g model posts title:string content:text --without-tz\n```\n\nThis flag is also available for migrations and scaffolds:\n\n```sh\n# Generate migration without timestamps\n$ cargo loco g migration CreatePosts title:string --without-tz\n\n# Generate scaffold without timestamps\n$ cargo loco g scaffold posts title:string --api --without-tz\n\n# Generate join table without timestamps\n$ cargo loco g migration CreateJoinTableUsersAndPosts --without-tz\n```\n\nWhen using `--without-tz`, the generated table will not include the `created_at` and `updated_at` columns, giving you full control over timestamp management in your models.\n\n### Field syntax\n\nEach field type may include either the `!` or `^` suffix:\n\n- `!` indicates that the field is **required** (i.e. `NOT NULL` in the database),\n- `^` indicates that the field must be **unique**.\n\nIf no suffix is used, then the field can be null.\n\n### Data types\n\nFor schema data types, you can use the following mapping to understand the schema:\n\n```rust\n(\"uuid^\", \"uuid_uniq\"),\n(\"uuid\", \"uuid_null\"),\n(\"uuid!\", \"uuid\"),\n(\"string\", \"string_null\"),\n(\"string!\", \"string\"),\n(\"string^\", \"string_uniq\"),\n(\"text\", \"text_null\"),\n(\"text!\", \"text\"),\n(\"text^\", \"text_uniq\"),\n(\"small_unsigned^\", \"small_unsigned_uniq\"),\n(\"small_unsigned\", \"small_unsigned_null\"),\n(\"small_unsigned!\", \"small_unsigned\"),\n(\"big_unsigned^\", \"big_unsigned\"),\n(\"big_unsigned\", \"big_unsigned_null\"),\n(\"big_unsigned!\", \"big_unsigned_uniq\"),\n(\"small_int\", \"small_integer_null\"),\n(\"small_int!\", \"small_integer\"),\n(\"small_int^\", \"small_integer_uniq\"),\n(\"int\", \"integer_null\"),\n(\"int!\", \"integer\"),\n(\"int^\", \"integer_uniq\"),\n(\"big_int\", \"big_integer_null\"),\n(\"big_int!\", \"big_integer\"),\n(\"big_int^\", \"big_integer_uniq\"),\n(\"float\", \"float_null\"),\n(\"float!\", \"float\"),\n(\"float^\", \"float_uniq\"),\n(\"double\", \"double_null\"),\n(\"double!\", \"double\"),\n(\"double^\", \"double_uniq\"),\n(\"decimal\", \"decimal_null\"),\n(\"decimal!\", \"decimal\"),\n(\"decimal_len\", \"decimal_len_null\"),\n(\"decimal_len!\", \"decimal_len\"),\n(\"decimal^\", \"decimal_uniq\"),\n(\"bool\", \"boolean_null\"),\n(\"bool!\", \"boolean\"),\n(\"tstz\", \"timestamp_with_time_zone_null\"),\n(\"tstz!\", \"timestamp_with_time_zone\"),\n(\"date\", \"date_null\"),\n(\"date!\", \"date\"),\n(\"date^\", \"date_uniq\"),\n(\"date_time\", \"date_time_null\"),\n(\"date_time!\", \"date_time\"),\n(\"date_time^\", \"date_time_uniq\"),\n(\"blob\", \"blob_null\"),\n(\"blob!\", \"blob\"),\n(\"blob^\", \"blob_uniq\"),\n(\"json\", \"json_null\"),\n(\"json!\", \"json\"),\n(\"jsonb\", \"json_binary_null\"),\n(\"jsonb!\", \"json_binary\"),\n(\"jsonb^\", \"jsonb_uniq\"),\n(\"money\", \"money_null\"),\n(\"money!\", \"money\"),\n(\"money^\", \"money_uniq\"),\n(\"unsigned\", \"unsigned_null\"),\n(\"unsigned!\", \"unsigned\"),\n(\"unsigned^\", \"unsigned_uniq\"),\n(\"binary_len\", \"binary_len_null\"),\n(\"binary_len!\", \"binary_len\"),\n(\"binary_len^\", \"binary_len_uniq\"),\n(\"var_binary\", \"var_binary_null\"),\n(\"var_binary!\", \"var_binary\"),\n(\" array\", \"array\"),\n(\" array!\", \"array\"),\n(\" array^\", \"array\"),\n```\n\nLoco makes used of `references` type to define foreign-key relations between the model being generated and the model we wish to refer to. Do note, however, that there are two ways to use this special type:\n\n1. `<other_model>:references`\n2. `<other_model>:references:<column_name>`\n\nThe first one (`<other_model>:references`) is used to, as already clear by the semantics, create a foreign-key relation to an already existing model (`other_model` in this case). However, the **field name is implied**.\n\ne.g. If we wish to create a new model named `post`, and it must have a field/column referring to the `users` table which already exists (in new loco project with migrations applied), we will use the following command:\n\n```\ncargo loco g model post title:string user:references\n```\n\nUsing `user:references` uses the special `<other_model>:references` type, which will create a relationship between the `post` (our new model) and a `user` (pre-existing model), adding a `user_id` (implied field name) reference field to the `posts` table.\n\n### Nullable Foreign Keys\n\nIf you want to create a **nullable foreign key** (i.e., a reference that can be `NULL` in the database), simply add a `?` after `references`:\n\n```\ncargo loco g model post title:string user:references?\n```\n\nThis will create a `user_id` column on the `posts` table that is nullable, and the foreign key constraint will use `ON DELETE SET NULL` and `ON UPDATE NO ACTION`.\n\n- `user:references` → `user_id` is NOT NULL (required foreign key)\n- `user:references?` → `user_id` is NULLABLE (optional foreign key)\n\nOn the other hand, using the second approach (`<other_model>:references:<column_name>`) gives us the luxury of being able to name the field/column as per our liking. Therefore, taking the previous example itself, if we wish to create a `post` table having a title, and a foreign key that points to, perhaps the author, we will use the same previous command, but with a nimble modification:\n\n```\ncargo loco g model post title:string user:references:authored_by\n```\n\nUsing `user:references:authored_by` uses the special `<other_model>:references:<column_name>` type, which will create a relationship between the `post` and the `user`, adding an `authored_by` (explicit field name) reference field to the `posts` table, instead of `user_id`.\n\nYou can generate an empty model:\n\n```\n$ cargo loco generate model posts\n```\n\nOr a data model, without any references:\n\n```\n$ cargo loco generate model posts title:string! content:text\n```\n\n## Migrations\n\nOther than using the model generator, you drive your schema by _creating migrations_.\n\n```\n$ cargo loco generate migration <name of migration> [name:type, name:type ...]\n```\n\nThis creates a migration in the root of your project in `migration/`.\n\nYou can apply it:\n\n```\n$ cargo loco db migrate\n```\n\nAnd generate back entities (Rust code) from it:\n\n```\n$ cargo loco db entities\n```\n\nLoco is a migration-first framework, similar to Rails. Which means that when you want to add models, data fields, or model oriented changes - you start with a migration that describes it, and then you apply the migration to get back generated entities in `model/_entities`.\n\nThis enforces _everything-as-code_, _reproducibility_ and _atomicity_, where no knowledge of the schema goes missing.\n\n**Naming the migration is important**, the type of migration that is being generated is inferred from the migration name.\n\n### Create a new table\n\n- Name template: `Create___`\n- Example: `CreatePosts`\n\n```\n$ cargo loco g migration CreatePosts title:string content:string\n```\n\n### Add columns\n\n- Name template: `Add___To___`\n- Example: `AddNameAndAgeToUsers` (the string `NameAndAge` does not matter, you specify columns individually, however `Users` does matter because this will be the name of the table)\n\n```\n$ cargo loco g migration AddNameAndAgeToUsers name:string age:int\n```\n\n### Remove columns\n\n- Name template: `Remove___From___`\n- Example: `RemoveNameAndAgeFromUsers` (same note exists as in _add columns_)\n\n```\n$ cargo logo g migration RemoveNameAndAgeFromUsers name:string age:int\n```\n\n### Add references\n\n- Name template: `Add___RefTo___`\n- Example: `AddUserRefToPosts` (`User` does not matter, as you specify one or many references individually, `Posts` does matter as it will be the table name in the migration)\n\n```\n$ cargo loco g migration AddUserRefToPosts user:references\n```\n\n### Create a join table\n\n- Name template: `CreateJoinTable___And___` (supported between 2 tables)\n- Example: `CreateJoinTableUsersAndGroups`\n\n```\n$ cargo loco g migration CreateJoinTableUsersAndGroups count:int\n```\n\nYou can also add some state columns regarding the relationship (such as `count` here).\n\n### Create an empty migration\n\nUse any descriptive name for a migration that does not fall into one of the above patterns to create an empty migration.\n\n```\n$ cargo loco g migration FixUsersTable\n```\n\n### Down Migrations\n\nIf you realize that you made a mistake, you can always undo the migration. This will undo the changes made by the migration (assuming that you added the appropriate code for `down` in the migration).\n\n<!-- <snip id=\"migrate-down-command\" inject_from=\"yaml\" template=\"sh\"> -->\n\n```sh\ncargo loco db down\n```\n\n<!-- </snip> -->\n\nThe `down` command on its own will rollback only the last migration. If you want to rollback multiple migrations, you can specify the number of migrations to rollback.\n\n<!-- <snip id=\"migrate-down-n-command\" inject_from=\"yaml\" template=\"sh\"> -->\n\n```sh\ncargo loco db down 2\n```\n\n<!-- </snip> -->\n\n### Verbs, singular and plural\n\n- **references**: use **singular** for the table name, and a `<other_model>:references` type. `user:references` (references `Users`), `vote:references` (references `Votes`). `<other_model>:references:<column_name>` is also available `train:references:departing_train` (references `Trains`).\n- **column names**: anything you like. Prefer `snake_case`.\n- **table names**: **plural, snake case**. `users`, `draft_posts`.\n- **migration names**: anything that can be a file name, prefer snake case. `create_table_users`, `add_vote_id_to_movies`.\n- **model names**: generated automatically for you. Usually the generated name is pascal case, plural. `Users`, `UsersVotes`.\n\nHere are some examples showcasing the naming conventions:\n\n```sh\n$ cargo loco generate model movies long_title:string user:references:added_by director:references\n```\n\n- model name in plural: `movies`\n- reference director is in singular: `director:references`\n- reference added_by is an explicit name in singular, the referenced model remains singular: `user:references:added_by`\n- column name in snake case: `long_title:string`\n\n### Authoring migrations\n\nTo use the migrations DSL, make sure you have the following `loco_rs::schema::*` import and SeaORM `prelude`.\n\n```rust\nuse loco_rs::schema::*;\nuse sea_orm_migration::prelude::*;\n```\n\nThen, create a struct:\n\n```rust\n#[derive(DeriveMigrationName)]\npub struct Migration;\n```\n\nAnd then implement your migration (see below).\n\n**Create a table**\n\nCreate a table, provide two arrays: (1) columns (2) references.\n\nLeave references empty to not create any reference fields.\n\n```rust\nimpl MigrationTrait for Migration {\n    async fn up(&self, m: &SchemaManager) -> Result<(), DbErr> {\n        create_table(\n            m,\n            \"posts\",\n            &[\n                (\"title\", ColType::StringNull),\n                (\"content\", ColType::StringNull),\n            ],\n            &[],\n        )\n        .await\n    }\n\n    async fn down(&self, m: &SchemaManager) -> Result<(), DbErr> {\n        drop_table(m, \"posts\").await\n    }\n}\n```\n\n**Create a join table**\n\nProvide the references to the second array argument. Use an empty string `\"\"` to indicate you want us to generate a reference column name for you (e.g. a `user` reference will imply connecting the `users` table through a `user_id` column in `group_users`).\n\nProvide a non-empty string to indicate a specific name for the reference column name.\n\n```rust\nimpl MigrationTrait for Migration {\n    async fn up(&self, m: &SchemaManager) -> Result<(), DbErr> {\n        create_join_table(m, \"group_users\", &[], &[(\"user\", \"\"), (\"group\", \"\")]).await\n    }\n\n    async fn down(&self, m: &SchemaManager) -> Result<(), DbErr> {\n        drop_table(m, \"group_users\").await\n    }\n}\n```\n\n**Add a column**\n\nAdd a single column. You can use as many such statements as you like in a single migration (to add multiple columns).\n\n```rust\nimpl MigrationTrait for Migration {\n    async fn up(&self, m: &SchemaManager) -> Result<(), DbErr> {\n        add_column(m, \"users\", \"amount\", ColType::DecimalLenNull(24,8)).await?;\n        Ok(())\n    }\n\n    async fn down(&self, m: &SchemaManager) -> Result<(), DbErr> {\n        remove_column(m, \"users\", \"amount\").await?;\n        Ok(())\n    }\n}\n```\n\n### Authoring advanced migrations\n\nUsing the `manager` directly lets you access more advanced operations while authoring your migrations.\n\n**Add index**\n\nYou can copy some of this code for adding an index\n\n```rust\n    async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {\n        let db = manager.get_connection();\n        let _ = db\n            .execute_unprepared(\"CREATE INDEX idx-movies-rating ON movies (rating);\")\n            .await?;\n        Ok(())\n    }\n```\n\n**Create a data fix**\n\nCreating a data fix in a migration is easy - just use SQL statements as you like:\n\n```rust\n  async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {\n\n    let db = manager.get_connection();\n\n    // issue SQL queries with `db`\n    // https://www.sea-ql.org/SeaORM/docs/basic-crud/raw-sql/#use-raw-query--execute-interface\n\n    Ok(())\n  }\n```\n\nHaving said that, it's up to you to code your data fixes in:\n\n- `task` - where you can use high level models\n- `migration` - where you can both change structure and fix data stemming from it with raw SQL\n- or an ad-hoc `playground` - where you can use high level models or experiment with things\n\n### Enum Types\n\nEnum types allow you to create columns with a predefined set of values. While enum types are not supported via the CLI generator, you can create them manually in migrations.\n\n#### Creating Enum Types in Migrations\n\nTo create enum types, you need to manually write a migration. Here's an example:\n\n```rust\nuse loco_rs::schema::*;\nuse sea_orm_migration::prelude::*;\n\n#[derive(DeriveMigrationName)]\npub struct Migration;\n\n#[async_trait::async_trait]\nimpl MigrationTrait for Migration {\n    async fn up(&self, m: &SchemaManager) -> Result<(), DbErr> {\n        create_table(\n            m,\n            \"task\",\n            &[\n                (\"id\", ColType::PkAuto),\n                (\"name\", ColType::StringNull),\n                (\n                    \"status\",\n                    ColType::Enum(\n                        \"product_status\".to_string(),\n                        vec![\n                            \"draft\".to_string(),\n                            \"published\".to_string(),\n                            \"archived\".to_string(),\n                        ],\n                    ),\n                ),\n                (\n                    \"priority\",\n                    ColType::EnumWithDefault(\n                        \"priority_level\".to_string(),\n                        vec![\n                            \"low\".to_string(),\n                            \"medium\".to_string(),\n                            \"high\".to_string(),\n                            \"urgent\".to_string(),\n                        ],\n                        \"medium\".to_string(), // default value\n                    ),\n                ),\n            ],\n            &[],\n        )\n        .await\n    }\n\n    async fn down(&self, m: &SchemaManager) -> Result<(), DbErr> {\n        drop_table(m, \"task\").await?;\n        drop_enum_type(m, \"product_status\").await?;\n        drop_enum_type(m, \"priority_level\").await?;\n        Ok(())\n\n    }\n}\n```\n\n#### Available Enum Types\n\n- `ColType::Enum(enum_name, variants)` - Non-nullable enum column\n- `ColType::EnumNull(enum_name, variants)` - Nullable enum column\n- `ColType::EnumWithDefault(enum_name, variants, default_value)` - Non-nullable enum column with default\n- `ColType::EnumNullWithDefault(enum_name, variants, default_value)` - Nullable enum column with default\n\n#### Key Features\n\n- **Automatic enum type creation**: Enum types are automatically created in the database if they don't exist\n- **Default values**: New records automatically get the specified default values if no value is provided\n- **Nullable support**: Both nullable and non-nullable enum columns are supported\n\n> **Note**: Enum types with default values are currently only supported in PostgreSQL.\n\n## Validation\n\nWe use the [validator](https://docs.rs/validator) library under the hood. First, build your validator with the constraints you need, and then implement `Validatable` for your `ActiveModel`.\n\n<!-- <snip id=\"model-validation\" inject_from=\"code\" template=\"rust\"> -->\n\n```rust\n#[derive(Debug, Validate, Deserialize)]\npub struct Validator {\n    #[validate(length(min = 2, message = \"Name must be at least 2 characters long.\"))]\n    pub name: String,\n    #[validate(email(message = \"invalid email\"))]\n    pub email: String,\n}\n\nimpl Validatable for super::_entities::users::ActiveModel {\n    fn validator(&self) -> Box<dyn Validate> {\n        Box::new(Validator {\n            name: self.name.as_ref().to_owned(),\n            email: self.email.as_ref().to_owned(),\n        })\n    }\n}\n```\n\n<!-- </snip> -->\n\nNote that `Validatable` is how you instruct Loco which `Validator` to provide and how to build it from a model.\n\nNow you can use `user.validate()` seamlessly in your code, when it is `Ok` the model is valid, otherwise you'll find validation errors in `Err(...)` available for inspection.\n\n## Relationships\n\n### One to many\n\nHere is how to associate a `Company` with an existing `User` model.\n\n```\n$ cargo loco generate model company name:string user:references\n```\n\nThis will create a migration with a `user_id` field in `Company` which will reference a `User`.\n\n### Many to many\n\nHere is how to create a typical \"votes\" table, which links a `User` and a `Movie` with a many-to-many relationship using a join table.\n\nLet's create a new `Movie` entity:\n\n```\n$ cargo loco generate model movies title:string\n```\n\nAnd now create a join table between `User` (which we already have) and `Movie` (which we just generated) to record votes:\n\n```\n$ cargo loco generate migration CreateJoinTableUsersAndMovies vote:int\n```\n\nThis will create a many-to-many join table named `users_movies` with a composite primary key containing both `user_id` and `movie_id`. The table will also include the `vote` column as specified.\n\nAfter running the migration, you can define the relationships in your entity files:\n\n```rust\n// In src/models/_entities/users.rs\nimpl Related<super::movies::Entity> for Entity {\n    fn to() -> RelationDef {\n        super::users_movies::Relation::Movies.def()\n    }\n    fn via() -> Option<RelationDef> {\n        Some(super::users_movies::Relation::Users.def().rev())\n    }\n}\n\n// In src/models/_entities/movies.rs\nimpl Related<super::users::Entity> for Entity {\n    fn to() -> RelationDef {\n        super::users_movies::Relation::Users.def()\n    }\n    fn via() -> Option<RelationDef> {\n        Some(super::users_movies::Relation::Movies.def().rev())\n    }\n}\n```\n\nUsing `via()` will cause `find_related` to walk through the join table without you needing to know the details of the link table.\n\n## Configuration\n\nModel configuration that's available to you is exciting because it controls all aspects of development, testing, and production, with a ton of goodies, coming from production experience.\n\n<!-- <snip id=\"configuration-database\" inject_from=\"code\" template=\"yaml\"> -->\n\n```yaml\ndatabase:\n  # Database connection URI\n  uri:\n    {\n      {\n        get_env(name=\"DATABASE_URL\",\n        default=\"postgres://loco:loco@localhost:5432/loco_app\"),\n      },\n    }\n  # When enabled, the sql query will be logged.\n  enable_logging: false\n  # Set the timeout duration when acquiring a connection.\n  connect_timeout: 500\n  # Set the idle duration before closing a connection.\n  idle_timeout: 500\n  # Minimum number of connections for a pool.\n  min_connections: 1\n  # Maximum number of connections for a pool.\n  max_connections: 1\n  # Run migration up when application loaded\n  auto_migrate: true\n  # Truncate database when application loaded. This is a dangerous operation, make sure that you using this flag only on dev environments or test mode\n  dangerously_truncate: false\n  # Recreating schema when application loaded.  This is a dangerous operation, make sure that you using this flag only on dev environments or test mode\n  dangerously_recreate: false\n```\n\n<!-- </snip>-->\n\nBy combining these flags, you can create different experiences to help you be more productive.\n\nYou can truncate before an app starts -- which is useful for running tests, or you can recreate the entire DB when the app starts -- which is useful for integration tests or setting up a new environment. In production, you want these turned off (hence the \"dangerously\" part).\n\n# Seeding\n\n`Loco` comes equipped with a convenient `seeds` feature, streamlining the process for quick and easy database reloading. This functionality proves especially invaluable during frequent resets in development and test environments. Let's explore how to get started with this feature:\n\n## Creating a new seed\n\n### 1. Creating a new seed file\n\nNavigate to `src/fixtures` and create a new seed file. For instance:\n\n```\nsrc/\n  fixtures/\n    users.yaml\n```\n\nIn this yaml file, enlist a set of database records for insertion. Each record should encompass the mandatory database fields, based on your database constraints. Optional values are at your discretion. Suppose you have a database DDL like this:\n\n```sql\nCREATE TABLE public.users (\n\tid serial4 NOT NULL,\n\temail varchar NOT NULL,\n\t\"password\" varchar NOT NULL,\n\treset_token varchar NULL,\n\tcreated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,\n\tCONSTRAINT users_email_key UNIQUE (email),\n\tCONSTRAINT users_pkey PRIMARY KEY (id)\n);\n```\n\nThe mandatory fields include `id`, `password`, `email`, and `created_at`. The reset token can be left empty. Your migration content file should resemble the following:\n\n```yaml\n---\n- id: 1\n  email: user1@example.com\n  password: \"$2b$12$gf4o2FShIahg/GY6YkK2wOcs8w4.lu444wP6BL3FyjX0GsxnEV6ZW\"\n  created_at: \"2023-11-12T12:34:56.789\"\n- id: 2\n  pid: 22222222-2222-2222-2222-222222222222\n  email: user2@example.com\n  reset_token: \"SJndjh2389hNJKnJI90U32NKJ\"\n  password: \"$2b$12$gf4o2FShIahg/GY6YkK2wOcs8w4.lu444wP6BL3FyjX0GsxnEV6ZW\"\n  created_at: \"2023-11-12T12:34:56.789\"\n```\n\n### Connect the seed\n\nIntegrate your seed into the app's Hook implementations by following these steps:\n\n1. Navigate to your app's Hook implementations.\n2. Add the seed within the seed function implementation. Here's an example in Rust:\n\n```rs\nimpl Hooks for App {\n    // Other implementations...\n\n    async fn seed(ctx: &AppContext, base: &Path) -> Result<()> {\n        db::seed::<users::ActiveModel>(&ctx.db, &base.join(\"users.yaml\").display().to_string()).await?;\n        Ok(())\n    }\n}\n\n```\n\nThis implementation ensures that the seed is executed when the seed function is called. Adjust the specifics based on your application's structure and requirements.\n\n## Managing Seed via CLI\n\n- **Reset the Database**  \n  Clear all existing data before importing seed files. This is useful when you want to start with a fresh database state, ensuring no old data remains.\n- **Dump Database Tables to Files**  \n  Export the contents of your database tables to files. This feature allows you to back up the current state of your database or prepare data for reuse across environments.\n\nTo access the seed commands, use the following CLI structure:\n\n<!-- <snip id=\"seed-help-command\" inject_from=\"yaml\" action=\"exec\" template=\"sh\"> -->\n\n```sh\nSeed your database with initial data or dump tables to files\n\nUsage: demo_app-cli db seed [OPTIONS]\n\nOptions:\n  -r, --reset                      Clears all data in the database before seeding\n  -d, --dump                       Dumps all database tables to files\n      --dump-tables <DUMP_TABLES>  Specifies specific tables to dump\n      --from <FROM>                Specifies the folder containing seed files (defaults to 'src/fixtures') [default: src/fixtures]\n  -e, --environment <ENVIRONMENT>  Specify the environment [default: development]\n  -h, --help                       Print help\n  -V, --version                    Print version\n```\n\n<!-- </snip> -->\n\n### Using a Test\n\n1. Enable the testing feature (`testing`)\n\n2. In your test section, follow the example below:\n\n```rust\nuse loco_rs::testing::prelude::*;\n\n#[tokio::test]\n#[serial]\nasync fn handle_create_with_password_with_duplicate() {\n\n    let boot = boot_test::<App, Migrator>().await;\n    seed::<App>(&boot.app_context).await.unwrap();\n    assert!(get_user_by_id(1).ok());\n}\n```\n\n# Multi-DB\n\n`Loco` enables you to work with more than one database and share instances across your application.\n\n## Extra DB\n\nTo set up an additional database, begin with database connections and configuration. The recommended approach is to navigate to your configuration file and add the following under [settings](@/docs/the-app/your-project.md#settings):\n\n```yaml\ninitializers:\n  extra_db:\n    uri: postgres://loco:loco@localhost:5432/loco_app\n    enable_logging: false\n    connect_timeout: 500\n    idle_timeout: 500\n    min_connections: 1\n    max_connections: 1\n    auto_migrate: true\n    dangerously_truncate: false\n    dangerously_recreate: false\n```\n\nLoad this [initializer](@/docs/extras/pluggability.md#initializers) into `initializers` hook like this example\n\n```rs\nasync fn initializers(ctx: &AppContext) -> Result<Vec<Box<dyn Initializer>>> {\n        let  initializers: Vec<Box<dyn Initializer>> = vec![\n            Box::new(loco_rs::initializers::extra_db::ExtraDbInitializer),\n        ];\n\n        Ok(initializers)\n    }\n```\n\nNow, you can use the secondary database in your controller:\n\n```rust\nuse sea_orm::DatabaseConnection;\nuse axum::{response::IntoResponse, Extension};\n\npub async fn list(\n    State(ctx): State<AppContext>,\n    Extension(secondary_db): Extension<DatabaseConnection>,\n) -> Result<impl IntoResponse> {\n  let res = Entity::find().all(&secondary_db).await;\n}\n```\n\n## Multi-DB (multi-tenant)\n\nTo connect more than two different databases, the database configuration should look like this:\n\n```yaml\ninitializers:\n  multi_db:\n    secondary_db:\n      uri: postgres://loco:loco@localhost:5432/loco_app\n      enable_logging: false\n      connect_timeout: 500\n      idle_timeout: 500\n      min_connections: 1\n      max_connections: 1\n      auto_migrate: true\n      dangerously_truncate: false\n      dangerously_recreate: false\n    third_db:\n      uri: postgres://loco:loco@localhost:5432/loco_app\n      enable_logging: false\n      connect_timeout: 500\n      idle_timeout: 500\n      min_connections: 1\n      max_connections: 1\n      auto_migrate: true\n      dangerously_truncate: false\n      dangerously_recreate: false\n```\n\nNext load this [initializer](@/docs/extras/pluggability.md#initializers) into `initializers` hook like this example\n\n```rs\nasync fn initializers(ctx: &AppContext) -> Result<Vec<Box<dyn Initializer>>> {\n        let  initializers: Vec<Box<dyn Initializer>> = vec![\n            Box::new(loco_rs::initializers::multi_db::MultiDbInitializer),\n        ];\n\n        Ok(initializers)\n    }\n```\n\nNow, you can use the multiple databases in your controller:\n\n```rust\nuse sea_orm::DatabaseConnection;\nuse axum::{response::IntoResponse, Extension};\nuse loco_rs::db::MultiDb;\n\npub async fn list(\n    State(ctx): State<AppContext>,\n    Extension(multi_db): Extension<MultiDb>,\n) -> Result<impl IntoResponse> {\n  let third_db = multi_db.get(\"third_db\")?;\n  let res = Entity::find().all(third_db).await;\n}\n```\n\n# Testing\n\nIf you used the generator to crate a model migration, you should also have an auto generated model test in `tests/models/posts.rs` (remember we generated a model named `post`?)\n\nA typical test contains everything you need to set up test data, boot the app, and reset the database automatically before the testing code runs. It looks like this:\n\n```rust\nuse loco_rs::testing::prelude::*;\n\n#[tokio::test]\n#[serial]\nasync fn can_find_by_pid() {\n    configure_insta!();\n\n    let boot = boot_test::<App, Migrator>().await;\n    seed::<App>(&boot.app_context).await.unwrap();\n\n    let existing_user =\n        Model::find_by_pid(&boot.app_context.db, \"11111111-1111-1111-1111-111111111111\").await;\n    let non_existing_user_results =\n        Model::find_by_email(&boot.app_context.db, \"23232323-2323-2323-2323-232323232323\").await;\n\n    assert_debug_snapshot!(existing_user);\n    assert_debug_snapshot!(non_existing_user_results);\n}\n```\n\nTo simplify the testing process, `Loco` provides helpful functions that make writing tests more convenient. Ensure you enable the testing feature in your `Cargo.toml`:\n\n```toml\n[dev-dependencies]\nloco-rs = { version = \"*\",  features = [\"testing\"] }\n```\n\n## Database cleanup\n\nIn some cases, you may want to run tests with a clean dataset, ensuring that each test is independent of others and not affected by previous data. To enable this feature, modify the `dangerously_truncate` option to true in the `config/test.yaml` file under the database section. This setting ensures that Loco truncates all data before each test that implements the boot app.\n\n> ⚠️ Caution: Be cautious when using this feature to avoid unintentional data loss, especially in a production environment.\n\n- When doing it recommended to run all the relevant task in with [serial](https://crates.io/crates/rstest) crate.\n- To decide which tables you want to truncate, add the entity model to the App hook:\n\n```rust\npub struct App;\n#[async_trait]\nimpl Hooks for App {\n    //...\n    async fn truncate(ctx: &AppContext) -> Result<()> {\n        // truncate_table(&ctx.db, users::Entity).await?;\n        Ok(())\n    }\n\n}\n```\n\n## Async\n\nWhen writing async tests with database data, it's important to ensure that one test does not affect the data used by other tests. Since async tests can run concurrently on the same database dataset, this can lead to unstable test results.\n\nInstead of using `boot_test`, as described in the documentation for synchronous tests, use the `boot_test_with_create_db` function. This function generates a random database schema name and ensures that the tables are deleted once the test is completed.\n\nNote: If you cancel the test run midway (e.g., by pressing `Ctrl + C`), the cleanup process will not execute, and the database tables will remain. In such cases, you will need to manually remove them.\n\n```rust\nuse loco_rs::testing::prelude::*;\n\n#[tokio::test]\nasync fn boot_test_with_create_db() {\n    let boot = boot_test_with_create_db::<App, Migrator>().await;\n}\n```\n\n## Seeding\n\n```rust\nuse loco_rs::testing::prelude::*;\n\n#[tokio::test]\n#[serial]\nasync fn is_user_exists() {\n    configure_insta!();\n\n    let boot = boot_test::<App, Migrator>().await;\n    seed::<App>(&boot.app_context).await.unwrap();\n    assert!(get_user_by_id(1).ok());\n\n}\n```\n\nThis documentation provides an in-depth guide on leveraging Loco's testing helpers, covering database cleanup, data cleanup for snapshot testing, and seeding data for tests.\n\n## Snapshot test data cleanup\n\nSnapshot testing often involves comparing data structures with dynamic fields such as `created_date`, `id`, `pid`, etc. To ensure consistent snapshots, Loco defines a list of constant data with regex replacements. These replacements can replace dynamic data with placeholders.\n\nExample using [insta](https://crates.io/crates/insta) for snapshots.\n\nin the following example you can use `cleanup_user_model` which clean all user model data.\n\n```rust\nuse loco_rs::testing::prelude::*;\n\n#[tokio::test]\n#[serial]\nasync fn can_create_user() {\n    request::<App, Migrator, _, _>(|request, _ctx| async move {\n        // create user test\n        with_settings!({\n            filters => cleanup_user_model()\n        }, {\n            assert_debug_snapshot!(current_user_request.text());\n        });\n    })\n    .await;\n}\n\n```\n\nYou can also use cleanup constants directly, starting with `CLEANUP_`.\n\n## Customizing Entity Generation\n\nYou can customize how `sea-orm-cli` generates entities by adding configuration to your `Cargo.toml` under the `[package.metadata.db.entity]` section. For example:\n\n```toml\n[package.metadata.db.entity]\nmax-connections = 1\nignore-tables = \"table1,table2\"\nmodel-extra-derives = \"CustomDerive\"\n```\n\nThis configuration will be passed as flags to `sea-orm-cli generate entity` when running `cargo loco db entities`.\n\nNote that some flags like `--output-dir` and `--database-url` cannot be overridden as they are managed by Loco.\n"
  },
  {
    "path": "docs-site/content/docs/the-app/views.md",
    "content": "+++\ntitle = \"Views\"\ndescription = \"\"\ndate = 2021-05-01T18:10:00+00:00\nupdated = 2021-05-01T18:10:00+00:00\ndraft = false\nweight = 4\nsort_by = \"weight\"\ntemplate = \"docs/page.html\"\n\n[extra]\nlead = \"\"\ntoc = true\ntop = false\nflair =[]\n+++\n\nIn `Loco`, the processing of web requests is divided between a controller, model and view.\n\n- **The controller** is handling requests parsing payload, and then control flows to models\n- **The model** primarily deals with communicating with the database and executing CRUD operations when required. As well as modeling all business and domain logic and operations.\n- **The view** takes on the responsibility of assembling and rendering the final response to be sent back to the client.\n\nYou can choose to have _JSON views_, which are JSON responses, or _Template views_ which are powered by a template view engine and eventually are HTML responses. You can also combine both.\n\n<div class=\"infobox\">\nThis is similar in spirit to Rails' `jbuilder` views which are JSON, and regular views, which are HTML, only that in LOCO we focus on being JSON-first.\n</div>\n\n## JSON views\n\nAs an example we have an endpoint that handles user login. When the user is valid we can pass the `user` model into the `LoginResponse` view (which is a JSON view) to return the response.\n\nThere are 3 steps:\n\n1. Parse, accept the request\n2. Create domain objects: models\n3. Hand off the domain model to a view object which **shapes** the final response\n\nThe following Rust code represents a controller responsible for handling user login requests, which handes off _shaping_ of the response to `LoginResponse`.\n\n```rust\nuse crate::{views::auth::LoginResponse};\nasync fn login(\n    State(ctx): State<AppContext>,\n    Json(params): Json<LoginParams>,\n) -> Result<Response> {\n    // Fetching the user model with the requested parameters\n    // let user = users::Model::find_by_email(&ctx.db, &params.email).await?;\n\n    // Formatting the JSON response using LoginResponse view\n    format::json(LoginResponse::new(&user, &token))\n}\n```\n\nOn the other hand, `LoginResponse` is a response shaping view, which is powered by `serde`:\n\n```rust\nuse serde::{Deserialize, Serialize};\n\nuse crate::models::_entities::users;\n\n#[derive(Debug, Deserialize, Serialize)]\npub struct LoginResponse {\n    pub token: String,\n    pub pid: String,\n    pub name: String,\n}\n\nimpl LoginResponse {\n    #[must_use]\n    pub fn new(user: &users::Model, token: &String) -> Self {\n        Self {\n            token: token.to_string(),\n            pid: user.pid.to_string(),\n            name: user.name.clone(),\n        }\n    }\n}\n\n```\n\n## Template views\n\nWhen you want to return HTML to the user, you use server-side templates. This is similar to how Ruby's `erb` works, or Node's `ejs`, or PHP for that matter.\n\nFor server-side templates rendering we provide the built in `TeraView` engine which is based on the popular [Tera](http://keats.github.io/tera/) template engine.\n\n<div class=\"infobox\">\nTo use this engine you need to verify that you have a <code>ViewEngineInitializer</code> in <code>initializers/view_engine.rs</code> which is also specified in your <code>app.rs</code>. If you used the SaaS Starter, this should already be configured for you.\n</div>\n\nThe Tera view engine takes resources from the new `assets/` folder. Here is an example structure:\n\n```\nassets/\n├── i18n\n│   ├── de-DE\n│   │   └── main.ftl\n│   ├── en-US\n│   │   └── main.ftl\n│   └── shared.ftl\n├── static\n│   ├── 404.html\n│   └── image.png\n└── views\n    └── home\n        └── hello.html\nconfig/\n:\nsrc/\n├── controllers/\n├── models/\n:\n└── views/\n```\n\n### Creating a new view\n\nFirst, create a template. In this case we add a Tera template, in `assets/views/home/hello.html`. Note that **assets/** sits in the root of your project (next to `src/` and `config/`).\n\n```html\n<html>\n  <body>\n    find this tera template at <code>assets/views/home/hello.html</code>:\n    <br />\n    <br />\n    {{ /* t(key=\"hello-world\", lang=\"en-US\") */ }},\n    <br />\n    {{ /* t(key=\"hello-world\", lang=\"de-DE\") */ }}\n  </body>\n</html>\n```\n\nNow create a strongly typed `view` to encapsulate this template in `src/views/dashboard.rs`:\n\n```rust\n// src/views/dashboard.rs\nuse loco_rs::prelude::*;\n\npub fn home(v: impl ViewRenderer) -> Result<impl IntoResponse> {\n    format::render().view(&v, \"home/hello.html\", data!({}))\n}\n\n```\n\nAnd add it to `src/views/mod.rs`:\n\n```rust\npub mod dashboard;\n```\n\nNext, go to your controller and use the view:\n\n```rust\n// src/controllers/dashboard.rs\nuse loco_rs::prelude::*;\n\nuse crate::views;\n\npub async fn render_home(ViewEngine(v): ViewEngine<TeraView>) -> Result<impl IntoResponse> {\n    views::dashboard::home(v)\n}\n\npub fn routes() -> Routes {\n    Routes::new().prefix(\"home\").add(\"/\", get(render_home))\n}\n\n```\n\nFinally, register your new controller's routes in `src/app.rs`\n\n```rust\npub struct App;\n#[async_trait]\nimpl Hooks for App {\n    // omitted for brevity\n\n    fn routes(_ctx: &AppContext) -> AppRoutes {\n        AppRoutes::with_default_routes()\n            .add_route(controllers::auth::routes())\n            // include your controller's routes here\n            .add_route(controllers::dashboard::routes())\n    }\n```\n\nOnce you've done all the above, you should be able to see your new routes when running `cargo loco routes`\n\n```\n$ cargo loco routes\n[GET] /_health\n[GET] /_ping\n[POST] /api/auth/forgot\n[POST] /api/auth/login\n[POST] /api/auth/register\n[POST] /api/auth/reset\n[POST] /api/auth/verify\n[GET] /api/auth/current\n[GET] /home              <-- the corresponding URL for our new view\n```\n\n### How does it work?\n\n- `ViewEngine` is an extractor that's available to you via `loco_rs::prelude::*`\n- `TeraView` is the Tera view engine that we supply with Loco also available via `loco_rs::prelude::*`\n- Controllers need to deal with getting a request, calling some model logic, and then supplying a view with **models and other data**, not caring about how the view does its thing\n- `views::dashboard::home` is an opaque call, it hides the details of how a view works, or how the bytes find their way into a browser, which is a _Good Thing_\n- Should you ever want to swap a view engine, the encapsulation here works like magic. You can change the extractor type: `ViewEngine<Foobar>` and everything works, because `v` is eventually just a `ViewRenderer` trait\n\n### Static assets\n\nIf you want to serve static assets and reference those in your view templates, you can use the _Static Middleware_, configure it this way:\n\n```yaml\nstatic:\n  enable: true\n  must_exist: true\n  precompressed: false\n  folder:\n    uri: \"/static\"\n    path: \"assets/static\"\n  fallback: \"assets/static/404.html\"\n```\n\nIn your templates you can refer to static resources in this way:\n\n```html\n<img src=\"/static/image.png\" />\n```\n\nHowever, for the static middleware to work, ensure that the default fallback is disabled:\n\n```yaml\nfallback:\n  enable: false\n```\n\n### Customizing the Tera view engine\n\nThe Tera view engine comes with the following configuration:\n\n- Template loading and location: `assets/**/*.html`\n- Internationalization (i18n) configured into the Tera view engine, you get the translation function: `t(..)` to use in your templates\n\nIf you want to change any configuration detail for the `i18n` library, you can go and edit `src/initializers/view_engine.rs`.\n\nBy editing the initializer you can:\n\n- Add custom Tera functions\n- Remove the `i18n` library\n- Change configuration for Tera or the `i18n` library\n- Provide a new or custom, Tera (maybe a different version) instance\n\n### Using your own view engine\n\nIf you do not like Tera as a view engine, or want to use Handlebars, or others you can create your own custom view engine very easily.\n\nHere's an example for a dummy \"Hello\" view engine. It's a view engine that always returns the word _hello_.\n\n```rust\n// src/initializers/hello_view_engine.rs\nuse axum::{Extension, Router as AxumRouter};\nuse async_trait::async_trait;\nuse loco_rs::{\n    app::{AppContext, Initializer},\n    controller::views::{ViewEngine, ViewRenderer},\n    Result,\n};\nuse serde::Serialize;\n\n#[derive(Clone)]\npub struct HelloView;\nimpl ViewRenderer for HelloView {\n    fn render<S: Serialize>(&self, _key: &str, _data: S) -> Result<String> {\n        Ok(\"hello\".to_string())\n    }\n}\n\npub struct HelloViewEngineInitializer;\n#[async_trait]\nimpl Initializer for HelloViewEngineInitializer {\n    fn name(&self) -> String {\n        \"custom-view-engine\".to_string()\n    }\n\n    async fn after_routes(&self, router: AxumRouter, _ctx: &AppContext) -> Result<AxumRouter> {\n        Ok(router.layer(Extension(ViewEngine::from(HelloView))))\n    }\n}\n```\n\nTo use it, you need to add it to your `src/app.rs` hooks:\n\n```rust\n// src/app.rs\n// add your custom \"hello\" view engine in the `initializers(..)` hook\nimpl Hooks for App {\n    // ...\n    async fn initializers(_ctx: &AppContext) -> Result<Vec<Box<dyn Initializer>>> {\n        Ok(vec![\n            // ,.----- add it here\n            Box::new(initializers::hello_view_engine::HelloViewEngineInitializer),\n        ])\n    }\n    // ...\n```\n\n### Tera Built-ins\n\nLoco includes Tera with its [built-ins](https://keats.github.io/tera/docs/#built-ins) functions. In addition, Loco introduces the following custom built-in functions:\n\nTo see Loco built-in function:\n\n- [numbers](https://docs.rs/loco-rs/latest/loco_rs/controller/views/tera_builtins/filters/number/index.html)\n\n## Embedded Assets Feature\n\nThe Embedded Assets feature in Loco allows you to bundle all your static assets directly into your application binary. This means that everything under the `assets` folder, including CSS, images, PDFs, and more, becomes part of a single executable file.\n\nTo use this feature, you need to enable the `embedded_assets` feature when importing `loco-rs` in your `Cargo.toml`:\n\n```toml\n[dependencies]\nloco-rs = { version = \"...\", features = [\"embedded_assets\"] }\n```\n\n### Benefits\n\n- **Single Binary Deployment:** Simplifies deployment as you only need to distribute a single file. No need to worry about separate asset directories or CDN configurations for simpler deployments.\n- **Atomic Updates:** When you update your application, the assets are updated atomically with the code, reducing the chances of mismatches between code and assets.\n- **Potentially Faster Load Times:** Assets are loaded directly from memory, which can be faster than reading from the filesystem, especially in environments with slow disk I/O.\n\n### Considerations\n\n- **Increased Binary Size:** Embedding assets will naturally increase the size of your application binary.\n- **Recompilation for Asset Changes:** Any change to an asset requires recompiling the application. This might slow down development workflows if assets are changed frequently.\n\n### Seamlessly Switching Modes\n\nYou can easily switch between using embedded assets and serving assets from the filesystem without any code changes in your controllers or views. The switch is handled by the presence or absence of the `embedded_assets` feature flag.\n\nHowever, to ensure Tera functions correctly when _not_ using embedded assets (i.e., serving from the filesystem), you need to ensure that your `src/initializers/view_engine.rs` file only contains the necessary Tera function registration if you had customized it previously. Specifically, for the translation function `t`, ensure your initializer looks like this if you are not using `loco_rs::tera_helpers::FluentLoader`:\n\n```rust\ntera_engine\n    .tera\n    .register_function(\"t\", FluentLoader::new(arc));\n```\n\nAlternatively, you can introduce an internal feature flag within your application to toggle how assets are loaded or how Tera is configured, providing more granular control.\n\n### Build Time Logs\n\nWhen you build your application with the `embedded_assets` feature enabled, Loco will scan your `assets` directory and embed the discovered files. You will see logs similar to the following during the build process, indicating which assets are being included:\n\n```\nwarning: loco-rs@0.15.0: Assets will only be loaded from the application directory\nwarning: loco-rs@0.15.0: Discovered directories for assets:\nwarning: loco-rs@0.15.0:   - /path/to/your/myapp/assets\nwarning: loco-rs@0.15.0:   - /path/to/your/myapp/assets/static\nwarning: loco-rs@0.15.0:   - /path/to/your/myapp/assets/i18n\nwarning: loco-rs@0.15.0:   - /path/to/your/myapp/assets/i18n/de-DE\nwarning: loco-rs@0.15.0:   - /path/to/your/myapp/assets/i18n/en-US\nwarning: loco-rs@0.15.0:   - /path/to/your/myapp/assets/views\nwarning: loco-rs@0.15.0:   - /path/to/your/myapp/assets/views/home\nwarning: loco-rs@0.15.0: Found asset: /path/to/your/myapp/assets/static/styles.css -> /static/styles.css\nwarning: loco-rs@0.15.0: Found asset: /path/to/your/myapp/assets/static/dummy.pdf -> /static/dummy.pdf\nwarning: loco-rs@0.15.0: Found asset: /path/to/your/myapp/assets/static/404.html -> /static/404.html\nwarning: loco-rs@0.15.0: Found asset: /path/to/your/myapp/assets/views/base.html -> base.html\nwarning: loco-rs@0.15.0: Found 13 asset files\nwarning: loco-rs@0.15.0: Generated code for 6 static assets and 7 templates\n```\n\nThis output confirms that Loco has found your asset files (like CSS, PDFs, HTML templates) and has generated the necessary code to embed them into the binary. The paths will reflect your project's structure.\n"
  },
  {
    "path": "docs-site/content/docs/the-app/your-project.md",
    "content": "+++\ntitle = \"Your Project\"\ndescription = \"\"\ndate = 2021-05-01T18:10:00+00:00\nupdated = 2024-01-07T21:10:00+00:00\ndraft = false\nweight = 2\nsort_by = \"weight\"\ntemplate = \"docs/page.html\"\n\n[extra]\nlead = \"\"\ntoc = true\ntop = false\nflair =[]\n+++\n\n## Driving development with `cargo loco`\n\nCreate your starter app:\n\n<!-- <snip id=\"loco-cli-new-from-template\" inject_from=\"yaml\" template=\"sh\"> -->\n```sh\n❯ loco new\n✔ ❯ App name? · myapp\n✔ ❯ What would you like to build? · Saas App with client side rendering\n✔ ❯ Select a DB Provider · Sqlite\n✔ ❯ Select your background worker type · Async (in-process tokio async tasks)\n\n🚂 Loco app generated successfully in:\nmyapp/\n\n- assets: You've selected `clientside` for your asset serving configuration.\n\nNext step, build your frontend:\n  $ cd frontend/\n  $ npm install && npm run build\n```\n<!-- </snip> -->\n\nNow `cd` into your app and try out the various commands:\n\n<!-- <snip id=\"help-command\" inject_from=\"yaml\" template=\"sh\"> -->\n```sh\ncargo loco --help\n```\n<!-- </snip> -->\n\n<!-- <snip id=\"exec-help-command\" inject_from=\"yaml\" action=\"exec\" template=\"sh\"> -->\n```sh\nThe one-person framework for Rust\n\nUsage: demo_app-cli [OPTIONS] <COMMAND>\n\nCommands:\n  start       Start an app\n  db          Perform DB operations\n  routes      Describe all application endpoints\n  middleware  Describe all application middlewares\n  task        Run a custom task\n  jobs        Managing jobs queue\n  scheduler   Run the scheduler\n  generate    code generation creates a set of files and code templates based on a predefined set of rules\n  doctor      Validate and diagnose configurations\n  version     Display the app version\n  watch       Watch and restart the app\n  help        Print this message or the help of the given subcommand(s)\n\nOptions:\n  -e, --environment <ENVIRONMENT>  Specify the environment [default: development]\n  -h, --help                       Print help\n  -V, --version                    Print version\n```\n<!-- </snip> -->\n\n\nYou can now drive your development through the CLI:\n\n```\n$ cargo loco generate model posts\n$ cargo loco generate controller posts\n$ cargo loco db migrate\n$ cargo loco start\n```\n\nAnd running tests or working with Rust is just as you already know:\n\n```\n$ cargo build\n$ cargo test\n```\n\n### Starting your app\n\nTo run you app, run:\n\n<!-- <snip id=\"starting-the-server-command\" inject_from=\"yaml\" template=\"sh\"> -->\n```sh\ncargo loco start\n```\n<!-- </snip> -->\n\n### Background workers\n\nBased on your configuration (in `config/`), your workers will know how to operate:\n\n```yaml\nworkers:\n  # requires Redis\n  mode: BackgroundQueue\n\n  # can also use:\n  # ForegroundBlocking - great for testing\n  # BackgroundAsync - for same-process jobs, using tokio async\n```\n\nAnd now, you can run the actual process in various ways:\n\n- `rr start --worker` - run only a worker and process background jobs. This is great for scale. Run one service app with `rr start`, and then run many process based workers with `rr start --worker` distributed on any machine you want.\n\n* `rr start --server-and-worker` - will run both a service and a background worker processor in the same unix process. It uses Tokio for executing background jobs. This is great for those cases when you want to run on a single server without too much of an expense or have constrained resources.\n\n### Getting your app version\n\nBecause your app is compiled, and then copied to production, Loco gives you two important operability pieces of information:\n\n* Which version is this app, and which GIT SHA was it built from? `cargo loco version`\n* Which Loco version was this app compiled against? `cargo loco --version`\n\nBoth version strings are parsable and stable so you can use it in integration scripts, monitoring tools and so on.\n\nYou can shape your own custom app versioning scheme by overriding the `app_version` hook in your `src/app.rs` file.\n\n\n## Using the scaffold generator\n\nScaffolding is an efficient and speedy method for generating key components of an application. By utilizing scaffolding, you can create models, views, and controllers for a new resource all in one go.\n\n\nSee scaffold command:\n<!-- <snip id=\"scaffold-help-command\" inject_from=\"yaml\" action=\"exec\" template=\"sh\"> -->\n```sh\nGenerates a CRUD scaffold, model and controller\n\nUsage: demo_app-cli generate scaffold [OPTIONS] <NAME> [FIELDS]...\n\nArguments:\n  <NAME>       Name of the thing to generate\n  [FIELDS]...  Model fields, eg. title:string hits:int\n\nOptions:\n  -k, --kind <KIND>                The kind of scaffold to generate [possible values: api, html, htmx]\n      --htmx                       Use HTMX scaffold\n      --html                       Use HTML scaffold\n      --api                        Use API scaffold\n  -e, --environment <ENVIRONMENT>  Specify the environment [default: development]\n  -h, --help                       Print help\n  -V, --version                    Print version\n```\n<!-- </snip> -->\n\nYou can begin by generating a scaffold for the Post resource, which will represent a single blog posting. To accomplish this, open your terminal and enter the following command:\n<!-- <snip id=\"scaffold-post-command\" inject_from=\"yaml\" template=\"sh\"> -->\n```sh\ncargo loco generate scaffold posts name:string title:string content:text --api\n```\n<!-- </snip> -->\n\nThe scaffold generate command support API, HTML or HTMX by adding `--template` flag to scaffold command.\n\n### Scaffold file layout\n\nThe scaffold generator will build several files in your application:\n\n| File    | Purpose                                                                                                                                    |\n| ------------------------------------------ | ------------------------------------------------------------------------------------------------------- |\n| `migration/src/lib.rs`                     |  Include Post migration.                                                                                |\n| `migration/src/m20240606_102031_posts.rs`  | Posts migration.                                                                                        |\n| `src/app.rs`                               | Adding Posts to application router.                                                                     |\n| `src/controllers/mod.rs`                   | Include the Posts controller.                                                                           |\n| `src/controllers/posts.rs`                 | The Posts controller.                                                                                   |\n| `tests/requests/posts.rs`                  | Functional testing.                                                                                     |\n| `src/models/mod.rs`                        | Including Posts model.                                                                                  |\n| `src/models/posts.rs`                      | Posts model,                                                                                            |\n| `src/models/_entities/mod.rs`              | Includes Posts Sea-orm entity model.                                                                    |\n| `src/models/_entities/posts.rs`            | Sea-orm entity model.                                                                                   |\n| `src/views/mod.rs`                         | Including Posts views. only for HTML and HTMX templates.                                                |\n| `src/views/posts.rs`                       | Posts template generator. only for HTML and HTMX templates.                                             |\n| `assets/views/posts/create.html`           | Create post template. only for HTML and HTMX templates.                                                 |\n| `assets/views/posts/edit.html`             | Edit post template. only for HTML and HTMX templates.                                                   |                                               |\n| `assets/views/posts/list.html`             | List post template. only for HTML and HTMX templates.                                                   |\n| `assets/views/posts/show.html`             | Show post template. only for HTML and HTMX templates.                                                   |\n\n## Your app configuration\nBy default, loco stores its configuration files in the config/ directory. It provides predefined configurations for three environments:\n\n```\nconfig/\n  development.yaml\n  production.yaml\n  test.yaml\n```\n\nAn environment is picked up automatically based on:\n\n- A command line flag: `cargo loco start --environment production`, if not given, fallback to\n- `LOCO_ENV` or `RAILS_ENV` or `NODE_ENV`\n\nWhen nothing is given, the default value is `development`.\n\nThe `Loco` framework allows support for custom environments in addition to the default environment. To add a custom environment, create a configuration file with a name matching the environment identifier used in the preceding example.\n\n### Overriding the Default Configuration Path\nTo use a custom configuration directory, set the `LOCO_CONFIG_FOLDER` environment variable to the desired folder path. This will instruct `loco` to load configuration files from the specified directory instead of the default `config/` folder.\n\n### Placeholders / variables in config\n\nIt is possible to inject values into a configuration file. In this example, we get a port value from the `NODE_PORT` environment variable:\n\n```yaml\n# config/development.yaml\n# every configuration file is a valid Tera template\nserver:\n  # Port on which the server will listen. the server binding is 0.0.0.0:{PORT}\n  port:  {{/* get_env(name=\"NODE_PORT\", default=5150) */}}\n  # The UI hostname or IP address that mailers will point to.\n  host: http://localhost\n  # Out of the box middleware configuration. to disable middleware you can changed the `enable` field to `false` of comment the middleware block\n```\n\nThe [get_env](https://keats.github.io/tera/docs/#get-env) function is part of the Tera template engine. Refer to the [Tera](https://keats.github.io/tera/docs) docs to see what more you can use.\n\n### Example\n\nSuppose you want to add a 'qa' environment. Create a `qa.yaml` file in the config folder:\n\n```\nconfig/\n  development.yaml\n  production.yaml\n  test.yaml\n  qa.yaml\n```\n\nTo run the application using the 'qa' environment, execute the following command:\n<!-- <snip id=\"starting-the-server-command-with-environment-env-var\" inject_from=\"yaml\" template=\"sh\"> -->\n```sh\nLOCO_ENV=qa cargo loco start\n```\n<!-- </snip> -->\n\n### Settings\n\nThe configuration files contain knobs to set up your Loco app. You can also have your custom settings, with the `settings:` section. in `config/development.yaml` add the `settings:` section\n<!-- <snip id=\"configuration-settings\" inject_from=\"code\" template=\"yaml\"> -->\n```yaml\nsettings:\n  allow_list:\n    - google.com\n    - apple.com\n```\n<!-- </snip> -->\n\nThese setting will appear in `ctx.config.settings` as `serde_json::Value`. You can create your strongly typed settings by adding a struct:\n\n```rust\n// put this in src/common/settings.rs\n#[derive(Serialize, Deserialize, Default, Debug)]\npub struct Settings {\n    pub allow_list: Option<Vec<String>>,\n}\n\nimpl Settings {\n    pub fn from_json(value: &serde_json::Value) -> Result<Self> {\n        Ok(serde_json::from_value(value.clone())?)\n    }\n}\n```\n\nThen, you can access settings from anywhere like this:\n\n\n```rust\n// in controllers, workers, tasks, or elsewhere,\n// as long as you have access to AppContext (here: `ctx`)\n\nif let Some(settings) = &ctx.config.settings {\n    let settings = common::settings::Settings::from_json(settings)?;\n    println!(\"allow list: {:?}\", settings.allow_list);\n}\n```\n\n### Server\n\n\n\nHere is a detailed description of the interface (listening, etc.) parameters under `server:`:\n\n* `port:` as the name says, for changing ports, mostly when behind a load balancer, etc.\n\n* `binding:` for changing what the IP interface \"binds\" to, mostly, when you are behind a load balancer like `nginx` you bind to a local address (when the LB is also there). However, you can also bind to \"world\" (`0.0.0.0`). You can set the binding: field via config, or via the CLI (using the `-b` flag) -- which is what Rails is doing.\n\n* `host:` - for \"visibility\" use cases or out-of-band use cases. For example, sometimes you want to display the current server host (in terms of domain name, etc.), which serves for visibility. And sometimes, as in the case of emails -- your server address is \"out of band\", meaning when I open my gmail account and I have your email -- I have to click what looks like your external address or visible address (official domain name, etc), and not an internal \"host\" address which is what may be the wrong thing to do (imagine an email link pointing to \"http://127.0.0.1/account/verify\")\n\n\n\n### Logger\n\nOther than the commented fields in the `logger:` section on your YAML file, here's some more context:\n\n* `logger.pretty_backtrace` - will display colorful backtrace without noise for great development experience. Note that this forcefully sets `RUST_BACKTRACE=1` into the process' env, which enables a (costly) backtrace capture on specific errors. Enable this in development, disable it in production. When needed in production, use `RUST_BACKTRACE=1` ad-hoc in the command line to show it.\n\n\nFor all available configuration options [click here](https://docs.rs/loco-rs/latest/loco_rs/config/struct.Config.html)\n"
  },
  {
    "path": "docs-site/content/privacy-policy/_index.md",
    "content": "+++\ntitle = \"Privacy Policy\"\ndescription = \"We do not use cookies and we do not collect any personal data.\"\ndraft = false\n\n[extra]\nclass = \"page single\"\n+++\n\n__TLDR__: We do not use cookies and we do not collect any personal data.\n\n## Website visitors\n\n- No personal information is collected.\n- No information is stored in the browser.\n- No information is shared with, sent to or sold to third-parties.\n- No information is shared with advertising companies.\n- No information is mined and harvested for personal and behavioral trends.\n- No information is monetized.\n\n## Contact us\n\n[Contact us](https://github.com/aaranxu/adidoks) if you have any questions.\n\nEffective Date: _1st May 2021_\n"
  },
  {
    "path": "docs-site/package.json",
    "content": "{\n  \"name\": \"loco\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"build\": \"NODE_ENV=production npx tailwindcss -i styles/styles.css -o static/styles/styles.css\",\n    \"watch-tailwinds\": \"npx tailwindcss -i styles/styles.css -o static/styles/styles.css --watch\",\n    \"serve\": \"zola serve\",\n    \"dev\": \"npm-run-all --parallel watch-tailwinds serve\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"@tailwindcss/typography\": \"^0.5.13\",\n    \"autoprefixer\": \"^10.4.19\",\n    \"npm-run-all\": \"^4.1.5\",\n    \"tailwindcss\": \"^3.4.7\"\n  }\n}"
  },
  {
    "path": "docs-site/static/ahrefs_f06cfb9c2671c1b7a5b6eec0d47a07719bd6d5a2240b829cad6a3f40684fa370",
    "content": "ahrefs-site-verification_f06cfb9c2671c1b7a5b6eec0d47a07719bd6d5a2240b829cad6a3f40684fa370"
  },
  {
    "path": "docs-site/static/js/main.js",
    "content": "// Set darkmode\ndocument.getElementById('mode').addEventListener('click', () => {\n\n    document.documentElement.classList.toggle('dark');\n    localStorage.setItem('theme', document.documentElement.classList.contains('dark') ? 'dark' : 'light');\n\n});\n\n// enforce local storage setting but also fallback to user-agent preferences\nif (localStorage.getItem('theme') === 'dark' || (!localStorage.getItem('theme') && window.matchMedia(\"(prefers-color-scheme: dark)\").matches)) {\n\n    document.documentElement.classList.add('dark');\n\n}"
  },
  {
    "path": "docs-site/static/js/search.js",
    "content": "var suggestions = document.getElementById('suggestions');\nvar userinput = document.getElementById('userinput');\n\ndocument.addEventListener('keydown', inputFocus);\n\nfunction inputFocus(e) {\n\n    if (e.keyCode === 191\n        && document.activeElement.tagName !== \"INPUT\"\n        && document.activeElement.tagName !== \"TEXTAREA\") {\n        e.preventDefault();\n        userinput.focus();\n    }\n\n    if (e.keyCode === 27) {\n        userinput.blur();\n        suggestions.classList.add('d-none');\n    }\n\n}\n\ndocument.addEventListener('click', function (event) {\n\n    var isClickInsideElement = suggestions.contains(event.target);\n\n    if (!isClickInsideElement) {\n        suggestions.classList.add('d-none');\n    }\n\n});\n\n/*\nSource:\n  - https://dev.to/shubhamprakash/trap-focus-using-javascript-6a3\n*/\n\ndocument.addEventListener('keydown', suggestionFocus);\n\nfunction suggestionFocus(e) {\n    const focusableSuggestions = suggestions.querySelectorAll('a');\n    if (suggestions.classList.contains('d-none')\n        || focusableSuggestions.length === 0) {\n        return;\n    }\n    const focusable = [...focusableSuggestions];\n    const index = focusable.indexOf(document.activeElement);\n\n    let nextIndex = 0;\n\n    if (e.keyCode === 38) {\n        e.preventDefault();\n        nextIndex = index > 0 ? index - 1 : 0;\n        focusableSuggestions[nextIndex].focus();\n    }\n    else if (e.keyCode === 40) {\n        e.preventDefault();\n        nextIndex = index + 1 < focusable.length ? index + 1 : index;\n        focusableSuggestions[nextIndex].focus();\n    }\n\n}\n\n/*\nSource:\n  - https://github.com/nextapps-de/flexsearch#index-documents-field-search\n  - https://raw.githack.com/nextapps-de/flexsearch/master/demo/autocomplete.html\n  - http://elasticlunr.com/\n  - https://github.com/getzola/zola/blob/master/docs/static/search.js\n*/\n(function () {\n    var index = elasticlunr.Index.load(window.searchIndex);\n    userinput.addEventListener('input', show_results, true);\n    suggestions.addEventListener('click', accept_suggestion, true);\n\n    function show_results() {\n        var value = this.value.trim();\n        var options = {\n            bool: \"OR\",\n            fields: {\n                title: { boost: 2 },\n                body: { boost: 1 },\n            }\n        };\n        var results = index.search(value, options);\n\n        var entry, childs = suggestions.childNodes;\n        var i = 0, len = results.length;\n        var items = value.split(/\\s+/);\n        suggestions.classList.remove('d-none');\n\n        results.forEach(function (page) {\n            if (page.doc.body !== '') {\n                entry = document.createElement('div');\n\n                entry.innerHTML = '<a href><span></span><span></span></a>';\n\n                a = entry.querySelector('a'),\n                    t = entry.querySelector('span:first-child'),\n                    d = entry.querySelector('span:nth-child(2)');\n                a.href = page.ref;\n                t.textContent = page.doc.title;\n                d.innerHTML = makeTeaser(page.doc.body, items);\n\n                suggestions.appendChild(entry);\n            }\n        });\n\n        while (childs.length > len) {\n            suggestions.removeChild(childs[i])\n        }\n\n    }\n\n    function accept_suggestion() {\n\n        while (suggestions.lastChild) {\n\n            suggestions.removeChild(suggestions.lastChild);\n        }\n\n        return false;\n    }\n\n    // Taken from mdbook\n    // The strategy is as follows:\n    // First, assign a value to each word in the document:\n    //  Words that correspond to search terms (stemmer aware): 40\n    //  Normal words: 2\n    //  First word in a sentence: 8\n    // Then use a sliding window with a constant number of words and count the\n    // sum of the values of the words within the window. Then use the window that got the\n    // maximum sum. If there are multiple maximas, then get the last one.\n    // Enclose the terms in <b>.\n    function makeTeaser(body, terms) {\n        var TERM_WEIGHT = 40;\n        var NORMAL_WORD_WEIGHT = 2;\n        var FIRST_WORD_WEIGHT = 8;\n        var TEASER_MAX_WORDS = 30;\n\n        var stemmedTerms = terms.map(function (w) {\n            return elasticlunr.stemmer(w.toLowerCase());\n        });\n        var termFound = false;\n        var index = 0;\n        var weighted = []; // contains elements of [\"word\", weight, index_in_document]\n\n        // split in sentences, then words\n        var sentences = body.toLowerCase().split(\". \");\n        for (var i in sentences) {\n            var words = sentences[i].split(/[\\s\\n]/);\n            var value = FIRST_WORD_WEIGHT;\n            for (var j in words) {\n\n                var word = words[j];\n\n                if (word.length > 0) {\n                    for (var k in stemmedTerms) {\n                        if (elasticlunr.stemmer(word).startsWith(stemmedTerms[k])) {\n                            value = TERM_WEIGHT;\n                            termFound = true;\n                        }\n                    }\n                    weighted.push([word, value, index]);\n                    value = NORMAL_WORD_WEIGHT;\n                }\n\n                index += word.length;\n                index += 1;  // ' ' or '.' if last word in sentence\n            }\n\n            index += 1;  // because we split at a two-char boundary '. '\n        }\n\n        if (weighted.length === 0) {\n            if (body.length !== undefined && body.length > TEASER_MAX_WORDS * 10) {\n                return body.substring(0, TEASER_MAX_WORDS * 10) + '...';\n            } else {\n                return body;\n            }\n        }\n\n        var windowWeights = [];\n        var windowSize = Math.min(weighted.length, TEASER_MAX_WORDS);\n        // We add a window with all the weights first\n        var curSum = 0;\n        for (var i = 0; i < windowSize; i++) {\n            curSum += weighted[i][1];\n        }\n        windowWeights.push(curSum);\n\n        for (var i = 0; i < weighted.length - windowSize; i++) {\n            curSum -= weighted[i][1];\n            curSum += weighted[i + windowSize][1];\n            windowWeights.push(curSum);\n        }\n\n        // If we didn't find the term, just pick the first window\n        var maxSumIndex = 0;\n        if (termFound) {\n            var maxFound = 0;\n            // backwards\n            for (var i = windowWeights.length - 1; i >= 0; i--) {\n                if (windowWeights[i] > maxFound) {\n                    maxFound = windowWeights[i];\n                    maxSumIndex = i;\n                }\n            }\n        }\n\n        var teaser = [];\n        var startIndex = weighted[maxSumIndex][2];\n        for (var i = maxSumIndex; i < maxSumIndex + windowSize; i++) {\n            var word = weighted[i];\n            if (startIndex < word[2]) {\n                // missing text from index to start of `word`\n                teaser.push(body.substring(startIndex, word[2]));\n                startIndex = word[2];\n            }\n\n            // add <em/> around search terms\n            if (word[1] === TERM_WEIGHT) {\n                teaser.push(\"<b>\");\n            }\n\n            startIndex = word[2] + word[0].length;\n            // Check the string is ascii characters or not\n            var re = /^[\\x00-\\xff]+$/\n            if (word[1] !== TERM_WEIGHT && word[0].length >= 12 && !re.test(word[0])) {\n                // If the string's length is too long, it maybe a Chinese/Japance/Korean article\n                // if using substring method directly, it may occur error codes on emoji chars\n                var strBefor = body.substring(word[2], startIndex);\n                var strAfter = substringByByte(strBefor, 12);\n                teaser.push(strAfter);\n            } else {\n                teaser.push(body.substring(word[2], startIndex));\n            }\n\n            if (word[1] === TERM_WEIGHT) {\n                teaser.push(\"</b>\");\n            }\n        }\n        teaser.push(\"…\");\n        return teaser.join(\"\");\n    }\n}());\n\n\n// Get substring by bytes\n// If using JavaScript inline substring method, it will return error codes \n// Source: https://www.52pojie.cn/thread-1059814-1-1.html\nfunction substringByByte(str, maxLength) {\n    var result = \"\";\n    var flag = false;\n    var len = 0;\n    var length = 0;\n    var length2 = 0;\n    for (var i = 0; i < str.length; i++) {\n        var code = str.codePointAt(i).toString(16);\n        if (code.length > 4) {\n            i++;\n            if ((i + 1) < str.length) {\n                flag = str.codePointAt(i + 1).toString(16) == \"200d\";\n            }\n        }\n        if (flag) {\n            len += getByteByHex(code);\n            if (i == str.length - 1) {\n                length += len;\n                if (length <= maxLength) {\n                    result += str.substr(length2, i - length2 + 1);\n                } else {\n                    break\n                }\n            }\n        } else {\n            if (len != 0) {\n                length += len;\n                length += getByteByHex(code);\n                if (length <= maxLength) {\n                    result += str.substr(length2, i - length2 + 1);\n                    length2 = i + 1;\n                } else {\n                    break\n                }\n                len = 0;\n                continue;\n            }\n            length += getByteByHex(code);\n            if (length <= maxLength) {\n                if (code.length <= 4) {\n                    result += str[i]\n                } else {\n                    result += str[i - 1] + str[i]\n                }\n                length2 = i + 1;\n            } else {\n                break\n            }\n        }\n    }\n    return result;\n}\n\n// Get the string bytes from binary\nfunction getByteByBinary(binaryCode) {\n    // Binary system, starts with `0b` in ES6\n    // Octal number system, starts with `0` in ES5 and starts with `0o` in ES6\n    // Hexadecimal, starts with `0x` in both ES5 and ES6\n    var byteLengthDatas = [0, 1, 2, 3, 4];\n    var len = byteLengthDatas[Math.ceil(binaryCode.length / 8)];\n    return len;\n}\n\n// Get the string bytes from hexadecimal\nfunction getByteByHex(hexCode) {\n    return getByteByBinary(parseInt(hexCode, 16).toString(2));\n}"
  },
  {
    "path": "docs-site/static/styles/styles.css",
    "content": "/*\n! tailwindcss v3.4.7 | MIT License | https://tailwindcss.com\n*/\n\n/*\n1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)\n2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)\n*/\n\n*,\n::before,\n::after {\n  box-sizing: border-box;\n  /* 1 */\n  border-width: 0;\n  /* 2 */\n  border-style: solid;\n  /* 2 */\n  border-color: #e5e7eb;\n  /* 2 */\n}\n\n::before,\n::after {\n  --tw-content: '';\n}\n\n/*\n1. Use a consistent sensible line-height in all browsers.\n2. Prevent adjustments of font size after orientation changes in iOS.\n3. Use a more readable tab size.\n4. Use the user's configured `sans` font-family by default.\n5. Use the user's configured `sans` font-feature-settings by default.\n6. Use the user's configured `sans` font-variation-settings by default.\n7. Disable tap highlights on iOS\n*/\n\nhtml,\n:host {\n  line-height: 1.5;\n  /* 1 */\n  -webkit-text-size-adjust: 100%;\n  /* 2 */\n  -moz-tab-size: 4;\n  /* 3 */\n  -o-tab-size: 4;\n     tab-size: 4;\n  /* 3 */\n  font-family: ui-sans-serif, system-ui, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n  /* 4 */\n  font-feature-settings: normal;\n  /* 5 */\n  font-variation-settings: normal;\n  /* 6 */\n  -webkit-tap-highlight-color: transparent;\n  /* 7 */\n}\n\n/*\n1. Remove the margin in all browsers.\n2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.\n*/\n\nbody {\n  margin: 0;\n  /* 1 */\n  line-height: inherit;\n  /* 2 */\n}\n\n/*\n1. Add the correct height in Firefox.\n2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)\n3. Ensure horizontal rules are visible by default.\n*/\n\nhr {\n  height: 0;\n  /* 1 */\n  color: inherit;\n  /* 2 */\n  border-top-width: 1px;\n  /* 3 */\n}\n\n/*\nAdd the correct text decoration in Chrome, Edge, and Safari.\n*/\n\nabbr:where([title]) {\n  -webkit-text-decoration: underline dotted;\n          text-decoration: underline dotted;\n}\n\n/*\nRemove the default font size and weight for headings.\n*/\n\nh1,\nh2,\nh3,\nh4,\nh5,\nh6 {\n  font-size: inherit;\n  font-weight: inherit;\n}\n\n/*\nReset links to optimize for opt-in styling instead of opt-out.\n*/\n\na {\n  color: inherit;\n  text-decoration: inherit;\n}\n\n/*\nAdd the correct font weight in Edge and Safari.\n*/\n\nb,\nstrong {\n  font-weight: bolder;\n}\n\n/*\n1. Use the user's configured `mono` font-family by default.\n2. Use the user's configured `mono` font-feature-settings by default.\n3. Use the user's configured `mono` font-variation-settings by default.\n4. Correct the odd `em` font sizing in all browsers.\n*/\n\ncode,\nkbd,\nsamp,\npre {\n  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n  /* 1 */\n  font-feature-settings: normal;\n  /* 2 */\n  font-variation-settings: normal;\n  /* 3 */\n  font-size: 1em;\n  /* 4 */\n}\n\n/*\nAdd the correct font size in all browsers.\n*/\n\nsmall {\n  font-size: 80%;\n}\n\n/*\nPrevent `sub` and `sup` elements from affecting the line height in all browsers.\n*/\n\nsub,\nsup {\n  font-size: 75%;\n  line-height: 0;\n  position: relative;\n  vertical-align: baseline;\n}\n\nsub {\n  bottom: -0.25em;\n}\n\nsup {\n  top: -0.5em;\n}\n\n/*\n1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)\n2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)\n3. Remove gaps between table borders by default.\n*/\n\ntable {\n  text-indent: 0;\n  /* 1 */\n  border-color: inherit;\n  /* 2 */\n  border-collapse: collapse;\n  /* 3 */\n}\n\n/*\n1. Change the font styles in all browsers.\n2. Remove the margin in Firefox and Safari.\n3. Remove default padding in all browsers.\n*/\n\nbutton,\ninput,\noptgroup,\nselect,\ntextarea {\n  font-family: inherit;\n  /* 1 */\n  font-feature-settings: inherit;\n  /* 1 */\n  font-variation-settings: inherit;\n  /* 1 */\n  font-size: 100%;\n  /* 1 */\n  font-weight: inherit;\n  /* 1 */\n  line-height: inherit;\n  /* 1 */\n  letter-spacing: inherit;\n  /* 1 */\n  color: inherit;\n  /* 1 */\n  margin: 0;\n  /* 2 */\n  padding: 0;\n  /* 3 */\n}\n\n/*\nRemove the inheritance of text transform in Edge and Firefox.\n*/\n\nbutton,\nselect {\n  text-transform: none;\n}\n\n/*\n1. Correct the inability to style clickable types in iOS and Safari.\n2. Remove default button styles.\n*/\n\nbutton,\ninput:where([type='button']),\ninput:where([type='reset']),\ninput:where([type='submit']) {\n  -webkit-appearance: button;\n  /* 1 */\n  background-color: transparent;\n  /* 2 */\n  background-image: none;\n  /* 2 */\n}\n\n/*\nUse the modern Firefox focus style for all focusable elements.\n*/\n\n:-moz-focusring {\n  outline: auto;\n}\n\n/*\nRemove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)\n*/\n\n:-moz-ui-invalid {\n  box-shadow: none;\n}\n\n/*\nAdd the correct vertical alignment in Chrome and Firefox.\n*/\n\nprogress {\n  vertical-align: baseline;\n}\n\n/*\nCorrect the cursor style of increment and decrement buttons in Safari.\n*/\n\n::-webkit-inner-spin-button,\n::-webkit-outer-spin-button {\n  height: auto;\n}\n\n/*\n1. Correct the odd appearance in Chrome and Safari.\n2. Correct the outline style in Safari.\n*/\n\n[type='search'] {\n  -webkit-appearance: textfield;\n  /* 1 */\n  outline-offset: -2px;\n  /* 2 */\n}\n\n/*\nRemove the inner padding in Chrome and Safari on macOS.\n*/\n\n::-webkit-search-decoration {\n  -webkit-appearance: none;\n}\n\n/*\n1. Correct the inability to style clickable types in iOS and Safari.\n2. Change font properties to `inherit` in Safari.\n*/\n\n::-webkit-file-upload-button {\n  -webkit-appearance: button;\n  /* 1 */\n  font: inherit;\n  /* 2 */\n}\n\n/*\nAdd the correct display in Chrome and Safari.\n*/\n\nsummary {\n  display: list-item;\n}\n\n/*\nRemoves the default spacing and border for appropriate elements.\n*/\n\nblockquote,\ndl,\ndd,\nh1,\nh2,\nh3,\nh4,\nh5,\nh6,\nhr,\nfigure,\np,\npre {\n  margin: 0;\n}\n\nfieldset {\n  margin: 0;\n  padding: 0;\n}\n\nlegend {\n  padding: 0;\n}\n\nol,\nul,\nmenu {\n  list-style: none;\n  margin: 0;\n  padding: 0;\n}\n\n/*\nReset default styling for dialogs.\n*/\n\ndialog {\n  padding: 0;\n}\n\n/*\nPrevent resizing textareas horizontally by default.\n*/\n\ntextarea {\n  resize: vertical;\n}\n\n/*\n1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)\n2. Set the default placeholder color to the user's configured gray 400 color.\n*/\n\ninput::-moz-placeholder, textarea::-moz-placeholder {\n  opacity: 1;\n  /* 1 */\n  color: #9ca3af;\n  /* 2 */\n}\n\ninput::placeholder,\ntextarea::placeholder {\n  opacity: 1;\n  /* 1 */\n  color: #9ca3af;\n  /* 2 */\n}\n\n/*\nSet the default cursor for buttons.\n*/\n\nbutton,\n[role=\"button\"] {\n  cursor: pointer;\n}\n\n/*\nMake sure disabled buttons don't get the pointer cursor.\n*/\n\n:disabled {\n  cursor: default;\n}\n\n/*\n1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)\n2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)\n   This can trigger a poorly considered lint error in some tools but is included by design.\n*/\n\nimg,\nsvg,\nvideo,\ncanvas,\naudio,\niframe,\nembed,\nobject {\n  display: block;\n  /* 1 */\n  vertical-align: middle;\n  /* 2 */\n}\n\n/*\nConstrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)\n*/\n\nimg,\nvideo {\n  max-width: 100%;\n  height: auto;\n}\n\n/* Make elements with the HTML hidden attribute stay hidden by default */\n\n[hidden] {\n  display: none;\n}\n\n:root {\n  --background: #ffffff;\n  --background-secondary: #5F456B;\n  /* 1f2937 gray-800*/\n  --foreground: #1f2937;\n  /* gray-900 */\n  --primary: #D45546;\n  --redrust:  #c12110;\n  --orangerust:  #cc4111;\n  --border: #3A3A54;\n  --card: #1D1D2A;\n  --card-foreground: #FDCEF1;\n  --radius: 0.5rem;\n}\n\n.dark {\n  --background: black;\n  --foreground: hsl(213, 31%, 91%);\n  --muted: hsl(223, 47%, 11%);\n  --muted-foreground: hsl(215.4, 16.3%, 56.9%);\n  --accent: hsl(216, 34%, 17%);\n  --accent-foreground: hsl(210, 40%, 98%);\n  --popover: hsl(224, 71%, 4%);\n  --popover-foreground: hsl(215, 20.2%, 65.1%);\n  --border: hsl(216, 34%, 17%);\n  --input: hsl(216, 34%, 17%);\n  --card: hsl(224, 71%, 4%);\n  --card-foreground: hsl(213, 31%, 91%);\n  --primary: hsl(210, 40%, 98%);\n  --primary-foreground: hsl(222.2, 47.4%, 1.2%);\n  --secondary: hsl(222.2, 47.4%, 11.2%);\n  --secondary-foreground: hsl(210, 40%, 98%);\n  --destructive: hsl(0, 63%, 31%);\n  --destructive-foreground: hsl(210, 40%, 98%);\n  --ring: hsl(216, 34%, 17%);\n  --radius: 0.5rem;\n}\n\n* {\n  border-color: var(--border);\n}\n\nbody {\n  background-color: var(--background);\n  font-family: Inter;\n  color: var(--foreground);\n  font-feature-settings: \"rlig\" 1, \"calt\" 1;\n}\n\nh1, h2, h3, h4, h5 {\n  letter-spacing: -0.03em;\n  scroll-margin-top: 56px;\n}\n\na.active {\n  color: var(--primary);\n}\n\n*, ::before, ::after {\n  --tw-border-spacing-x: 0;\n  --tw-border-spacing-y: 0;\n  --tw-translate-x: 0;\n  --tw-translate-y: 0;\n  --tw-rotate: 0;\n  --tw-skew-x: 0;\n  --tw-skew-y: 0;\n  --tw-scale-x: 1;\n  --tw-scale-y: 1;\n  --tw-pan-x:  ;\n  --tw-pan-y:  ;\n  --tw-pinch-zoom:  ;\n  --tw-scroll-snap-strictness: proximity;\n  --tw-gradient-from-position:  ;\n  --tw-gradient-via-position:  ;\n  --tw-gradient-to-position:  ;\n  --tw-ordinal:  ;\n  --tw-slashed-zero:  ;\n  --tw-numeric-figure:  ;\n  --tw-numeric-spacing:  ;\n  --tw-numeric-fraction:  ;\n  --tw-ring-inset:  ;\n  --tw-ring-offset-width: 0px;\n  --tw-ring-offset-color: #fff;\n  --tw-ring-color: rgb(59 130 246 / 0.5);\n  --tw-ring-offset-shadow: 0 0 #0000;\n  --tw-ring-shadow: 0 0 #0000;\n  --tw-shadow: 0 0 #0000;\n  --tw-shadow-colored: 0 0 #0000;\n  --tw-blur:  ;\n  --tw-brightness:  ;\n  --tw-contrast:  ;\n  --tw-grayscale:  ;\n  --tw-hue-rotate:  ;\n  --tw-invert:  ;\n  --tw-saturate:  ;\n  --tw-sepia:  ;\n  --tw-drop-shadow:  ;\n  --tw-backdrop-blur:  ;\n  --tw-backdrop-brightness:  ;\n  --tw-backdrop-contrast:  ;\n  --tw-backdrop-grayscale:  ;\n  --tw-backdrop-hue-rotate:  ;\n  --tw-backdrop-invert:  ;\n  --tw-backdrop-opacity:  ;\n  --tw-backdrop-saturate:  ;\n  --tw-backdrop-sepia:  ;\n  --tw-contain-size:  ;\n  --tw-contain-layout:  ;\n  --tw-contain-paint:  ;\n  --tw-contain-style:  ;\n}\n\n::backdrop {\n  --tw-border-spacing-x: 0;\n  --tw-border-spacing-y: 0;\n  --tw-translate-x: 0;\n  --tw-translate-y: 0;\n  --tw-rotate: 0;\n  --tw-skew-x: 0;\n  --tw-skew-y: 0;\n  --tw-scale-x: 1;\n  --tw-scale-y: 1;\n  --tw-pan-x:  ;\n  --tw-pan-y:  ;\n  --tw-pinch-zoom:  ;\n  --tw-scroll-snap-strictness: proximity;\n  --tw-gradient-from-position:  ;\n  --tw-gradient-via-position:  ;\n  --tw-gradient-to-position:  ;\n  --tw-ordinal:  ;\n  --tw-slashed-zero:  ;\n  --tw-numeric-figure:  ;\n  --tw-numeric-spacing:  ;\n  --tw-numeric-fraction:  ;\n  --tw-ring-inset:  ;\n  --tw-ring-offset-width: 0px;\n  --tw-ring-offset-color: #fff;\n  --tw-ring-color: rgb(59 130 246 / 0.5);\n  --tw-ring-offset-shadow: 0 0 #0000;\n  --tw-ring-shadow: 0 0 #0000;\n  --tw-shadow: 0 0 #0000;\n  --tw-shadow-colored: 0 0 #0000;\n  --tw-blur:  ;\n  --tw-brightness:  ;\n  --tw-contrast:  ;\n  --tw-grayscale:  ;\n  --tw-hue-rotate:  ;\n  --tw-invert:  ;\n  --tw-saturate:  ;\n  --tw-sepia:  ;\n  --tw-drop-shadow:  ;\n  --tw-backdrop-blur:  ;\n  --tw-backdrop-brightness:  ;\n  --tw-backdrop-contrast:  ;\n  --tw-backdrop-grayscale:  ;\n  --tw-backdrop-hue-rotate:  ;\n  --tw-backdrop-invert:  ;\n  --tw-backdrop-opacity:  ;\n  --tw-backdrop-saturate:  ;\n  --tw-backdrop-sepia:  ;\n  --tw-contain-size:  ;\n  --tw-contain-layout:  ;\n  --tw-contain-paint:  ;\n  --tw-contain-style:  ;\n}\n\n.container {\n  width: 100%;\n}\n\n@media (min-width: 640px) {\n  .container {\n    max-width: 640px;\n  }\n}\n\n@media (min-width: 768px) {\n  .container {\n    max-width: 768px;\n  }\n}\n\n@media (min-width: 1024px) {\n  .container {\n    max-width: 1024px;\n  }\n}\n\n@media (min-width: 1280px) {\n  .container {\n    max-width: 1280px;\n  }\n}\n\n@media (min-width: 1536px) {\n  .container {\n    max-width: 1536px;\n  }\n}\n\n.prose {\n  color: var(--tw-prose-body);\n  max-width: 65ch;\n}\n\n.prose :where(p):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  margin-top: 1.25em;\n  margin-bottom: 1.25em;\n}\n\n.prose :where([class~=\"lead\"]):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  color: var(--tw-prose-lead);\n  font-size: 1.25em;\n  line-height: 1.6;\n  margin-top: 1.2em;\n  margin-bottom: 1.2em;\n}\n\n.prose :where(a):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  color: var(--tw-prose-links);\n  text-decoration: underline;\n  font-weight: 500;\n}\n\n.prose :where(strong):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  color: var(--tw-prose-bold);\n  font-weight: 600;\n}\n\n.prose :where(a strong):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  color: inherit;\n}\n\n.prose :where(blockquote strong):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  color: inherit;\n}\n\n.prose :where(thead th strong):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  color: inherit;\n}\n\n.prose :where(ol):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  list-style-type: decimal;\n  margin-top: 1.25em;\n  margin-bottom: 1.25em;\n  padding-inline-start: 1.625em;\n}\n\n.prose :where(ol[type=\"A\"]):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  list-style-type: upper-alpha;\n}\n\n.prose :where(ol[type=\"a\"]):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  list-style-type: lower-alpha;\n}\n\n.prose :where(ol[type=\"A\" s]):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  list-style-type: upper-alpha;\n}\n\n.prose :where(ol[type=\"a\" s]):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  list-style-type: lower-alpha;\n}\n\n.prose :where(ol[type=\"I\"]):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  list-style-type: upper-roman;\n}\n\n.prose :where(ol[type=\"i\"]):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  list-style-type: lower-roman;\n}\n\n.prose :where(ol[type=\"I\" s]):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  list-style-type: upper-roman;\n}\n\n.prose :where(ol[type=\"i\" s]):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  list-style-type: lower-roman;\n}\n\n.prose :where(ol[type=\"1\"]):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  list-style-type: decimal;\n}\n\n.prose :where(ul):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  list-style-type: disc;\n  margin-top: 1.25em;\n  margin-bottom: 1.25em;\n  padding-inline-start: 1.625em;\n}\n\n.prose :where(ol > li):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *))::marker {\n  font-weight: 400;\n  color: var(--tw-prose-counters);\n}\n\n.prose :where(ul > li):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *))::marker {\n  color: var(--tw-prose-bullets);\n}\n\n.prose :where(dt):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  color: var(--tw-prose-headings);\n  font-weight: 600;\n  margin-top: 1.25em;\n}\n\n.prose :where(hr):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  border-color: var(--tw-prose-hr);\n  border-top-width: 1px;\n  margin-top: 3em;\n  margin-bottom: 3em;\n}\n\n.prose :where(blockquote):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  font-weight: 500;\n  font-style: italic;\n  color: var(--tw-prose-quotes);\n  border-inline-start-width: 0.25rem;\n  border-inline-start-color: var(--tw-prose-quote-borders);\n  quotes: \"\\201C\"\"\\201D\"\"\\2018\"\"\\2019\";\n  margin-top: 1.6em;\n  margin-bottom: 1.6em;\n  padding-inline-start: 1em;\n}\n\n.prose :where(blockquote p:first-of-type):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *))::before {\n  content: open-quote;\n}\n\n.prose :where(blockquote p:last-of-type):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *))::after {\n  content: close-quote;\n}\n\n.prose :where(h1):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  color: var(--tw-prose-headings);\n  font-weight: 800;\n  font-size: 2.25em;\n  margin-top: 0;\n  margin-bottom: 0.8888889em;\n  line-height: 1.1111111;\n}\n\n.prose :where(h1 strong):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  font-weight: 900;\n  color: inherit;\n}\n\n.prose :where(h2):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  color: var(--tw-prose-headings);\n  font-weight: 700;\n  font-size: 1.5em;\n  margin-top: 2em;\n  margin-bottom: 1em;\n  line-height: 1.3333333;\n}\n\n.prose :where(h2 strong):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  font-weight: 800;\n  color: inherit;\n}\n\n.prose :where(h3):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  color: var(--tw-prose-headings);\n  font-weight: 600;\n  font-size: 1.25em;\n  margin-top: 1.6em;\n  margin-bottom: 0.6em;\n  line-height: 1.6;\n}\n\n.prose :where(h3 strong):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  font-weight: 700;\n  color: inherit;\n}\n\n.prose :where(h4):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  color: var(--tw-prose-headings);\n  font-weight: 600;\n  margin-top: 1.5em;\n  margin-bottom: 0.5em;\n  line-height: 1.5;\n}\n\n.prose :where(h4 strong):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  font-weight: 700;\n  color: inherit;\n}\n\n.prose :where(img):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  margin-top: 2em;\n  margin-bottom: 2em;\n}\n\n.prose :where(picture):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  display: block;\n  margin-top: 2em;\n  margin-bottom: 2em;\n}\n\n.prose :where(video):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  margin-top: 2em;\n  margin-bottom: 2em;\n}\n\n.prose :where(kbd):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  font-weight: 500;\n  font-family: inherit;\n  color: var(--tw-prose-kbd);\n  box-shadow: 0 0 0 1px rgb(var(--tw-prose-kbd-shadows) / 10%), 0 3px 0 rgb(var(--tw-prose-kbd-shadows) / 10%);\n  font-size: 0.875em;\n  border-radius: 0.3125rem;\n  padding-top: 0.1875em;\n  padding-inline-end: 0.375em;\n  padding-bottom: 0.1875em;\n  padding-inline-start: 0.375em;\n}\n\n.prose :where(code):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  color: var(--tw-prose-code);\n  font-weight: 600;\n  font-size: 0.875em;\n}\n\n.prose :where(code):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *))::before {\n  content: \"`\";\n}\n\n.prose :where(code):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *))::after {\n  content: \"`\";\n}\n\n.prose :where(a code):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  color: inherit;\n}\n\n.prose :where(h1 code):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  color: inherit;\n}\n\n.prose :where(h2 code):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  color: inherit;\n  font-size: 0.875em;\n}\n\n.prose :where(h3 code):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  color: inherit;\n  font-size: 0.9em;\n}\n\n.prose :where(h4 code):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  color: inherit;\n}\n\n.prose :where(blockquote code):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  color: inherit;\n}\n\n.prose :where(thead th code):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  color: inherit;\n}\n\n.prose :where(pre):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  color: var(--tw-prose-pre-code);\n  background-color: var(--tw-prose-pre-bg);\n  overflow-x: auto;\n  font-weight: 400;\n  font-size: 0.875em;\n  line-height: 1.7142857;\n  margin-top: 1.7142857em;\n  margin-bottom: 1.7142857em;\n  border-radius: 0.375rem;\n  padding-top: 0.8571429em;\n  padding-inline-end: 1.1428571em;\n  padding-bottom: 0.8571429em;\n  padding-inline-start: 1.1428571em;\n}\n\n.prose :where(pre code):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  background-color: transparent;\n  border-width: 0;\n  border-radius: 0;\n  padding: 0;\n  font-weight: inherit;\n  color: inherit;\n  font-size: inherit;\n  font-family: inherit;\n  line-height: inherit;\n}\n\n.prose :where(pre code):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *))::before {\n  content: none;\n}\n\n.prose :where(pre code):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *))::after {\n  content: none;\n}\n\n.prose :where(table):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  width: 100%;\n  table-layout: auto;\n  text-align: start;\n  margin-top: 2em;\n  margin-bottom: 2em;\n  font-size: 0.875em;\n  line-height: 1.7142857;\n}\n\n.prose :where(thead):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  border-bottom-width: 1px;\n  border-bottom-color: var(--tw-prose-th-borders);\n}\n\n.prose :where(thead th):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  color: var(--tw-prose-headings);\n  font-weight: 600;\n  vertical-align: bottom;\n  padding-inline-end: 0.5714286em;\n  padding-bottom: 0.5714286em;\n  padding-inline-start: 0.5714286em;\n}\n\n.prose :where(tbody tr):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  border-bottom-width: 1px;\n  border-bottom-color: var(--tw-prose-td-borders);\n}\n\n.prose :where(tbody tr:last-child):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  border-bottom-width: 0;\n}\n\n.prose :where(tbody td):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  vertical-align: baseline;\n}\n\n.prose :where(tfoot):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  border-top-width: 1px;\n  border-top-color: var(--tw-prose-th-borders);\n}\n\n.prose :where(tfoot td):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  vertical-align: top;\n}\n\n.prose :where(figure > *):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  margin-top: 0;\n  margin-bottom: 0;\n}\n\n.prose :where(figcaption):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  color: var(--tw-prose-captions);\n  font-size: 0.875em;\n  line-height: 1.4285714;\n  margin-top: 0.8571429em;\n}\n\n.prose {\n  --tw-prose-body: var(--foreground);\n  --tw-prose-headings: var(--foreground);\n  --tw-prose-lead: #4b5563;\n  --tw-prose-links: #111827;\n  --tw-prose-bold: #111827;\n  --tw-prose-counters: #6b7280;\n  --tw-prose-bullets: #d1d5db;\n  --tw-prose-hr: #e5e7eb;\n  --tw-prose-quotes: #111827;\n  --tw-prose-quote-borders: #e5e7eb;\n  --tw-prose-captions: #6b7280;\n  --tw-prose-kbd: #111827;\n  --tw-prose-kbd-shadows: 17 24 39;\n  --tw-prose-code: #111827;\n  --tw-prose-pre-code: #e5e7eb;\n  --tw-prose-pre-bg: #1f2937;\n  --tw-prose-th-borders: #d1d5db;\n  --tw-prose-td-borders: #e5e7eb;\n  --tw-prose-invert-body: #d1d5db;\n  --tw-prose-invert-headings: #fff;\n  --tw-prose-invert-lead: #9ca3af;\n  --tw-prose-invert-links: #fff;\n  --tw-prose-invert-bold: #fff;\n  --tw-prose-invert-counters: #9ca3af;\n  --tw-prose-invert-bullets: #4b5563;\n  --tw-prose-invert-hr: #374151;\n  --tw-prose-invert-quotes: #f3f4f6;\n  --tw-prose-invert-quote-borders: #374151;\n  --tw-prose-invert-captions: #9ca3af;\n  --tw-prose-invert-kbd: #fff;\n  --tw-prose-invert-kbd-shadows: 255 255 255;\n  --tw-prose-invert-code: #fff;\n  --tw-prose-invert-pre-code: #d1d5db;\n  --tw-prose-invert-pre-bg: rgb(0 0 0 / 50%);\n  --tw-prose-invert-th-borders: #4b5563;\n  --tw-prose-invert-td-borders: #374151;\n  font-size: 1rem;\n  line-height: 1.75;\n}\n\n.prose :where(picture > img):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  margin-top: 0;\n  margin-bottom: 0;\n}\n\n.prose :where(li):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  margin-top: 0.5em;\n  margin-bottom: 0.5em;\n}\n\n.prose :where(ol > li):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  padding-inline-start: 0.375em;\n}\n\n.prose :where(ul > li):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  padding-inline-start: 0.375em;\n}\n\n.prose :where(.prose > ul > li p):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  margin-top: 0.75em;\n  margin-bottom: 0.75em;\n}\n\n.prose :where(.prose > ul > li > p:first-child):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  margin-top: 1.25em;\n}\n\n.prose :where(.prose > ul > li > p:last-child):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  margin-bottom: 1.25em;\n}\n\n.prose :where(.prose > ol > li > p:first-child):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  margin-top: 1.25em;\n}\n\n.prose :where(.prose > ol > li > p:last-child):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  margin-bottom: 1.25em;\n}\n\n.prose :where(ul ul, ul ol, ol ul, ol ol):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  margin-top: 0.75em;\n  margin-bottom: 0.75em;\n}\n\n.prose :where(dl):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  margin-top: 1.25em;\n  margin-bottom: 1.25em;\n}\n\n.prose :where(dd):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  margin-top: 0.5em;\n  padding-inline-start: 1.625em;\n}\n\n.prose :where(hr + *):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  margin-top: 0;\n}\n\n.prose :where(h2 + *):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  margin-top: 0;\n}\n\n.prose :where(h3 + *):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  margin-top: 0;\n}\n\n.prose :where(h4 + *):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  margin-top: 0;\n}\n\n.prose :where(thead th:first-child):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  padding-inline-start: 0;\n}\n\n.prose :where(thead th:last-child):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  padding-inline-end: 0;\n}\n\n.prose :where(tbody td, tfoot td):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  padding-top: 0.5714286em;\n  padding-inline-end: 0.5714286em;\n  padding-bottom: 0.5714286em;\n  padding-inline-start: 0.5714286em;\n}\n\n.prose :where(tbody td:first-child, tfoot td:first-child):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  padding-inline-start: 0;\n}\n\n.prose :where(tbody td:last-child, tfoot td:last-child):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  padding-inline-end: 0;\n}\n\n.prose :where(figure):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  margin-top: 2em;\n  margin-bottom: 2em;\n}\n\n.prose :where(.prose > :first-child):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  margin-top: 0;\n}\n\n.prose :where(.prose > :last-child):not(:where([class~=\"not-prose\"],[class~=\"not-prose\"] *)) {\n  margin-bottom: 0;\n}\n\n.sr-only {\n  position: absolute;\n  width: 1px;\n  height: 1px;\n  padding: 0;\n  margin: -1px;\n  overflow: hidden;\n  clip: rect(0, 0, 0, 0);\n  white-space: nowrap;\n  border-width: 0;\n}\n\n.visible {\n  visibility: visible;\n}\n\n.static {\n  position: static;\n}\n\n.absolute {\n  position: absolute;\n}\n\n.relative {\n  position: relative;\n}\n\n.sticky {\n  position: sticky;\n}\n\n.inset-0 {\n  inset: 0px;\n}\n\n.bottom-0 {\n  bottom: 0px;\n}\n\n.left-0 {\n  left: 0px;\n}\n\n.right-0 {\n  right: 0px;\n}\n\n.top-0 {\n  top: 0px;\n}\n\n.top-\\[-490px\\] {\n  top: -490px;\n}\n\n.z-10 {\n  z-index: 10;\n}\n\n.z-50 {\n  z-index: 50;\n}\n\n.order-2 {\n  order: 2;\n}\n\n.order-7 {\n  order: 7;\n}\n\n.col-span-2 {\n  grid-column: span 2 / span 2;\n}\n\n.m-0 {\n  margin: 0px;\n}\n\n.mx-auto {\n  margin-left: auto;\n  margin-right: auto;\n}\n\n.my-1 {\n  margin-top: 0.25rem;\n  margin-bottom: 0.25rem;\n}\n\n.my-3 {\n  margin-top: 0.75rem;\n  margin-bottom: 0.75rem;\n}\n\n.my-5 {\n  margin-top: 1.25rem;\n  margin-bottom: 1.25rem;\n}\n\n.my-8 {\n  margin-top: 2rem;\n  margin-bottom: 2rem;\n}\n\n.my-\\[8rem\\] {\n  margin-top: 8rem;\n  margin-bottom: 8rem;\n}\n\n.my-16 {\n  margin-top: 4rem;\n  margin-bottom: 4rem;\n}\n\n.my-4 {\n  margin-top: 1rem;\n  margin-bottom: 1rem;\n}\n\n.my-\\[4rem\\] {\n  margin-top: 4rem;\n  margin-bottom: 4rem;\n}\n\n.my-\\[4\\] {\n  margin-top: 4;\n  margin-bottom: 4;\n}\n\n.my-\\[2rem\\] {\n  margin-top: 2rem;\n  margin-bottom: 2rem;\n}\n\n.-mt-1 {\n  margin-top: -0.25rem;\n}\n\n.mb-1 {\n  margin-bottom: 0.25rem;\n}\n\n.mb-10 {\n  margin-bottom: 2.5rem;\n}\n\n.mb-12 {\n  margin-bottom: 3rem;\n}\n\n.mb-20 {\n  margin-bottom: 5rem;\n}\n\n.mb-3 {\n  margin-bottom: 0.75rem;\n}\n\n.mb-4 {\n  margin-bottom: 1rem;\n}\n\n.mb-5 {\n  margin-bottom: 1.25rem;\n}\n\n.mb-8 {\n  margin-bottom: 2rem;\n}\n\n.ml-2\\.5 {\n  margin-left: 0.625rem;\n}\n\n.mr-2 {\n  margin-right: 0.5rem;\n}\n\n.mr-4 {\n  margin-right: 1rem;\n}\n\n.ms-auto {\n  margin-inline-start: auto;\n}\n\n.mt-12 {\n  margin-top: 3rem;\n}\n\n.mt-2 {\n  margin-top: 0.5rem;\n}\n\n.mt-8 {\n  margin-top: 2rem;\n}\n\n.mt-\\[-60px\\] {\n  margin-top: -60px;\n}\n\n.mt-\\[106px\\] {\n  margin-top: 106px;\n}\n\n.mt-\\[90px\\] {\n  margin-top: 90px;\n}\n\n.mt-\\[9px\\] {\n  margin-top: 9px;\n}\n\n.mt-px {\n  margin-top: 1px;\n}\n\n.mb-2 {\n  margin-bottom: 0.5rem;\n}\n\n.mt-14 {\n  margin-top: 3.5rem;\n}\n\n.mt-16 {\n  margin-top: 4rem;\n}\n\n.block {\n  display: block;\n}\n\n.inline-block {\n  display: inline-block;\n}\n\n.inline {\n  display: inline;\n}\n\n.flex {\n  display: flex;\n}\n\n.inline-flex {\n  display: inline-flex;\n}\n\n.table {\n  display: table;\n}\n\n.grid {\n  display: grid;\n}\n\n.contents {\n  display: contents;\n}\n\n.hidden {\n  display: none;\n}\n\n.h-14 {\n  height: 3.5rem;\n}\n\n.h-5 {\n  height: 1.25rem;\n}\n\n.h-8 {\n  height: 2rem;\n}\n\n.h-9 {\n  height: 2.25rem;\n}\n\n.h-full {\n  height: 100%;\n}\n\n.max-h-\\[230px\\] {\n  max-height: 230px;\n}\n\n.max-h-\\[600px\\] {\n  max-height: 600px;\n}\n\n.min-h-screen {\n  min-height: 100vh;\n}\n\n.w-5 {\n  width: 1.25rem;\n}\n\n.w-8 {\n  width: 2rem;\n}\n\n.w-full {\n  width: 100%;\n}\n\n.min-w-0 {\n  min-width: 0px;\n}\n\n.min-w-\\[100\\%\\] {\n  min-width: 100%;\n}\n\n.max-w-\\[100\\%\\] {\n  max-width: 100%;\n}\n\n.max-w-\\[1024px\\] {\n  max-width: 1024px;\n}\n\n.max-w-\\[200px\\] {\n  max-width: 200px;\n}\n\n.max-w-\\[50rem\\] {\n  max-width: 50rem;\n}\n\n.max-w-\\[52rem\\] {\n  max-width: 52rem;\n}\n\n.max-w-\\[60rem\\] {\n  max-width: 60rem;\n}\n\n.flex-1 {\n  flex: 1 1 0%;\n}\n\n.grow {\n  flex-grow: 1;\n}\n\n.flex-col {\n  flex-direction: column;\n}\n\n.flex-wrap {\n  flex-wrap: wrap;\n}\n\n.items-start {\n  align-items: flex-start;\n}\n\n.items-center {\n  align-items: center;\n}\n\n.justify-start {\n  justify-content: flex-start;\n}\n\n.justify-center {\n  justify-content: center;\n}\n\n.justify-between {\n  justify-content: space-between;\n}\n\n.gap-10 {\n  gap: 2.5rem;\n}\n\n.gap-3 {\n  gap: 0.75rem;\n}\n\n.gap-4 {\n  gap: 1rem;\n}\n\n.gap-5 {\n  gap: 1.25rem;\n}\n\n.gap-6 {\n  gap: 1.5rem;\n}\n\n.space-x-2 > :not([hidden]) ~ :not([hidden]) {\n  --tw-space-x-reverse: 0;\n  margin-right: calc(0.5rem * var(--tw-space-x-reverse));\n  margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse)));\n}\n\n.overflow-auto {\n  overflow: auto;\n}\n\n.overflow-visible {\n  overflow: visible;\n}\n\n.truncate {\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n.whitespace-nowrap {\n  white-space: nowrap;\n}\n\n.rounded {\n  border-radius: 0.25rem;\n}\n\n.rounded-\\[0\\.5rem\\] {\n  border-radius: 0.5rem;\n}\n\n.rounded-lg {\n  border-radius: var(--radius);\n}\n\n.rounded-md {\n  border-radius: calc(var(--radius) - 2px);\n}\n\n.rounded-sm {\n  border-radius: calc(var(--radius) - 4px);\n}\n\n.border {\n  border-width: 1px;\n}\n\n.border-b {\n  border-bottom-width: 1px;\n}\n\n.border-gray-200 {\n  --tw-border-opacity: 1;\n  border-color: rgb(229 231 235 / var(--tw-border-opacity));\n}\n\n.border-red-100 {\n  --tw-border-opacity: 1;\n  border-color: rgb(254 226 226 / var(--tw-border-opacity));\n}\n\n.border-transparent {\n  border-color: transparent;\n}\n\n.bg-background {\n  background-color: var(--background);\n}\n\n.bg-card {\n  background-color: var(--card);\n}\n\n.bg-gray-100 {\n  --tw-bg-opacity: 1;\n  background-color: rgb(243 244 246 / var(--tw-bg-opacity));\n}\n\n.bg-gray-50 {\n  --tw-bg-opacity: 1;\n  background-color: rgb(249 250 251 / var(--tw-bg-opacity));\n}\n\n.bg-inherit {\n  background-color: inherit;\n}\n\n.bg-orangerust {\n  background-color: var(--orangerust);\n}\n\n.bg-red-900 {\n  --tw-bg-opacity: 1;\n  background-color: rgb(127 29 29 / var(--tw-bg-opacity));\n}\n\n.bg-redrust {\n  background-color: var(--redrust);\n}\n\n.bg-transparent {\n  background-color: transparent;\n}\n\n.bg-left-bottom {\n  background-position: left bottom;\n}\n\n.p-0 {\n  padding: 0px;\n}\n\n.p-2 {\n  padding: 0.5rem;\n}\n\n.p-4 {\n  padding: 1rem;\n}\n\n.p-8 {\n  padding: 2rem;\n}\n\n.px-0 {\n  padding-left: 0px;\n  padding-right: 0px;\n}\n\n.px-1 {\n  padding-left: 0.25rem;\n  padding-right: 0.25rem;\n}\n\n.px-2 {\n  padding-left: 0.5rem;\n  padding-right: 0.5rem;\n}\n\n.px-4 {\n  padding-left: 1rem;\n  padding-right: 1rem;\n}\n\n.py-0 {\n  padding-top: 0px;\n  padding-bottom: 0px;\n}\n\n.py-1 {\n  padding-top: 0.25rem;\n  padding-bottom: 0.25rem;\n}\n\n.py-2 {\n  padding-top: 0.5rem;\n  padding-bottom: 0.5rem;\n}\n\n.py-6 {\n  padding-top: 1.5rem;\n  padding-bottom: 1.5rem;\n}\n\n.py-8 {\n  padding-top: 2rem;\n  padding-bottom: 2rem;\n}\n\n.py-\\[15px\\] {\n  padding-top: 15px;\n  padding-bottom: 15px;\n}\n\n.py-4 {\n  padding-top: 1rem;\n  padding-bottom: 1rem;\n}\n\n.pb-12 {\n  padding-bottom: 3rem;\n}\n\n.pb-20 {\n  padding-bottom: 5rem;\n}\n\n.pb-3 {\n  padding-bottom: 0.75rem;\n}\n\n.pl-5 {\n  padding-left: 1.25rem;\n}\n\n.pt-20 {\n  padding-top: 5rem;\n}\n\n.pt-8 {\n  padding-top: 2rem;\n}\n\n.pt-2 {\n  padding-top: 0.5rem;\n}\n\n.text-left {\n  text-align: left;\n}\n\n.text-center {\n  text-align: center;\n}\n\n.text-2xl {\n  font-size: 1.5rem;\n  line-height: 2rem;\n}\n\n.text-3xl {\n  font-size: 1.875rem;\n  line-height: 2.25rem;\n}\n\n.text-\\[11px\\] {\n  font-size: 11px;\n}\n\n.text-\\[9px\\] {\n  font-size: 9px;\n}\n\n.text-base {\n  font-size: 1rem;\n  line-height: 1.5rem;\n}\n\n.text-lg {\n  font-size: 1.125rem;\n  line-height: 1.75rem;\n}\n\n.text-sm {\n  font-size: 0.875rem;\n  line-height: 1.25rem;\n}\n\n.text-xl {\n  font-size: 1.25rem;\n  line-height: 1.75rem;\n}\n\n.font-extrabold {\n  font-weight: 800;\n}\n\n.font-medium {\n  font-weight: 500;\n}\n\n.font-normal {\n  font-weight: 400;\n}\n\n.font-semibold {\n  font-weight: 600;\n}\n\n.text-background {\n  color: var(--background);\n}\n\n.text-black {\n  --tw-text-opacity: 1;\n  color: rgb(0 0 0 / var(--tw-text-opacity));\n}\n\n.text-card-foreground {\n  color: var(--card-foreground);\n}\n\n.text-foreground {\n  color: var(--foreground);\n}\n\n.text-gray-500 {\n  --tw-text-opacity: 1;\n  color: rgb(107 114 128 / var(--tw-text-opacity));\n}\n\n.text-green-400 {\n  --tw-text-opacity: 1;\n  color: rgb(74 222 128 / var(--tw-text-opacity));\n}\n\n.text-inherit {\n  color: inherit;\n}\n\n.text-red-100 {\n  --tw-text-opacity: 1;\n  color: rgb(254 226 226 / var(--tw-text-opacity));\n}\n\n.text-red-200 {\n  --tw-text-opacity: 1;\n  color: rgb(254 202 202 / var(--tw-text-opacity));\n}\n\n.text-red-400 {\n  --tw-text-opacity: 1;\n  color: rgb(248 113 113 / var(--tw-text-opacity));\n}\n\n.text-red-50 {\n  --tw-text-opacity: 1;\n  color: rgb(254 242 242 / var(--tw-text-opacity));\n}\n\n.text-sky-300 {\n  --tw-text-opacity: 1;\n  color: rgb(125 211 252 / var(--tw-text-opacity));\n}\n\n.text-sky-500 {\n  --tw-text-opacity: 1;\n  color: rgb(14 165 233 / var(--tw-text-opacity));\n}\n\n.text-white {\n  --tw-text-opacity: 1;\n  color: rgb(255 255 255 / var(--tw-text-opacity));\n}\n\n.text-yellow-400 {\n  --tw-text-opacity: 1;\n  color: rgb(250 204 21 / var(--tw-text-opacity));\n}\n\n.text-yellow-500 {\n  --tw-text-opacity: 1;\n  color: rgb(234 179 8 / var(--tw-text-opacity));\n}\n\n.placeholder-black::-moz-placeholder {\n  --tw-placeholder-opacity: 1;\n  color: rgb(0 0 0 / var(--tw-placeholder-opacity));\n}\n\n.placeholder-black::placeholder {\n  --tw-placeholder-opacity: 1;\n  color: rgb(0 0 0 / var(--tw-placeholder-opacity));\n}\n\n.shadow {\n  --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);\n  --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);\n  box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);\n}\n\n.shadow-lg {\n  --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);\n  --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);\n  box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);\n}\n\n.shadow-none {\n  --tw-shadow: 0 0 #0000;\n  --tw-shadow-colored: 0 0 #0000;\n  box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);\n}\n\n.backdrop-blur {\n  --tw-backdrop-blur: blur(8px);\n  -webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);\n  backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);\n}\n\n.transition-colors {\n  transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;\n  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n  transition-duration: 150ms;\n}\n\n@import \"tailwindcss/typography\";\n\n.infobox {\n  position: relative;\n  margin-top: 2rem;\n  margin-bottom: 2rem;\n  width: 100%;\n  border-radius: var(--radius);\n  border-width: 1px;\n  border-color: rgb(34 197 94 / 0.5);\n  --tw-bg-opacity: 1;\n  background-color: rgb(240 253 244 / var(--tw-bg-opacity));\n  padding-right: 1rem;\n  padding-top: 0.75rem;\n  padding-bottom: 0.75rem;\n  padding-left: 2.5rem;\n  font-size: 0.875rem;\n  line-height: 1.25rem;\n  --tw-text-opacity: 1;\n  color: rgb(34 197 94 / var(--tw-text-opacity));\n}\n\n.infobox:is(.dark *) {\n  --tw-border-opacity: 1;\n  border-color: rgb(34 197 94 / var(--tw-border-opacity));\n  --tw-bg-opacity: 1;\n  background-color: rgb(5 46 22 / var(--tw-bg-opacity));\n}\n\n.infobox {\n  &:before{\n    content: '';\n    width: 15px;\n    height: 15px;\n    background-color: currentColor;\n    -webkit-mask: url(\"data:image/svg+xml,%3Csvg width='15' height='15' viewBox='0 0 15 15' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M7.49991 0.876892C3.84222 0.876892 0.877075 3.84204 0.877075 7.49972C0.877075 11.1574 3.84222 14.1226 7.49991 14.1226C11.1576 14.1226 14.1227 11.1574 14.1227 7.49972C14.1227 3.84204 11.1576 0.876892 7.49991 0.876892ZM1.82707 7.49972C1.82707 4.36671 4.36689 1.82689 7.49991 1.82689C10.6329 1.82689 13.1727 4.36671 13.1727 7.49972C13.1727 10.6327 10.6329 13.1726 7.49991 13.1726C4.36689 13.1726 1.82707 10.6327 1.82707 7.49972ZM8.24992 4.49999C8.24992 4.9142 7.91413 5.24999 7.49992 5.24999C7.08571 5.24999 6.74992 4.9142 6.74992 4.49999C6.74992 4.08577 7.08571 3.74999 7.49992 3.74999C7.91413 3.74999 8.24992 4.08577 8.24992 4.49999ZM6.00003 5.99999H6.50003H7.50003C7.77618 5.99999 8.00003 6.22384 8.00003 6.49999V9.99999H8.50003H9.00003V11H8.50003H7.50003H6.50003H6.00003V9.99999H6.50003H7.00003V6.99999H6.50003H6.00003V5.99999Z' fill='currentColor' fill-rule='evenodd' clip-rule='evenodd'%3E%3C/path%3E%3C/svg%3E\");\n            mask: url(\"data:image/svg+xml,%3Csvg width='15' height='15' viewBox='0 0 15 15' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M7.49991 0.876892C3.84222 0.876892 0.877075 3.84204 0.877075 7.49972C0.877075 11.1574 3.84222 14.1226 7.49991 14.1226C11.1576 14.1226 14.1227 11.1574 14.1227 7.49972C14.1227 3.84204 11.1576 0.876892 7.49991 0.876892ZM1.82707 7.49972C1.82707 4.36671 4.36689 1.82689 7.49991 1.82689C10.6329 1.82689 13.1727 4.36671 13.1727 7.49972C13.1727 10.6327 10.6329 13.1726 7.49991 13.1726C4.36689 13.1726 1.82707 10.6327 1.82707 7.49972ZM8.24992 4.49999C8.24992 4.9142 7.91413 5.24999 7.49992 5.24999C7.08571 5.24999 6.74992 4.9142 6.74992 4.49999C6.74992 4.08577 7.08571 3.74999 7.49992 3.74999C7.91413 3.74999 8.24992 4.08577 8.24992 4.49999ZM6.00003 5.99999H6.50003H7.50003C7.77618 5.99999 8.00003 6.22384 8.00003 6.49999V9.99999H8.50003H9.00003V11H8.50003H7.50003H6.50003H6.00003V9.99999H6.50003H7.00003V6.99999H6.50003H6.00003V5.99999Z' fill='currentColor' fill-rule='evenodd' clip-rule='evenodd'%3E%3C/path%3E%3C/svg%3E\");\n  }\n  &:before {\n    position: absolute;\n  }\n  &:before {\n    left: 1rem;\n  }\n  &:before {\n    top: 1rem;\n  }\n  &:before {\n    --tw-translate-y: -3px;\n    transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n  }\n}\n\n.section-colored {\n  h1, h2 {\n    color: var(--background);\n  }\n}\n\n.dark {\n  .section-colored {\n    h1,\n            h2 {\n      --tw-text-opacity: 1;\n      color: rgb(255 255 255 / var(--tw-text-opacity));\n    }\n  }\n}\n\n.section {\n  padding-left: 0.5rem;\n  padding-right: 0.5rem;\n}\n\n@media (min-width: 640px) {\n  .section {\n    padding-left: 0px;\n    padding-right: 0px;\n  }\n}\n\n.section {\n  h1 {\n    font-weight: 600;\n  }\n  @media (min-width: 640px) {\n    h1 {\n      font-size: 3.75rem;\n      line-height: 1;\n    }\n  }\n  h1 {\n    margin-bottom: 2rem;\n  }\n  h1 {\n    letter-spacing: -0.05em;\n  }\n  h2 {\n    font-size: 1.25rem;\n    line-height: 1.75rem;\n  }\n  h2 {\n    font-weight: 300;\n  }\n  @media (min-width: 640px) {\n    h2 {\n      font-size: 1.875rem;\n      line-height: 2.25rem;\n    }\n  }\n  h2 {\n    margin-top: 0px;\n  }\n  pre {\n    margin-top: 0px;\n    margin-bottom: 0px;\n  }\n}\n\n.footer-links {\n  a {\n    margin-left: 0.5rem;\n    margin-right: 0.5rem;\n  }\n  a {\n    --tw-text-opacity: 1;\n    color: rgb(255 255 255 / var(--tw-text-opacity));\n  }\n  a {\n    text-decoration-line: none;\n  }\n}\n\n.fatbtn {\n  border-radius: calc(var(--radius) - 4px);\n  background-color: var(--background);\n  padding-left: 2rem;\n  padding-right: 2rem;\n  padding-top: 0.75rem;\n  padding-bottom: 0.75rem;\n  font-size: 1.125rem;\n  line-height: 1.75rem;\n  font-weight: 600;\n  color: var(--foreground);\n  text-decoration-line: none;\n}\n\n.video-container {\n  position: relative;\n  padding-bottom: 56.25%;\n  /* 16:9 */\n  height: 0;\n}\n\n.video-container iframe {\n  position: absolute;\n  top: 0;\n  left: 0;\n  width: 100%;\n  height: 100%;\n}\n\n.container {\n  width: 100%;\n  margin-right: auto;\n  margin-left: auto;\n  padding-left: 1rem;\n  padding-right: 1rem\n}\n\n@media (min-width: 640px) {\n  .container {\n    padding-left: 1.5rem;\n    padding-right: 1.5rem;\n  }\n}\n\n@media (min-width: 768px) {\n  .container {\n    padding-left: 2rem;\n    padding-right: 2rem;\n  }\n}\n\n@media (min-width: 1024px) {\n  .container {\n    padding-left: 3rem;\n    padding-right: 3rem;\n  }\n}\n\n.prose {\n  ul {\n    padding-inline-start: 1em;\n  }\n}\n\n.navbar-form {\n  position: relative;\n}\n\n#suggestions {\n  position: absolute;\n  right: 0;\n  margin-top: 0.5rem;\n  width: calc(100vw - 8rem);\n  max-height: 500px;\n  overflow: auto;\n  background-color: var(--background);\n}\n\n#suggestions a {\n  display: block;\n  text-decoration: none;\n  padding: 0.75rem;\n  margin: 0 0.5rem;\n}\n\n#suggestions a:focus {\n  --tw-bg-opacity: 1;\n  background-color: rgb(243 244 246 / var(--tw-bg-opacity));\n}\n\n#suggestions a:focus:is(.dark *) {\n  --tw-bg-opacity: 1;\n  background-color: rgb(63 63 70 / var(--tw-bg-opacity));\n}\n\n#suggestions a:focus {\n  outline: 0;\n}\n\n#suggestions div:not(:first-child) {\n  --tw-border-opacity: 1;\n  border-color: rgb(229 231 235 / var(--tw-border-opacity));\n}\n\n#suggestions div:first-child {\n  margin-top: 0.5rem;\n}\n\n#suggestions div:last-child {\n  margin-bottom: 0.5rem;\n}\n\n#suggestions a:hover {\n  --tw-bg-opacity: 1;\n  background-color: rgb(243 244 246 / var(--tw-bg-opacity));\n}\n\n#suggestions a:hover:is(.dark *) {\n  --tw-bg-opacity: 1;\n  background-color: rgb(39 39 42 / var(--tw-bg-opacity));\n}\n\n#suggestions span {\n  font-size: 0.875rem;\n  line-height: 1.25rem;\n}\n\n#suggestions span:first-child {\n  color: var(--foreground);\n  font-weight: 600;\n}\n\n#suggestions span:nth-child(2) {\n  --tw-text-opacity: 1;\n  color: rgb(55 65 81 / var(--tw-text-opacity));\n}\n\n#suggestions span:nth-child(2):is(.dark *) {\n  --tw-text-opacity: 1;\n  color: rgb(156 163 175 / var(--tw-text-opacity));\n}\n\n@media (min-width: 640px) {\n  #suggestions {\n    width: 30rem;\n  }\n\n  #suggestions a {\n    display: flex;\n  }\n\n  #suggestions span:first-child {\n    width: 9rem;\n    padding-right: 1rem;\n    --tw-border-opacity: 1;\n    border-color: rgb(229 231 235 / var(--tw-border-opacity));\n    display: inline-block;\n    text-align: right;\n  }\n\n  #suggestions span:nth-child(2) {\n    width: 19rem;\n    padding-left: 1rem;\n  }\n}\n\nhtml .toggle-dark {\n  display: block;\n}\n\nhtml .toggle-light {\n  display: none;\n}\n\nhtml.dark .toggle-light {\n  display: block;\n}\n\nhtml.dark .toggle-dark {\n  display: none;\n}\n\n.dark\\:prose-invert:is(.dark *) {\n  --tw-prose-body: var(--tw-prose-invert-body);\n  --tw-prose-headings: var(--tw-prose-invert-headings);\n  --tw-prose-lead: var(--tw-prose-invert-lead);\n  --tw-prose-links: var(--tw-prose-invert-links);\n  --tw-prose-bold: var(--tw-prose-invert-bold);\n  --tw-prose-counters: var(--tw-prose-invert-counters);\n  --tw-prose-bullets: var(--tw-prose-invert-bullets);\n  --tw-prose-hr: var(--tw-prose-invert-hr);\n  --tw-prose-quotes: var(--tw-prose-invert-quotes);\n  --tw-prose-quote-borders: var(--tw-prose-invert-quote-borders);\n  --tw-prose-captions: var(--tw-prose-invert-captions);\n  --tw-prose-kbd: var(--tw-prose-invert-kbd);\n  --tw-prose-kbd-shadows: var(--tw-prose-invert-kbd-shadows);\n  --tw-prose-code: var(--tw-prose-invert-code);\n  --tw-prose-pre-code: var(--tw-prose-invert-pre-code);\n  --tw-prose-pre-bg: var(--tw-prose-invert-pre-bg);\n  --tw-prose-th-borders: var(--tw-prose-invert-th-borders);\n  --tw-prose-td-borders: var(--tw-prose-invert-td-borders);\n}\n\n.hover\\:bg-gray-50:hover {\n  --tw-bg-opacity: 1;\n  background-color: rgb(249 250 251 / var(--tw-bg-opacity));\n}\n\n.hover\\:bg-transparent:hover {\n  background-color: transparent;\n}\n\n.hover\\:text-white:hover {\n  --tw-text-opacity: 1;\n  color: rgb(255 255 255 / var(--tw-text-opacity));\n}\n\n.hover\\:underline:hover {\n  text-decoration-line: underline;\n}\n\n.focus-visible\\:bg-transparent:focus-visible {\n  background-color: transparent;\n}\n\n.focus-visible\\:outline-none:focus-visible {\n  outline: 2px solid transparent;\n  outline-offset: 2px;\n}\n\n.focus-visible\\:ring-0:focus-visible {\n  --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);\n  --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color);\n  box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);\n}\n\n.focus-visible\\:ring-1:focus-visible {\n  --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);\n  --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);\n  box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);\n}\n\n.focus-visible\\:ring-offset-0:focus-visible {\n  --tw-ring-offset-width: 0px;\n}\n\n.disabled\\:pointer-events-none:disabled {\n  pointer-events: none;\n}\n\n.disabled\\:opacity-50:disabled {\n  opacity: 0.5;\n}\n\n.dark\\:rounded-sm:is(.dark *) {\n  border-radius: calc(var(--radius) - 4px);\n}\n\n.dark\\:border:is(.dark *) {\n  border-width: 1px;\n}\n\n.dark\\:border-gray-700:is(.dark *) {\n  --tw-border-opacity: 1;\n  border-color: rgb(55 65 81 / var(--tw-border-opacity));\n}\n\n.dark\\:bg-background:is(.dark *) {\n  background-color: var(--background);\n}\n\n.dark\\:bg-foreground:is(.dark *) {\n  background-color: var(--foreground);\n}\n\n.dark\\:bg-zinc-700:is(.dark *) {\n  --tw-bg-opacity: 1;\n  background-color: rgb(63 63 70 / var(--tw-bg-opacity));\n}\n\n.dark\\:bg-zinc-800:is(.dark *) {\n  --tw-bg-opacity: 1;\n  background-color: rgb(39 39 42 / var(--tw-bg-opacity));\n}\n\n.dark\\:bg-zinc-900:is(.dark *) {\n  --tw-bg-opacity: 1;\n  background-color: rgb(24 24 27 / var(--tw-bg-opacity));\n}\n\n.dark\\:fill-zinc-700:is(.dark *) {\n  fill: #3f3f46;\n}\n\n.dark\\:fill-zinc-900:is(.dark *) {\n  fill: #18181b;\n}\n\n.dark\\:text-background:is(.dark *) {\n  color: var(--background);\n}\n\n.dark\\:text-sky-700:is(.dark *) {\n  --tw-text-opacity: 1;\n  color: rgb(3 105 161 / var(--tw-text-opacity));\n}\n\n.dark\\:text-white:is(.dark *) {\n  --tw-text-opacity: 1;\n  color: rgb(255 255 255 / var(--tw-text-opacity));\n}\n\n.dark\\:text-zinc-400:is(.dark *) {\n  --tw-text-opacity: 1;\n  color: rgb(161 161 170 / var(--tw-text-opacity));\n}\n\n.dark\\:placeholder-white:is(.dark *)::-moz-placeholder {\n  --tw-placeholder-opacity: 1;\n  color: rgb(255 255 255 / var(--tw-placeholder-opacity));\n}\n\n.dark\\:placeholder-white:is(.dark *)::placeholder {\n  --tw-placeholder-opacity: 1;\n  color: rgb(255 255 255 / var(--tw-placeholder-opacity));\n}\n\n.dark\\:hover\\:bg-gray-900:hover:is(.dark *) {\n  --tw-bg-opacity: 1;\n  background-color: rgb(17 24 39 / var(--tw-bg-opacity));\n}\n\n@media (min-width: 640px) {\n  .sm\\:my-6 {\n    margin-top: 1.5rem;\n    margin-bottom: 1.5rem;\n  }\n\n  .sm\\:my-\\[4rem\\] {\n    margin-top: 4rem;\n    margin-bottom: 4rem;\n  }\n\n  .sm\\:my-\\[8rem\\] {\n    margin-top: 8rem;\n    margin-bottom: 8rem;\n  }\n\n  .sm\\:flex {\n    display: flex;\n  }\n\n  .sm\\:hidden {\n    display: none;\n  }\n\n  .sm\\:w-7\\/12 {\n    width: 58.333333%;\n  }\n\n  .sm\\:flex-row {\n    flex-direction: row;\n  }\n\n  .sm\\:p-6 {\n    padding: 1.5rem;\n  }\n\n  .sm\\:px-0 {\n    padding-left: 0px;\n    padding-right: 0px;\n  }\n\n  .sm\\:pr-0 {\n    padding-right: 0px;\n  }\n\n  .sm\\:pr-12 {\n    padding-right: 3rem;\n  }\n\n  .sm\\:text-\\[16px\\] {\n    font-size: 16px;\n  }\n}\n\n@media (min-width: 768px) {\n  .md\\:my-\\[8rem\\] {\n    margin-top: 8rem;\n    margin-bottom: 8rem;\n  }\n\n  .md\\:my-\\[6rem\\] {\n    margin-top: 6rem;\n    margin-bottom: 6rem;\n  }\n\n  .md\\:my-\\[4rem\\] {\n    margin-top: 4rem;\n    margin-bottom: 4rem;\n  }\n\n  .md\\:mt-0 {\n    margin-top: 0px;\n  }\n\n  .md\\:mb-2 {\n    margin-bottom: 0.5rem;\n  }\n\n  .md\\:mb-4 {\n    margin-bottom: 1rem;\n  }\n\n  .md\\:grid {\n    display: grid;\n  }\n\n  .md\\:hidden {\n    display: none;\n  }\n\n  .md\\:grid-cols-\\[240px_minmax\\(0\\2c 1fr\\)\\] {\n    grid-template-columns: 240px minmax(0,1fr);\n  }\n\n  .md\\:justify-start {\n    justify-content: flex-start;\n  }\n\n  .md\\:gap-10 {\n    gap: 2.5rem;\n  }\n\n  .md\\:py-8 {\n    padding-top: 2rem;\n    padding-bottom: 2rem;\n  }\n\n  .md\\:pr-12 {\n    padding-right: 3rem;\n  }\n}\n\n@media (min-width: 1024px) {\n  .lg\\:flex {\n    display: flex;\n  }\n\n  .lg\\:max-w-64 {\n    max-width: 16rem;\n  }\n\n  .lg\\:justify-end {\n    justify-content: flex-end;\n  }\n\n  .lg\\:py-8 {\n    padding-top: 2rem;\n    padding-bottom: 2rem;\n  }\n}\n\n@media (min-width: 1280px) {\n  .xl\\:col-span-1 {\n    grid-column: span 1 / span 1;\n  }\n\n  .xl\\:mx-auto {\n    margin-left: auto;\n    margin-right: auto;\n  }\n\n  .xl\\:block {\n    display: block;\n  }\n\n  .xl\\:grid {\n    display: grid;\n  }\n\n  .xl\\:max-w-\\[80rem\\] {\n    max-width: 80rem;\n  }\n\n  .xl\\:grid-cols-2 {\n    grid-template-columns: repeat(2, minmax(0, 1fr));\n  }\n\n  .xl\\:grid-cols-\\[1fr_450px\\] {\n    grid-template-columns: 1fr 450px;\n  }\n}\n"
  },
  {
    "path": "docs-site/static/syntax-theme-dark.css",
    "content": "/*\n * theme \"OneHalfLight\" generated by syntect\n */\n\n.dark {\n  .z-code {\n  color: #dcdfe4;\n  background-color: #282c34;\n  border: 1px solid #5c6370;\n  }\n\n  .z-comment {\n  color: #5c6370;\n  }\n  .z-variable.z-parameter.z-function {\n  color: #dcdfe4;\n  }\n  .z-keyword {\n  color: #c678dd;\n  }\n  .z-variable {\n  color: #e06c75;\n  }\n  .z-entity.z-name.z-function, .z-meta.z-require, .z-support.z-function.z-any-method {\n  color: #61afef;\n  }\n  .z-support.z-class, .z-entity.z-name.z-class, .z-entity.z-name.z-type.z-class {\n  color: #e5c07b;\n  }\n  .z-meta.z-class {\n  color: #e5c07b;\n  }\n  .z-keyword.z-other.z-special-method {\n  color: #61afef;\n  }\n  .z-storage {\n  color: #c678dd;\n  }\n  .z-support.z-function {\n  color: #61afef;\n  }\n  .z-string {\n  color: #98c379;\n  }\n  .z-constant.z-numeric {\n  color: #e5c07b;\n  }\n  .z-none {\n  color: #e5c07b;\n  }\n  .z-none {\n  color: #e5c07b;\n  }\n  .z-constant {\n  color: #e5c07b;\n  }\n  .z-entity.z-name.z-tag {\n  color: #e06c75;\n  }\n  .z-entity.z-other.z-attribute-name {\n  color: #e5c07b;\n  }\n  .z-entity.z-other.z-attribute-name.z-id, .z-punctuation.z-definition.z-entity {\n  color: #e5c07b;\n  }\n  .z-meta.z-selector {\n  color: #c678dd;\n  }\n  .z-markup.z-heading .z-punctuation.z-definition.z-heading, .z-entity.z-name.z-section {\n  color: #61afef;\n  }\n  .z-markup.z-bold, .z-punctuation.z-definition.z-bold {\n  color: #c678dd;\n  }\n  .z-markup.z-italic, .z-punctuation.z-definition.z-italic {\n  color: #c678dd;\n  }\n  .z-markup.z-raw.z-inline {\n  color: #98c379;\n  }\n  .z-meta.z-link {\n  color: #98c379;\n  }\n  .z-markup.z-quote {\n  color: #98c379;\n  }\n  .z-source.z-java .z-meta.z-class.z-java .z-meta.z-method.z-java {\n  color: #dcdfe4;\n  }\n  .z-source.z-java .z-meta.z-class.z-java .z-meta.z-class.z-body.z-java {\n  color: #dcdfe4;\n  }\n  .z-source.z-js .z-meta.z-function.z-js .z-variable.z-parameter.z-function.z-js {\n  color: #e06c75;\n  }\n  .z-source.z-js .z-variable.z-other.z-readwrite.z-js {\n  color: #e06c75;\n  }\n  .z-source.z-js .z-variable.z-other.z-object.z-js {\n  color: #dcdfe4;\n  }\n  .z-source.z-js .z-meta.z-function-call.z-method.z-js .z-variable.z-other.z-readwrite.z-js {\n  color: #e06c75;\n  }\n  .z-source.z-js .z-meta.z-block.z-js .z-variable.z-other.z-readwrite.z-js {\n  color: #e06c75;\n  }\n  .z-source.z-js .z-meta.z-block.z-js .z-variable.z-other.z-object.z-js {\n  color: #dcdfe4;\n  }\n  .z-source.z-js .z-meta.z-block.z-js .z-meta.z-function-call.z-method.z-js .z-variable.z-other.z-readwrite.z-js {\n  color: #dcdfe4;\n  }\n  .z-source.z-js .z-meta.z-function-call.z-method.z-js .z-variable.z-function.z-js {\n  color: #dcdfe4;\n  }\n  .z-source.z-js .z-meta.z-property.z-object.z-js .z-entity.z-name.z-function.z-js {\n  color: #61afef;\n  }\n  .z-source.z-js .z-support.z-constant.z-prototype.z-js {\n  color: #dcdfe4;\n  }\n  .z-markup.z-inserted {\n  color: #98c379;\n  }\n  .z-markup.z-deleted {\n  color: #e06c75;\n  }\n  .z-markup.z-changed {\n  color: #e5c07b;\n  }\n  .z-string.z-regexp {\n  color: #98c379;\n  }\n  .z-constant.z-character.z-escape {\n  color: #56b6c2;\n  }\n  .z-invalid.z-illegal {\n  color: #dcdfe4;\n  background-color: #e06c75;\n  }\n  .z-invalid.z-broken {\n  color: #dcdfe4;\n  background-color: #e5c07b;\n  }\n  .z-invalid.z-deprecated {\n  color: #dcdfe4;\n  background-color: #e5c07b;\n  }\n  .z-invalid.z-unimplemented {\n  color: #dcdfe4;\n  background-color: #c678dd;\n  }\n}\n"
  },
  {
    "path": "docs-site/static/syntax-theme-light.css",
    "content": "/*\n * theme \"OneHalfLight\" generated by syntect\n */\n\n.z-code {\n color: #383a42;\n background-color: #fafafa;\n border: 1px solid #e0e0e0;\n overflow: auto;\n}\n\n.z-comment {\n color: #a0a1a7;\n}\n.z-variable.z-parameter.z-function {\n color: #383a42;\n}\n.z-keyword {\n color: #a626a4;\n}\n.z-variable {\n color: #e45649;\n}\n.z-entity.z-name.z-function, .z-meta.z-require, .z-support.z-function.z-any-method {\n color: #0184bc;\n}\n.z-support.z-class, .z-entity.z-name.z-class, .z-entity.z-name.z-type.z-class {\n color: #c18401;\n}\n.z-meta.z-class {\n color: #c18401;\n}\n.z-keyword.z-other.z-special-method {\n color: #0184bc;\n}\n.z-storage {\n color: #a626a4;\n}\n.z-support.z-function {\n color: #0184bc;\n}\n.z-string {\n color: #50a14f;\n}\n.z-constant.z-numeric {\n color: #c18401;\n}\n.z-none {\n color: #c18401;\n}\n.z-none {\n color: #c18401;\n}\n.z-constant {\n color: #c18401;\n}\n.z-entity.z-name.z-tag {\n color: #e45649;\n}\n.z-entity.z-other.z-attribute-name {\n color: #c18401;\n}\n.z-entity.z-other.z-attribute-name.z-id, .z-punctuation.z-definition.z-entity {\n color: #c18401;\n}\n.z-meta.z-selector {\n color: #a626a4;\n}\n.z-markup.z-heading .z-punctuation.z-definition.z-heading, .z-entity.z-name.z-section {\n color: #0184bc;\n}\n.z-markup.z-bold, .z-punctuation.z-definition.z-bold {\n color: #a626a4;\n}\n.z-markup.z-italic, .z-punctuation.z-definition.z-italic {\n color: #a626a4;\n}\n.z-markup.z-raw.z-inline {\n color: #50a14f;\n}\n.z-meta.z-link {\n color: #50a14f;\n}\n.z-markup.z-quote {\n color: #50a14f;\n}\n.z-source.z-java .z-meta.z-class.z-java .z-meta.z-method.z-java {\n color: #383a42;\n}\n.z-source.z-java .z-meta.z-class.z-java .z-meta.z-class.z-body.z-java {\n color: #383a42;\n}\n.z-source.z-js .z-meta.z-function.z-js .z-variable.z-parameter.z-function.z-js {\n color: #e45649;\n}\n.z-source.z-js .z-variable.z-other.z-readwrite.z-js {\n color: #e45649;\n}\n.z-source.z-js .z-variable.z-other.z-object.z-js {\n color: #383a42;\n}\n.z-source.z-js .z-meta.z-function-call.z-method.z-js .z-variable.z-other.z-readwrite.z-js {\n color: #e45649;\n}\n.z-source.z-js .z-meta.z-block.z-js .z-variable.z-other.z-readwrite.z-js {\n color: #e45649;\n}\n.z-source.z-js .z-meta.z-block.z-js .z-variable.z-other.z-object.z-js {\n color: #383a42;\n}\n.z-source.z-js .z-meta.z-block.z-js .z-meta.z-function-call.z-method.z-js .z-variable.z-other.z-readwrite.z-js {\n color: #383a42;\n}\n.z-source.z-js .z-meta.z-function-call.z-method.z-js .z-variable.z-function.z-js {\n color: #383a42;\n}\n.z-source.z-js .z-meta.z-property.z-object.z-js .z-entity.z-name.z-function.z-js {\n color: #0184bc;\n}\n.z-source.z-js .z-support.z-constant.z-prototype.z-js {\n color: #383a42;\n}\n.z-markup.z-inserted {\n color: #98c379;\n}\n.z-markup.z-deleted {\n color: #e06c75;\n}\n.z-markup.z-changed {\n color: #e5c07b;\n}\n.z-string.z-regexp {\n color: #50a14f;\n}\n.z-constant.z-character.z-escape {\n color: #0997b3;\n}\n.z-invalid.z-illegal {\n color: #fafafa;\n background-color: #e06c75;\n}\n.z-invalid.z-broken {\n color: #fafafa;\n background-color: #e5c07b;\n}\n.z-invalid.z-deprecated {\n color: #fafafa;\n background-color: #e5c07b;\n}\n.z-invalid.z-unimplemented {\n color: #fafafa;\n background-color: #c678dd;\n}\n"
  },
  {
    "path": "docs-site/styles/styles.css",
    "content": "@import \"tailwindcss/base\";\n@import \"tailwindcss/components\";\n@import \"tailwindcss/utilities\";\n@import \"tailwindcss/typography\";\n\n@layer base {\n    :root {\n        --background: #ffffff;\n        --background-secondary: #5F456B;\n\n        /* 1f2937 gray-800*/\n        --foreground: #1f2937; /* gray-900 */\n        --primary: #D45546;\n        --redrust:  #c12110;\n        --orangerust:  #cc4111;\n\n        --border: #3A3A54;\n\n        --card: #1D1D2A;\n        --card-foreground: #FDCEF1;\n\n        --radius: 0.5rem;\n    }\n    \n    .dark {\n        --background: black;\n        --foreground: hsl(213, 31%, 91%);\n\n        --muted: hsl(223, 47%, 11%);\n        --muted-foreground: hsl(215.4, 16.3%, 56.9%);\n\n        --accent: hsl(216, 34%, 17%);\n        --accent-foreground: hsl(210, 40%, 98%);\n\n        --popover: hsl(224, 71%, 4%);\n        --popover-foreground: hsl(215, 20.2%, 65.1%);\n\n        --border: hsl(216, 34%, 17%);\n        --input: hsl(216, 34%, 17%);\n\n        --card: hsl(224, 71%, 4%);\n        --card-foreground: hsl(213, 31%, 91%);\n\n        --primary: hsl(210, 40%, 98%);\n        --primary-foreground: hsl(222.2, 47.4%, 1.2%);\n\n        --secondary: hsl(222.2, 47.4%, 11.2%);\n        --secondary-foreground: hsl(210, 40%, 98%);\n\n        --destructive: hsl(0, 63%, 31%);\n        --destructive-foreground: hsl(210, 40%, 98%);\n\n        --ring: hsl(216, 34%, 17%);\n\n        --radius: 0.5rem;\n    }\n}\n   \n@layer base { \n    * {\n        @apply border-border; \n    }\n\n    body {\n        @apply bg-background text-foreground font-text;\n        font-feature-settings: \"rlig\" 1, \"calt\" 1; \n    }\n\n    h1, h2, h3, h4, h5 {\n        letter-spacing: -0.03em;\n        scroll-margin-top: 56px;\n    }\n    a.active {\n        @apply text-primary;\n    }\n}\n \n.infobox {\n    @apply relative my-8 w-full rounded-lg border px-4 py-3 text-sm border-green-500/50 text-green-500 bg-green-50 dark:bg-green-950 dark:border-green-500  pl-10;\n    \n    &:before{\n        content: '';\n        width: 15px;\n        height: 15px;\n        background-color: currentColor;\n        mask: url(\"data:image/svg+xml,%3Csvg width='15' height='15' viewBox='0 0 15 15' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M7.49991 0.876892C3.84222 0.876892 0.877075 3.84204 0.877075 7.49972C0.877075 11.1574 3.84222 14.1226 7.49991 14.1226C11.1576 14.1226 14.1227 11.1574 14.1227 7.49972C14.1227 3.84204 11.1576 0.876892 7.49991 0.876892ZM1.82707 7.49972C1.82707 4.36671 4.36689 1.82689 7.49991 1.82689C10.6329 1.82689 13.1727 4.36671 13.1727 7.49972C13.1727 10.6327 10.6329 13.1726 7.49991 13.1726C4.36689 13.1726 1.82707 10.6327 1.82707 7.49972ZM8.24992 4.49999C8.24992 4.9142 7.91413 5.24999 7.49992 5.24999C7.08571 5.24999 6.74992 4.9142 6.74992 4.49999C6.74992 4.08577 7.08571 3.74999 7.49992 3.74999C7.91413 3.74999 8.24992 4.08577 8.24992 4.49999ZM6.00003 5.99999H6.50003H7.50003C7.77618 5.99999 8.00003 6.22384 8.00003 6.49999V9.99999H8.50003H9.00003V11H8.50003H7.50003H6.50003H6.00003V9.99999H6.50003H7.00003V6.99999H6.50003H6.00003V5.99999Z' fill='currentColor' fill-rule='evenodd' clip-rule='evenodd'%3E%3C/path%3E%3C/svg%3E\");\n        @apply translate-y-[-3px] absolute left-4 top-4;\n    }\n}\n\n.section-colored {\n    h1, h2 {\n        @apply text-background;\n    }\n}\n.dark {\n    .section-colored {\n    \n            h1,\n            h2 {\n                @apply text-white;\n            }\n        }\n}\n\n.section {\n    @apply px-2 sm:px-0;\n    h1 {\n        @apply font-semibold;\n        @apply sm:text-6xl;\n        @apply mb-8;\n        letter-spacing: -0.05em;\n    }\n    h2 {\n        @apply text-xl sm:text-3xl font-light;\n        @apply mt-0;\n    }\n    pre {\n        @apply my-0;\n    }\n}\n.footer-links {\n    a {\n        @apply no-underline text-white mx-2;\n    }\n}\n.fatbtn {\n    @apply no-underline text-lg font-semibold text-foreground bg-background rounded-sm px-8 py-3; \n}\n.video-container {\n    position: relative;\n    padding-bottom: 56.25%; /* 16:9 */\n    height: 0;\n}\n.video-container iframe {\nposition: absolute;\ntop: 0;\nleft: 0;\nwidth: 100%;\nheight: 100%;\n}\n.container {\n    width: 100%;\n    margin-right: auto;\n    margin-left: auto;\n    @apply px-4 sm:px-6 md:px-8 lg:px-12\n}\n.prose {\n    ul {\n        padding-inline-start: 1em;\n    }\n}\n\n\n\n.navbar-form {\n    position: relative;\n  }\n  \n#suggestions {\n    position: absolute;\n    right: 0;\n    margin-top: 0.5rem;\n    width: calc(100vw - 8rem);\n    max-height: 500px;\n    overflow: auto;\n    @apply bg-background;\n}\n\n#suggestions a {\n    display: block;\n    text-decoration: none;\n    padding: 0.75rem;\n    margin: 0 0.5rem;\n}\n\n#suggestions a:focus {\n    @apply bg-gray-100 dark:bg-zinc-700;\n    outline: 0;\n}\n\n#suggestions div:not(:first-child) {\n    @apply border-gray-200;\n}\n\n#suggestions div:first-child {\n    margin-top: 0.5rem;\n}\n\n#suggestions div:last-child {\n    margin-bottom: 0.5rem;\n}\n\n#suggestions a:hover {\n    @apply bg-gray-100 dark:bg-zinc-800;\n}\n\n#suggestions span {\n    @apply text-sm;\n}\n\n#suggestions span:first-child {\n    @apply text-foreground;\n    @apply font-semibold;\n}\n\n#suggestions span:nth-child(2) {\n    @apply text-gray-700 dark:text-gray-400;\n}\n\n@media screen(sm) {\n    #suggestions {\n        width: 30rem;\n    }\n\n    #suggestions a {\n        display: flex;\n    }\n\n    #suggestions span:first-child {\n        width: 9rem;\n        padding-right: 1rem;\n        @apply border-gray-200;\n        display: inline-block;\n        text-align: right;\n    }\n\n    #suggestions span:nth-child(2) {\n        width: 19rem;\n        padding-left: 1rem;\n    }\n}\n\nhtml .toggle-dark {\n    display: block;\n}\n\nhtml .toggle-light {\n    display: none;\n}\n\nhtml.dark .toggle-light {\n    display: block;\n}\n\nhtml.dark .toggle-dark {\n    display: none;\n}\n"
  },
  {
    "path": "docs-site/tailwind.config.js",
    "content": "/** @type {import('tailwindcss').Config} */\n\nconst { fontFamily } = require(\"tailwindcss/defaultTheme\");\n\nmodule.exports = {\n  darkMode: [\"class\"],\n  content: [\"./themes/**/*.html\", \"./templates/**/*.html\", \"./content/**/*.md\"],\n  theme: {\n    extend: {\n      typography: ({ theme }) => ({\n        DEFAULT: {\n          // this is for prose class\n          css: {\n            \"--tw-prose-headings\": theme(\"colors.foreground\"),\n            \"--tw-prose-body\": theme(\"colors.foreground\"),\n          },\n        },\n      }),\n      spacing: {},\n      colors: {\n        background: \"var(--background)\",\n        \"background-secondary\": \"var(--background-secondary)\",\n        foreground: \"var(--foreground)\",\n        redrust: \"var(--redrust)\",\n        orangerust: \"var(--orangerust)\",\n        primary: {\n          DEFAULT: \"var(--primary)\",\n          foreground: \"var(--primary-foreground)\",\n        },\n        secondary: {\n          DEFAULT: \"var(--secondary)\",\n          foreground: \"var(--secondary-foreground)\",\n        },\n        border: \"var(--border)\",\n        card: {\n          DEFAULT: \"var(--card)\",\n          foreground: \"var(--card-foreground)\",\n        },\n      },\n      borderRadius: {\n        lg: `var(--radius)`,\n        md: `calc(var(--radius) - 2px)`,\n        sm: \"calc(var(--radius) - 4px)\",\n      },\n      fontFamily: {\n        inter: [\"Inter\"],\n        text: [\"Inter\"],\n        heading: [\"Arimo\"],\n      },\n    },\n  },\n  variants: {\n    extend: {\n      inset: [\"negative\"],\n    },\n  },\n  plugins: [require(\"@tailwindcss/typography\")],\n};\n"
  },
  {
    "path": "docs-site/templates/404.html",
    "content": "{% extends \"base.html\" %}\n\n{% block seo %}\n  {{ super() }} \n  {% set title = \"404 Page not found\" %}\n  \n  {% if config.title %}\n    {% set title_addition = title_separator ~ config.title %}\n  {% else %}\n    {% set title_addition = \"\" %}\n  {% endif %}\n  \n  {% set description = config.description %}\n  \n  {{ macros_head::seo(title=title, title_addition=title_addition, description=description, is_404=true) }}\n{% endblock seo %}\n\n{% block content %}\n<div class=\"wrap container\" role=\"document\">\n  <div class=\"content\">\n    <section class=\"section container-fluid mt-n3 pb-3\">\n      <div class=\"row justify-content-center\">\n        <div class=\"row justify-content-center\">\n\t\t\t\t\t<div class=\"col-md-12 col-lg-10 col-xxl-8\">\n\t\t\t\t\t\t<article>\n\t\t\t\t\t\t\t<h1 class=\"text-center\">Page not found :(</h1>\n\t\t\t\t\t\t\t<p class=\"text-center\">The page you are looking for doesn't exist or has been moved.</p>\n\t\t\t\t\t\t</article>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n      </div>\n    </section>\n  </div>\n</div>\n{% endblock content %}\n"
  },
  {
    "path": "docs-site/templates/base.html",
    "content": "{% import 'macros/footer.html' as macros_footer -%}\n{% import 'macros/header.html' as macros_header -%}\n{% import 'macros/javascript.html' as macros_js -%}\n{% import 'macros/docs-sidebar.html' as macros_sidebar -%}\n{% import 'macros/docs-edit-page.html' as macros_edit_page -%}\n{% import 'macros/docs-navigation.html' as macros_navigation -%}\n{% import 'macros/docs-toc.html' as macros_toc -%}\n{% import 'macros/page-publish-metadata.html' as macros_publish -%}\n{% import 'macros/youtube.html' as youtube -%}\n\n\n<!DOCTYPE html>\n<html lang=\"{{ config.extra.language_code | default(value=\" en-US\") }}\" >\n<head>\n     {% if page.extra.meta %}\n         <!-- the meta data config goes here  -->\n         {% for data in page.extra.meta %}\n             <meta\n                 {% for key, value in data%}\n                     {% if key == \"property\" and value == \"og:title\"%}\n                         {% set_global page_has_og_title = true -%}\n                     {% endif %}\n                     {% if key == \"property\" and value == \"og:description\"%}\n                         {% set_global page_has_og_description = true -%}\n                     {% endif %}\n                     {% if key == \"name\" and value == \"description\"%}\n                         {% set_global page_has_description = true -%}\n                     {% endif %}\n                     {{ key }}=\"{{ value }}\"\n                {% endfor %}\n            />\n        {% endfor %}\n    {% endif %}\n\n    {# Site title #}\n    {% set current_path = current_path | default(value=\"/\") %}\n    {% if current_path == \"/\" %}\n      <title>\n          {{ config.title | default(value=\"Home\") }}\n      </title>\n\n    {% else %}\n      <title>\n          {% if page.title %} {{ page.title ~ \" - Loco.rs\" }}\n          {% elif section.title %} {{ section.title ~ \" - Loco.rs\" }}\n          {% elif config.title %} {{ config.title }}\n          {% else %} Post  ~ \" - Loco.rs\" {% endif %}\n      </title>\n\n      {% if not page_has_og_title %}\n          <meta property=\"og:title\" content=\"{% if page.title -%}{{ page.title }}{% elif config.title -%}{{ config.title }}{% else -%}Post{% endif %} - Loco.rs\" />\n          <meta property=\"og:title\" content=\"{% if page.title -%}{{ page.title }}{% elif config.title -%}{{ config.title }}{% else -%}Post{% endif %} - Loco.rs\">  \n          <meta name=\"twitter:title\" content=\"{% if page.title -%}{{ page.title }}{% elif config.title -%}{{ config.title }}{% else -%}Post{% endif %} - Loco.rs\"> \n      {% endif %}\n    {% endif %}\n\n\n     {% if not page_has_og_description %}\n         {% if page.description %}\n             <meta property=\"og:description\" content=\"{{ page.description }}\" />\n         {% elif config.description %}\n             <meta property=\"og:description\" content=\"{{ config.description }}\" />\n         {% endif %}\n     {% endif %}\n\n     {% if not page_has_description %}\n         {% if page.description %}\n             <meta name=\"description\" content=\"{{ page.description }}\" />\n         {% elif config.description %}\n             <meta name=\"description\" content=\"{{ config.description }}\" />\n         {% endif %}\n    {% endif %}\n\n\n  <meta charset=\"utf-8\">\n  <meta http-equiv=\"x-ua-compatible\" content=\"ie=edge\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n\n\n  <meta property=\"og:image\" content=\"https://loco.rs/apple-touch-icon@x4.png\">\n\n  <meta name=\"twitter:card\" content=\"summary_large_image\"> \n  <meta name=\"twitter:description\" content=\"Loco.rs is like Ruby on Rails for Rust. Use it to quickly build and deploy Rust based apps from zero to production.\">\n  <meta name=\"twitter:image\" content=\"https://loco.rs/apple-touch-icon@x4.png\">\n  \n  \n\n  <link rel=\"stylesheet\" type=\"text/css\" href={{ get_url(path=\"styles/styles.css\" ) }} />\n  <link rel=\"stylesheet\" type=\"text/css\" href={{ get_url(path=\"syntax-theme-light.css\" ) }} />\n  <link rel=\"stylesheet\" type=\"text/css\" href={{ get_url(path=\"syntax-theme-dark.css\" ) }} />\n\n  <link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700;800\" rel=\"stylesheet\">\n\n  <link rel=\"apple-touch-icon\" sizes=\"180x180\" href={{ get_url(path=\"apple-touch-icon.png\" ) }}>\n  <link rel=\"icon\" type=\"image/png\" href={{ get_url(path=\"favicon-32x32.png\" ) }}>\n\n</head>\n\n<body class=\"bg-background\">\n  <div class=\"dark\"></div>\n  <div class=\"relative flex min-h-screen flex-col \">\n    {% block header %}\n    {{ macros_header::header(current_section=\"/\") }}\n    {% endblock header %}\n\n    {% block content %}{% endblock content %}\n  </div>\n\n  {{macros_footer::footer()}}\n  {{ macros_js::javascript() }}\n\n</body>\n\n</html>\n"
  },
  {
    "path": "docs-site/templates/blog/atom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<feed xmlns=\"http://www.w3.org/2005/Atom\" xml:lang=\"{{ lang }}\">\n    <title>{{ config.title }}\n    {%- if term %} - {{ term.name }}\n    {%- elif section.title %} - {{ section.title }}\n    {%- endif -%}\n    </title>\n    {%- if config.description %}\n    <subtitle>{{ config.description }}</subtitle>\n    {%- endif %}\n    <link rel=\"self\" type=\"application/atom+xml\" href=\"{{ feed_url | safe }}\"/>\n    <link rel=\"alternate\" type=\"text/html\" href=\"\n      {%- if section -%}\n        {{ section.permalink | escape_xml | safe }}\n      {%- else -%}\n        {{ config.base_url | escape_xml | safe }}\n      {%- endif -%}\n    \"/>\n    <generator uri=\"https://www.getzola.org/\">Zola</generator>\n    <updated>{{ last_updated | date(format=\"%+\") }}</updated>\n    <id>{{ feed_url | safe }}</id>\n    {%- for page in pages %}\n    {% if page.permalink is starting_with(\"https://loco.rs/blog/\") or page.permalink is starting_with(\"http://127.0.0.1:1111/blog/\") %}\n    <entry xml:lang=\"{{ page.lang }}\">\n        <title>{{ page.title }}</title>\n        <published>{{ page.date | date(format=\"%+\") }}</published>\n        <updated>{{ page.updated | default(value=page.date) | date(format=\"%+\") }}</updated>\n        {% for author in page.authors %}\n        <author>\n          <name>\n            {{ author }}\n          </name>\n        </author>\n        {% else %}\n        <author>\n          <name>\n            {%- if config.author -%}\n              {{ config.author }}\n            {%- else -%}\n              Unknown\n            {%- endif -%}\n          </name>\n        </author>\n        {% endfor %}\n        <link rel=\"alternate\" type=\"text/html\" href=\"{{ page.permalink | safe }}\"/>\n        <id>{{ page.permalink | safe }}</id>\n        {% if page.summary %}\n        <summary type=\"html\">{{ page.summary }}</summary>\n        {% else %}\n        <content type=\"html\" xml:base=\"{{ page.permalink | escape_xml | safe }}\">{{ page.content }}</content>\n        {% endif %}\n    </entry>\n    {%- endif -%}\n    {%- endfor %}\n</feed>"
  },
  {
    "path": "docs-site/templates/blog/page.html",
    "content": "{# Default page template used for blog contents #}\n\n{% extends \"page.html\" %}\n\n{% block body %}\n{% set page_class = \"blog single\" %}\n{% endblock body %}\n\n{% block header %}\n{{ macros_header::header(current_section='blog')}}\n{% endblock header %}\n\n{% block content %}\n<div class=\"container\" role=\"document\">\n  <article class=\"prose mx-auto my-[2rem] md:my-[8rem]\">\n    {{ macros_publish::page_publish_metadata(page=page) }}\n    <h1 class=\"mb-1 font-extrabold\">{{ page.title }}</h1>\n    <div class=\"text-gray-500 font-semibold\">\n      {% if page.taxonomies.authors and\n      config.taxonomies %} {% for author in page.taxonomies.authors %}{% if author_flag %} and {% endif %}{{ author }}{% set_global author_flag\n      = true %}{% endfor %}{% endif %}\n    </div>\n    {% if page.extra.lead %}<p class=\"lead\">{{ page.extra.lead | safe }}</p>{% endif %}\n    <div class=\"markdown mt-8\">\n      {{ page.content | safe }}\n    </div>\n\n  </article>\n</div>\n{% endblock content %}\n"
  },
  {
    "path": "docs-site/templates/blog/rss.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<rss xmlns:atom=\"http://www.w3.org/2005/Atom\" version=\"2.0\">\n    <channel>\n      <title>{{ config.title }}\n        {%- if term %} - {{ term.name }}\n        {%- elif section.title %} - {{ section.title }}\n        {%- endif -%}\n      </title>\n      <link>\n        {%- if section -%}\n          {{ section.permalink | escape_xml | safe }}\n        {%- else -%}\n          {{ config.base_url | escape_xml | safe }}\n        {%- endif -%}\n      </link>\n      <description>{{ config.description }}</description>\n      <generator>Zola</generator>\n      <language>{{ lang }}</language>\n      <atom:link href=\"{{ feed_url | safe }}\" rel=\"self\" type=\"application/rss+xml\"/>\n      <lastBuildDate>{{ last_updated | date(format=\"%a, %d %b %Y %H:%M:%S %z\") }}</lastBuildDate>\n      {%- for page in pages %}\n      {% if page.permalink is starting_with(\"https://loco.rs/blog/\") or page.permalink is starting_with(\"http://127.0.0.1:1111/blog/\") %}\n      <item>\n          <title>{{ page.title }}</title>\n          <pubDate>{{ page.date | date(format=\"%a, %d %b %Y %H:%M:%S %z\") }}</pubDate>\n          <author>\n            {%- if page.authors -%}\n              {{ page.authors[0] }}\n            {%- elif config.author -%}\n              {{ config.author }}\n            {%- else -%}\n              Unknown\n            {%- endif -%}\n          </author>\n          <link>{{ page.permalink | escape_xml | safe }}</link>\n          <guid>{{ page.permalink | escape_xml | safe }}</guid>\n          <description xml:base=\"{{ page.permalink | escape_xml | safe }}\">{% if page.summary %}{{ page.summary }}{% else %}{{ page.content }}{% endif %}</description>\n      </item>\n      {%- endif -%}\n      {%- endfor %}\n    </channel>\n</rss>"
  },
  {
    "path": "docs-site/templates/blog/section.html",
    "content": "{% extends \"section.html\" %}\n\n{% block body %}\n{% set page_class = \"blog list\" %}\n{% endblock body %}\n\n{% block header %}\n{# This value is matched by the config.extra.menu.main->section #}\n{% set current_section = \"blog\" %}\n{{ macros_header::header(current_section=current_section)}}\n{% endblock header %}\n\n{% block content %}\n\n<svg class=\"w-full h-full\" viewBox=\"0 0 1512 235\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n  <path class=\"dark:fill-zinc-700\" d=\"M 1512 0 V 235 H 0 L 1512 0 Z\" fill=\"#7A1307\"/>\n  <path class=\"dark:fill-zinc-900\" d=\"M 1512 29 V 236 H 0 L 1512 29 Z\" fill=\"#c12110\"/>\n</svg>\n\n<div class=\"bg-redrust dark:bg-zinc-900 text-white px-2\">\n      <h1 class=\"text-center text-3xl mb-3 font-semibold text-white\">Latest Updates</h1>\n      <h2 class=\"text-center text-xl text-red-50 mb-12\">Updates and techincal articles from the team and contributors</h2>\n</div>\n\n<div class=\"container\" role=\"document\">\n  <div class=\"content mx-auto max-w-[52rem] my-16\">\n        <div class=\"flex flex-col card-list gap-3\">\n          {% for page in paginator.pages %}\n          <a class=\"stretched-link text-body hover:bg-gray-50 dark:hover:bg-gray-900 rounded-sm p-4\" href=\"{{ page.permalink }}\">\n            <div>\n              <h2 class=\"text-lg font-semibold  -mt-1\">{{ page.title }} </h2>\n              <p class=\"text-gray-500\">{{page.description | safe }}</p>\n            </div>\n            <div class=\"flex mt-2 items-center\">\n              <div class=\"text-sm text-sky-500 font-medium\">\n                Read more\n              </div>\n              <svg class=\"relative mt-px overflow-visible ml-2.5 text-sky-300 dark:text-sky-700\" width=\"3\" height=\"6\" viewBox=\"0 0 3 6\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M0 0L3 3L0 6\"></path></svg>\n            </div>\n          </a>\n          {% endfor %}\n          {% if paginator.previous or paginator.next %}\n          {{ macros_section_nav::navigation(paginator=paginator) }}\n          {% endif %}\n        </div>\n  </div>\n</div>\n{% endblock content %}\n"
  },
  {
    "path": "docs-site/templates/casts/page.html",
    "content": "{# Default page template used for blog contents #}\n\n{% extends \"page.html\" %}\n\n{% block body %}\n{% set page_class = \"casts single\" %}\n{% endblock body %}\n\n{% block header %}\n{{ macros_header::header(current_section='casts')}}\n{% endblock header %}\n\n{% block content %}\n<div class=\"container\" role=\"document\">\n  <article class=\"max-w-[1024px] mx-auto my-[2rem] md:my-[8rem]\">\n    {{ youtube::youtube(id=page.extra.id) }}\n    {% if page.extra.lead %}<p class=\"lead\">{{ page.extra.lead | safe }}</p>{% endif %}\n    <div class=\"prose markdown mt-8 mx-auto\">\n      {{ macros_publish::page_publish_metadata(page=page) }}\n      <h1 class=\"mb-3\">{{ page.title }}</h1>\n      {{ page.content | safe }}\n    </div>\n\n  </article>\n</div>\n\n\n{% endblock content %}\n"
  },
  {
    "path": "docs-site/templates/casts/section.html",
    "content": "{% extends \"section.html\" %}\n\n{% block body %}\n{% set page_class = \"casts list\" %}\n{% endblock body %}\n\n{% block header %}\n{# This value is matched by the config.extra.menu.main->section #}\n{% set current_section = \"casts\" %}\n{{ macros_header::header(current_section=current_section)}}\n{% endblock header %}\n\n{% block content %}\n\n<svg class=\"w-full h-full\" viewBox=\"0 0 1512 235\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n  <path class=\"dark:fill-zinc-700\" d=\"M 1512 0 V 235 H 0 L 1512 0 Z\" fill=\"#7A1307\"/>\n  <path class=\"dark:fill-zinc-900\" d=\"M 1512 29 V 236 H 0 L 1512 29 Z\" fill=\"#c12110\"/>\n</svg>\n\n<div class=\"bg-redrust dark:bg-zinc-900 text-white px-2\">\n  <h1 class=\"text-center text-3xl mb-3 font-semibold text-white\">Loco Casts</h1>\n  <h2 class=\"text-center text-xl text-red-50 mb-12\">Short and focused tutorials for learning various Loco features</h2>\n</div>\n\n<div class=\"container\" role=\"document\">\n<div class=\"content mx-auto max-w-[52rem] my-16\">\n    <div class=\"flex flex-col card-list gap-10\">\n      {% for page in paginator.pages %}\n      <a class=\"flex flex-col sm:flex-row items-center gap-6 stretched-link text-body hover:bg-gray-50 dark:hover:bg-gray-900 rounded-sm p-4\" href=\"{{ page.permalink }}\">\n        <div class=\"dark:border-gray-700 dark:rounded-sm dark:border\">\n        {{ youtube::thumb(id=page.extra.id) }}\n        </div>\n        <div>\n          <div>\n            <h2 class=\"text-lg font-semibold  -mt-1\">{{ page.title }} </h2>\n            <p class=\"text-gray-500\">{{page.description | safe }}</p>\n          </div>\n          <div class=\"flex mt-2 items-center\">\n            <div class=\"text-sm text-sky-500 font-medium\">\n              Watch\n            </div>\n            <svg class=\"relative mt-px overflow-visible ml-2.5 text-sky-300 dark:text-sky-700\" width=\"3\" height=\"6\" viewBox=\"0 0 3 6\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M0 0L3 3L0 6\"></path></svg>\n          </div>\n        </div>\n      </a>\n      {% endfor %}\n      {% if paginator.previous or paginator.next %}\n      {{ macros_section_nav::navigation(paginator=paginator) }}\n      {% endif %}\n    </div>\n</div>\n</div>\n\n{% endblock content %}\n"
  },
  {
    "path": "docs-site/templates/docs/page.html",
    "content": "{% extends \"page.html\" %}\n\n{% block body %}\n{% set page_class = \"docs single\" %}\n{% endblock body %}\n\n{% block header %}\n{# This value is matched by the config.extra.menu.main~url #}\n{% set current_section = \"docs\" %}\n{{ macros_header::header(current_section=current_section)}}\n{% endblock header %}\n\n{% block content %}\n<div role=\"document\">\n  <div class=\"bg-redrust text-background dark:bg-zinc-900\">\n    <div class=\"container xl:max-w-[80rem]\">\n      <aside class=\"w-full\">\n        {{ macros_sidebar::docs_sidebar(current_section=current_section) }}\n      </aside>\n    </div>\n  </div>\n  <div\n    class=\"container flex-1 items-start\">\n    <main class=\"py-6 xl:grid xl:grid-cols-[1fr_450px] xl:max-w-[80rem] xl:mx-auto\">\n      <div class=\"mx-auto w-full min-w-0\">\n        <div class=\"markdown prose dark:prose-invert max-w-[60rem] md:pr-12 sm:pr-0\">\n          <h1 class=\"mb-10 mt-[9px]\">{{ page.title | safe }}</h1>\n          {% if page.extra.lead %}<p class=\"lead\">{{ page.extra.lead | safe }}</p>{% endif %}\n\n          {{ page.content | safe }}\n\n          {% if config.extra.edit_page %}\n          {{ macros_edit_page::docs_edit_page(current_path=current_path) }}\n          {% endif %}\n          {{ macros_navigation::docs_navigation(page=page, current_section=current_section) }}\n        </div>\n      </div>\n\n      <div class=\"hidden text-sm xl:block\">\n        <div class=\"bg-gray-50 p-2 mt-[90px] border border-gray-200 rounded-sm dark:bg-zinc-900\">\n        {{ macros_toc::docs_toc(page=page) }}\n        </div>\n      </div>\n\n    </main>\n\n  </div>\n\n\n\n</div>\n\n\n</div>\n{% endblock content %}\n"
  },
  {
    "path": "docs-site/templates/docs/section.html",
    "content": "{% extends \"section.html\" %}\n\n{% block body %}\n{% set page_class = \"docs list\" %}\n{% endblock body %}\n\n{% block header %}\n{# This value is matched by the config.extra.menu.main~section #}\n{% set current_section = \"docs\" %}\n{{ macros_header::header(current_section=current_section)}}\n{% endblock header %}\n\n{% block content %}\n<div class=\"wrap container\" role=\"document\">\n  <div class=\"content\">\n    <div class=\"row justify-content-center\">\n      <div class=\"col-md-12 col-lg-10 col-xxl-8\">\n        <article>\n          <h1 class=\"text-center\">{{ section.title }}</h1>\n          <div class=\"text-center\">{{ section.content | safe }}</div>\n          <div class=\"card-list\">\n            {% set index_path = current_path ~ \"_index.md\" | trim_start_matches(pat=\"/\") %}\n            {% set index = get_section(path=index_path) %}\n            {% for page in index.pages %}\n            <div class=\"card my-3\">\n              <div class=\"card-body\">\n                <a class=\"stretched-link\" href=\"{{ page.permalink }}\">{{ page.title }} &rarr;</a>\n              </div>\n            </div>\n            {% endfor %}\n            {% for s in index.subsections %}\n            {% set subsection = get_section(path=s) %}\n            {% if subsection.pages %}\n            {% for page in subsection.pages %}\n            <div class=\"card my-3\">\n              <div class=\"card-body\">\n                <a class=\"stretched-link\" href=\"{{ page.permalink }}\">{{ page.title }} &rarr;</a>\n              </div>\n            </div>\n            {% endfor %}\n            {% endif %}\n            {% endfor %}\n          </div>\n        </article>\n      </div>\n    </div>\n  </div>\n</div>\n{% endblock content %}"
  },
  {
    "path": "docs-site/templates/index.html",
    "content": "{% import 'macros/footer.html' as macros_footer -%}\n\n{% extends \"base.html\" %}\n\n{% block seo %}\n{{ super() }}\n\n{% if config.title %}\n{% set title = config.title %}\n{% else %}\n{% set title = \"\" %}\n{% endif %}\n\n{% if config.extra.title_addition and title %}\n{% set title_addition = title_separator ~ config.extra.title_addition %}\n{% elif config.extra.title_addition %}\n{% set title_addition = config.extra.title_addition %}\n{% else %}\n{% set title_addition = \"\" %}\n{% endif %}\n\n{% set description = config.description %}\n\n{{ macros_head::seo(title=title, title_addition=title_addition, description=description, is_home=true) }}\n{% endblock seo %}\n\n{% block content %}\n<div class=\"prose dark:prose-invert max-w-[100%]\">\n\n<div class=\"relative bg-left-bottom mt-12 md:mt-0\">\n    <img class=\"m-0 mt-[-60px] w-full\" src=\"header.svg\"/>\n</div>\n\n\n<div class=\"pb-20 section section-colored bg-redrust\">\n    <div class=\"mb-10 pt-8 text-center section-colored\">\n        <h1>\n            It’s Like Ruby on Rails, but for Rust.\n        </h1>\n        <h2>\n            Get the same great building experience of Rails, with the incredible<br /> performance and safety of Rust.\n        </h2>\n    </div>\n    <div class=\"flex items-center justify-center \">\n        <div class=\"relative w-full max-w-[50rem] max-h-[600px] mx-auto px-1 sm:px-0\">\n            <div class=\"relative\" style=\"padding-top: 56.25%\">\n                <iframe class=\"absolute inset-0 w-full h-full rounded-md shadow-lg\"\n                    src=\"https://www.youtube.com/embed/EircfwF8c0E?si=jv_PZuWZIJ59Qz3O&rel=0\" title=\"Loco.rs\" frameborder=\"0\"\n                    allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share\"\n                    referrerpolicy=\"strict-origin-when-cross-origin\" allowfullscreen></iframe>\n            </div>\n        </div>\n    </div>\n\n    <div class=\"flex items-center justify-center py-6 z-10 relative\">\n        <a class=\"fatbtn dark:bg-foreground dark:text-background\" href=\"/docs/getting-started/tour/\">Get started &rarr;</a>\n    </div>\n</div>\n\n\n<div class=\"pt-20 relative section section-colored bg-orangerust\">\n    <div class=\"mb-10 text-center\">\n        <h1>\n            It’s time to make Rust your super-power.\n        </h1>\n        <h2>\n            Using Rust with Loco is super easy. With a simple request lifecycle,<br /> code generators, productivity\n            toolkits and more.\n        </h2>\n    </div>\n\n    <div class=\"container\"> \n        <div class=\"flex items-center justify-center relative z-1 \">\n            <div\n                class=\"rounded-lg bg-card text-card-foreground shadow-lg mb-5 w-full max-w-[50rem] z-10 \">\n\n                <div class=\"p-0 sm:p-6\">\n<pre class=\"text-lg bg-inherit text-inherit text-[11px] sm:text-[16px]\"><div class=\"text-red-100 font-semibold py-0\">$ cargo loco generate scaffold post title:string content:text</div>added: \"src/controllers/post.rs\"\ninjected: \"src/controllers/mod.rs\"\ninjected: \"src/app.rs\"\n...<div class=\"text-red-100 font-semibold py-0 \">$ cargo loco start</div>\n<div style=\"line-height: 16px;\" class=\"text-[9px] sm:text-[16px]\">\n                      ▄     ▀\n                                 ▀  ▄\n                  ▄       ▀     ▄  ▄ ▄▀\n                                    ▄ ▀▄▄\n                        ▄     ▀    ▀  ▀▄▀█▄\n                                          ▀█▄\n▄▄▄▄▄▄▄  ▄▄▄▄▄▄▄▄▄   ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄ ▀▀█\n ██████  █████   ███ █████   ███ █████   ███ ▀█\n ██████  █████   ███ █████   ▀▀▀ █████   ███ ▄█▄\n ██████  █████   ███ █████       █████   ███ ████▄\n ██████  █████   ███ █████   ▄▄▄ █████   ███ █████\n ██████  █████   ███  ████   ███ █████   ███ ████▀\n   ▀▀▀██▄ ▀▀▀▀▀▀▀▀▀▀  ▀▀▀▀▀▀▀▀▀▀  ▀▀▀▀▀▀▀▀▀▀ ██▀\n       ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀\n                https://loco.rs\n</div>\nenvironment: <span class=\"text-red-400\">development</span>\n   database: <span class=\"text-yellow-400\">automigrate</span>\n     logger: <span class=\"text-red-400\">disabled</span>\ncompilation: <span class=\"text-yellow-400\">debug</span>\n      modes: <span class=\"text-green-400\">server</span>\n\n<div class=\"text-yellow-500\">listening on localhost:5150</div></pre>\n                </div>\n            </div>\n        </div>\n\n\n    </div>\n    <div class=\"py-6\"></div>\n\n    <img src=\"/cloud.svg\" class=\"absolute right-0 bottom-0 m-0\" />\n</div>\n\n\n\n<div class=\"pt-20 pb-20 section relative section-colored bg-redrust \">\n    <div class=\"mb-10 mt-15 z-10 relative text-center\">\n        <h1>\n            Enjoy that sweet & effortless Rust performance\n        </h1>\n        <h2>\n            Loco packs a lot of features and still gives you 10x more<br /> performance compared to Node.js\n        </h2>\n    </div>\n\n    <div class=\"container\">\n        <div class=\"flex flex-col items-center rounded-lg shadow-lg bg-red-900 w-full sm:w-7/12 mx-auto\">\n            <img src=\"bench-db-q.svg\"/>\n            <img src=\"bench-no-db.svg\"/>\n        </div>\n    </div>\n\n    <div class=\"flex items-center justify-center py-6 z-10 relative\">\n        <a class=\"fatbtn dark:bg-foreground dark:text-background\" href=\"/docs/getting-started/tour/\">Get started &rarr;</a>\n    </div>\n    <img src=\"/mountain-bg.svg\" class=\"absolute left-0 top-[-490px]\" />\n</div>\n\n<div class=\"section bg-gray-100 dark:bg-background\">\n    <div class=\"container\">\n        <div class=\"pt-20 text-center\">\n            <h1>\n                Build apps locally and save lots of time.\n            </h1>\n            <h2>\n                No need for SaaS or cloud services. Save time, money, and effort with<br /> auth, workers, emails & more\n                out of the box.\n            </h2>\n        </div>\n\n        <div\n            class=\" items-start justify-center gap-6 rounded-lg p-8 md:grid xl:grid-cols-2  relative z-4 \">\n\n            {% for val in config.extra.homepage.features %}\n            <div class=\"col-span-2 grid items-start xl:col-span-1 text-center\">\n                    <div class=\"overflow-auto text-sm prose min-w-[100%] text-left\">\n                        {{ val.example | markdown(inline=true) | safe }}\n                    </div>\n\n                    <h3 class=\"my-5 mb-3 text-2xl\">\n                        {{ val.name }}\n                    </h3>\n                    <p class=\"mb-20 text-xl\">\n                        {{val.description | safe}}\n                    </p>\n            </div>\n            {% endfor %}\n\n        </div>\n\n        <div class=\"flex items-center justify-center py-6 z-10 relative pb-20\">\n            <a class=\"fatbtn dark:bg-zinc-700\" href=\"/docs/getting-started/guide/\">Read the Guide &rarr;</a>\n        </div>\n    </div>\n</div>\n\n</div>\n{% endblock content %}\n\n"
  },
  {
    "path": "docs-site/templates/macros/docs-edit-page.html",
    "content": "{% macro docs_edit_page(current_path) %}\n<p class=\"edit-page\"><a\n        href=\"{{ config.extra.docs_repo }}/blob/{{ config.extra.repo_branch }}/content{{ current_path | replace(from=\"\n        \\\\\", to=\"/\" ) | trim_end_matches(pat=\"/\" ) }}.md\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\"\n            viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"\n            stroke-linejoin=\"round\" class=\"feather feather-edit-2\">\n            <path d=\"M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z\"></path>\n        </svg>Edit this page on GitHub</a></p>\n{% endmacro %}"
  },
  {
    "path": "docs-site/templates/macros/docs-navigation.html",
    "content": "{% macro docs_navigation(page, current_section) %}\n<div class=\"flex items-center justify-between pt-8\">\n  {# Find lighter navigation. There are three types: #}\n  {# 1. directly find the lighter if exists, #}\n  {# 2. find the last page in the previous sibling section, and #}\n  {# 3. find the last page in the parent section. #}\n  {% if page.lower %}\n  <a href=\"{{ page.lower.permalink }}\">\n    <div class=\"card my-1\">\n      <div class=\"card-body py-2\">\n        &larr; {{ page.lower.title }}\n      </div>\n    </div>\n  </a>\n  {% elif not page.extra.top %}\n  {% set index = get_section(path=page.ancestors | reverse | first) %}\n  {% set parent = get_section(path=index.ancestors | reverse | first)%}\n  {% set first_subsection = get_section(path=parent.subsections | first) %}\n  {% if index == first_subsection %}\n  {% if parent.pages %}\n  {% set last_page = parent.pages | last %}\n  <a href=\"{{ last_page.permalink }}\">\n    <div class=\"card my-1\">\n      <div class=\"card-body py-2\">\n        &larr; {{ last_page.title }}\n      </div>\n    </div>\n  </a>\n  {% endif %}\n  {% else %}\n  {% for s in parent.subsections | reverse %}\n  {% set subsection = get_section(path=s) %}\n  {% if subsection.permalink == index.permalink %}\n  {% set_global found_current = true %}\n  {% else %}\n  {% if found_current %}\n  {% if subsection.pages %}\n  {% set last_page = subsection.pages | last %}\n  <a href=\"{{ last_page.permalink }}\">\n    <div class=\"card my-1\">\n      <div class=\"card-body py-2\">\n        &larr; {{ last_page.title }}\n      </div>\n    </div>\n  </a>\n  {% endif %}\n  {# no break #}\n  {% set_global found_current = false %}\n  {% endif %}\n  {% endif %}\n  {% endfor %}\n  {% endif %}\n  {% endif %}\n\n  {# Find heavier navigation. There are also three types: #}\n  {# 1. directly find the heavier if exists, #}\n  {# 2. find the first page in the subsection, and #}\n  {# 3. find the first page in the next sibling section. #}\n  {% if page.higher %}\n  <a class=\"ms-auto\" href=\"{{ page.higher.permalink }}\">\n    <div class=\"card my-1\">\n      <div class=\"card-body py-2\">\n        {{ page.higher.title }} &rarr;\n      </div>\n    </div>\n  </a>\n  {% elif page.extra.top %}\n  {% set index_path = current_section ~ \"/_index.md\" | trim_start_matches(pat=\"/\") %}\n  {% set index = get_section(path=index_path) %}\n  {% set first_subsection = get_section(path=index.subsections | first) %}\n  {% if first_subsection.pages %}\n  {% set first_page = first_subsection.pages | first %}\n  <a class=\"ms-auto\" href=\"{{ first_page.permalink }}\">\n    <div class=\"card my-1\">\n      <div class=\"card-body py-2\">\n        {{ first_page.title }} &rarr;\n      </div>\n    </div>\n  </a>\n  {% endif %}\n  {% else %}\n  {% set index = get_section(path=page.ancestors | reverse | first) %}\n  {% set parent = get_section(path=index.ancestors | reverse | first)%}\n  {% for s in parent.subsections %}\n  {% set subsection = get_section(path=s) %}\n  {% if subsection.permalink == index.permalink %}\n  {% set_global found_current = true %}\n  {% else %}\n  {% if found_current %}\n  {% if subsection.pages %}\n  {% set first_page = subsection.pages | first %}\n  <a class=\"ms-auto\" href=\"{{ first_page.permalink }}\">\n    <div class=\"card my-1\">\n      <div class=\"card-body py-2\">\n        {{ first_page.title }} &rarr;\n      </div>\n    </div>\n  </a>\n  {% endif %}\n  {# no break #}\n  {% set_global found_current = false %}\n  {% endif %}\n  {% endif %}\n  {% endfor %}\n  {% endif %}\n</div>\n{% endmacro %}\n"
  },
  {
    "path": "docs-site/templates/macros/docs-sidebar.html",
    "content": "{% macro docs_sidebar(current_section) %}\n<div>\n\t<nav class=\"flex gap-5 md:gap-10 flex-wrap py-8\" aria-label=\"Main navigation\">\n\t\t{% set index_path = current_section ~ \"/_index.md\" | trim_start_matches(pat=\"/\") %}\n\t\t{% set index = get_section(path=index_path) %}\n\t\t{% if index.pages %}\n\t\t<div>\n\t\t\t<h3>{{ index.title }}</h3>\n\t\t\t<ul>\n\t\t\t\t{% for page in index.pages %}\n\t\t\t\t<li><a class=\"{% if current_url == page.permalink %} active{% endif %} text-background\"\n\t\t\t\t\t\thref=\"{{ page.permalink | safe }}\">{{ page.title }}\n\n\t\t\t\t\t</a></li>\n\t\t\t\t{% endfor %}\n\t\t\t</ul>\n\t\t</div>\n\t\t{% endif %}\n\t\t{% if index.subsections %}\n\t\t{% for s in index.subsections %}\n\t\t{% set subsection = get_section(path=s) %}\n\t\t{% if subsection.pages %}\n\t\t<div>\n\t\t\t<h3 class=\"mb-2 md:mb-4 py-1 text-sm font-semibold text-white border-b border-red-100\">{{ subsection.title }}</h3>\n\t\t\t<ul class=\"mb-4\">\n\t\t\t\t{% for page in subsection.pages %}\n\t\t\t\t<li>\n\t\t\t\t\t<a class=\"inline-block w-full py-1 hover:text-white text-sm {% if current_url == page.permalink %} text-white font-semibold {% else %} text-red-200 dark:text-zinc-400{% endif %}\"\n\t\t\t\t\t\thref=\"{{ page.permalink | safe }}\">{{ page.title }}\n\t\t\t\t\t</a>\n\t\t\t\t</li>\n\t\t\t\t{% endfor %}\n\t\t\t</ul>\n\t\t</div>\n\t\t{% endif %}\n\t\t{% endfor %}\n\t\t{% endif %}\n\t</nav>\n</div>\n{% endmacro %}\n"
  },
  {
    "path": "docs-site/templates/macros/docs-toc.html",
    "content": "{% macro docs_toc(page) %}\n{% if page.extra.toc %}\n<nav>\n\t<ul>\n\t\t{% for h1 in page.toc %}\n\t\t<li class=\"mb-1 px-2 py-1 text-sm font-semibold\"><a href=\"{{ h1.permalink | safe}}\">{{\n\t\t\t\th1.title }}</a></li>\n\t\t{% if h1.children %}\n\t\t<ul>\n\t\t\t{% for h2 in h1.children %}\n\t\t\t<li\n\t\t\t\tclass=\"group flex w-full items-center border border-transparent pl-5 px-2 py-1 hover:underline text-sm  text-foreground\">\n\t\t\t\t<a href=\"{{ h2.permalink | safe }}\">{{ h2.title }}</a>\n\t\t\t</li>\n\t\t\t{% endfor %}\n\t\t</ul>\n\t\t{% endif %}\n\t\t{% endfor %}\n\t</ul>\n</nav>\n{% endif %}\n{% endmacro %}\n"
  },
  {
    "path": "docs-site/templates/macros/footer.html",
    "content": "{% macro footer() %}\n<div class=\"section bg-redrust dark:bg-zinc-800 pt-8\">\n  <div class=\"container flex flex-col items-center text-center text-background py-8 pb-12\">\n      <div class=\"mb-8\">\n      <img src=\"/icon.svg\" width=\"130px\" />\n      </div>\n      <div class=\"footer-links\">\n          <a href=\"https://github.com/loco-rs/loco\">\n              Github\n          </a>\n          <a href=\"https://github.com/loco-rs/loco/blob/master/CODE_OF_CONDUCT.md\">\n              Code of Conduct\n          </a>\n          <a href=\"https://github.com/loco-rs/loco/blob/master/LICENSE\">\n              License\n          </a>\n          <a href=\"https://github.com/loco-rs/loco/blob/master/SECURITY.md\">\n              Security\n          </a>\n      </div>\n  </div>\n</div>\n{% endmacro %}\n"
  },
  {
    "path": "docs-site/templates/macros/header.html",
    "content": "{% macro header(current_section) %}\n<header\n\tclass=\" sticky top-0 z-50 w-full border-border/40 backdrop-blur bg-background\">\n\t<div class=\"container relative flex h-14 md:grid md:grid-cols-[240px_minmax(0,1fr)] xl:max-w-[80rem]\">\n\t\t<div class=\"mr-4 hidden sm:flex\">\n\t\t\t<a class=\"flex mr-4  items-center space-x-2\" href=\"/\">\n\t\t\t\t<img src=\"/icon.svg\" width=\"30px\" />\n\t\t\t</a>\n\t\t\t<nav class=\"flex w-full bg-transparent items-center gap-4 text-sm\">\n\t\t\t\t{% if lang == config.default_language %}\n\t\t\t\t{% set rootsectionpath = \"_index.md\" %}\n\t\t\t\t{% else %}\n\t\t\t\t{% set rootsectionpath = \"_index.\" ~ lang ~ \".md\" %}\n\t\t\t\t{% endif %}\n\t\t\t\t{% set mainsec = get_section(path=rootsectionpath) %}\n\t\t\t\t{% if mainsec.extra.menu.main %}\n\t\t\t\t{% for val in config.extra.menu.main %}\n\t\t\t\t<a class=\"nav-link text-black dark:text-white {% if current_section == val.section %} active{% endif %}\" href=\"{{ get_url(path=val.url, trailing_slash=true) | safe }}\">{{\n\t\t\t\t\tval.name\n\t\t\t\t\t}}\n\t\t\t\t</a>\n\t\t\t\t{% endfor %}\n\t\t\t\t{% elif config.extra.menu.main %}\n\t\t\t\t{% for val in config.extra.menu.main %}\n\t\t\t\t<a class=\"nav-link text-black {% if current_section == val.section %} active{% endif %}\" href=\"{{ get_url(path=val.url, trailing_slash=true) | safe }}\">{{\n\t\t\t\t\tval.name\n\t\t\t\t\t}}\n\t\t\t\t</a>\n\t\t\t\t{% endfor %}\n\t\t\t\t{% else %}\n\t\n\t\t\t\t{% endif %}\n\t\t\t</nav>\n\t\t</div>\n\n\t\t<div class=\"flex items-center sm:hidden\">\n\t\t\t\t<button onclick=\"toggleMenu()\"\n\t\t\t\t\tclass=\"inline-flex items-center justify-center whitespace-nowrap rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 hover:text-accent-foreground h-9 py-2 mr-2 px-0 text-base hover:bg-transparent focus-visible:bg-transparent focus-visible:ring-0 focus-visible:ring-offset-0 md:hidden\"\n\t\t\t\t\ttype=\"button\" aria-haspopup=\"dialog\" aria-expanded=\"false\" aria-controls=\"radix-:R15u6ja:\" data-state=\"closed\">\n\t\t\t\t\t<svg stroke-width=\"1.5\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" class=\"h-5 w-5\">\n\t\t\t\t\t\t<path d=\"M3 5H11\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n\t\t\t\t\t\t</path>\n\t\t\t\t\t\t<path d=\"M3 12H16\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n\t\t\t\t\t\t</path>\n\t\t\t\t\t\t<path d=\"M3 19H21\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n\t\t\t\t\t\t</path>\n\t\t\t\t\t</svg>\n\t\t\t\t\t<span class=\"sr-only\">Toggle Menu</span>\n\t\t\t\t</button>\n\n\t\t\t\t<div class=\"absolute mt-[106px] left-0 right-0\">\n\t\t\t\t\t\t<nav id=\"menu-toggle\" class=\"flex hidden bg-background  items-center gap-4 text-sm py-[15px] px-2 w-full\">\n\t\t\t\t\t\t\t<a class=\"flex items-center\" href=\"/\">\n\t\t\t\t\t\t\t\t<img src=\"/icon.svg\" width=\"30px\" />\n\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t{% if lang == config.default_language %}\n\t\t\t\t\t\t\t{% set rootsectionpath = \"_index.md\" %}\n\t\t\t\t\t\t\t{% else %}\n\t\t\t\t\t\t\t{% set rootsectionpath = \"_index.\" ~ lang ~ \".md\" %}\n\t\t\t\t\t\t\t{% endif %}\n\t\t\t\t\t\t\t{% set mainsec = get_section(path=rootsectionpath) %}\n\t\t\t\t\t\t\t{% if mainsec.extra.menu.main %}\n\t\t\t\t\t\t\t{% for val in config.extra.menu.main %}\n\t\t\t\t\t\t\t<a class=\"nav-link text-black dark:text-white {% if current_section == val.section %} active{% endif %}\" href=\"{{ get_url(path=val.url, trailing_slash=true) | safe }}\">{{\n\t\t\t\t\t\t\t\tval.name\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t{% endfor %}\n\t\t\t\t\t\t\t{% elif config.extra.menu.main %}\n\t\t\t\t\t\t\t{% for val in config.extra.menu.main %}\n\t\t\t\t\t\t\t<a class=\"nav-link text-black {% if current_section == val.section %} active{% endif %}\" href=\"{{ get_url(path=val.url, trailing_slash=true) | safe }}\">{{\n\t\t\t\t\t\t\t\tval.name\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t{% endfor %}\n\t\t\t\t\t\t\t{% else %}\n\t\t\t\t\n\t\t\t\t\t\t\t{% endif %}\n\t\t\t\t\t\t</nav>\n\t\t</div>\n\t\t</div>\n\n\t\t<div class=\"flex flex-1 items-center justify-between space-x-2\">\n\t\t\n\t\t\t{% if config.build_search_index %}\n\t\t\t<div class=\"flex-1 lg:flex lg:justify-end\">\n\t\t\t\t<form class=\"navbar-form flex-grow-1 order-7 order-md-3\">\n\t\t\t\t\t<input id=\"userinput\"\n\t\t\t\t\t\tclass=\"placeholder-black dark:placeholder-white bg-transparent   inline-flex items-center whitespace-nowrap transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 border border-input hover:bg-accent hover:text-accent-foreground px-4 py-2 relative h-8 w-full justify-start rounded-[0.5rem] bg-muted/50 text-sm font-normal shadow-none sm:pr-12 lg:max-w-64\"\n\t\t\t\t\t\ttype=\"search\" placeholder=\"Search docs...\" aria-label=\"Search docs...\" autocomplete=\"off\">\n\t\t\n\t\t\t\t\t<div id=\"suggestions\" class=\"shadow  rounded\"></div>\n\t\t\t\t</form>\n\t\t\t</div>\n\t\t\t{% endif %}\n\t\t\n\t\t\n\t\t\n\t\t\t<nav class=\"flex items-center\">\n\t\t\t\t<button id=\"mode\" class=\"btn btn-link order-2 order-md-4\" type=\"button\" aria-label=\"Toggle mode\">\n\t\t\t\t\t<span class=\"toggle-dark hidden\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\"\n\t\t\t\t\t\t\tfill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\n\t\t\t\t\t\t\tclass=\"feather feather-moon\">\n\t\t\t\t\t\t\t<path d=\"M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z\"></path>\n\t\t\t\t\t\t</svg></span>\n\t\t\t\t\t<span class=\"toggle-light\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\"\n\t\t\t\t\t\t\tfill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\n\t\t\t\t\t\t\tclass=\"feather feather-sun\">\n\t\t\t\t\t\t\t<circle cx=\"12\" cy=\"12\" r=\"5\"></circle>\n\t\t\t\t\t\t\t<line x1=\"12\" y1=\"1\" x2=\"12\" y2=\"3\"></line>\n\t\t\t\t\t\t\t<line x1=\"12\" y1=\"21\" x2=\"12\" y2=\"23\"></line>\n\t\t\t\t\t\t\t<line x1=\"4.22\" y1=\"4.22\" x2=\"5.64\" y2=\"5.64\"></line>\n\t\t\t\t\t\t\t<line x1=\"18.36\" y1=\"18.36\" x2=\"19.78\" y2=\"19.78\"></line>\n\t\t\t\t\t\t\t<line x1=\"1\" y1=\"12\" x2=\"3\" y2=\"12\"></line>\n\t\t\t\t\t\t\t<line x1=\"21\" y1=\"12\" x2=\"23\" y2=\"12\"></line>\n\t\t\t\t\t\t\t<line x1=\"4.22\" y1=\"19.78\" x2=\"5.64\" y2=\"18.36\"></line>\n\t\t\t\t\t\t\t<line x1=\"18.36\" y1=\"5.64\" x2=\"19.78\" y2=\"4.22\"></line>\n\t\t\t\t\t\t</svg></span>\n\t\t\t\t</button>\n\t\t\t\t{% if config.extra.menu.social %}\n\t\t\t\t{% for val in config.extra.menu.social %}\n\t\t\t\t<div\n\t\t\t\t\tclass=\"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground py-2 h-8 w-8 px-0\">\n\t\t\t\t\t<a class=\"nav-link\" href=\"{{ val.url | safe }}\">{{ val.pre | safe }}<span class=\"sr-only\">{{\n\t\t\t\t\t\t\tval.name}}</span></a>\n\t\t\t\t</div>\n\t\t\t\t{% endfor %}\n\t\t\t\t{% else %}\n\t\t\t\t{% endif %}\n\t\t\t</nav>\n\t\t</div>\n\t</div>\n</header>\n<!-- JavaScript to toggle menu visibility -->\n<script>\n\tfunction toggleMenu() {\n\t\tvar menu = document.getElementById('menu-toggle');\n\t\tmenu.classList.toggle('hidden');\n\t}\n</script>\n{% endmacro %}\n"
  },
  {
    "path": "docs-site/templates/macros/javascript.html",
    "content": "{% macro javascript() %}\n<script type=\"text/javascript\" src=\"{{ get_url(path=\"js/main.js\") | safe }}\" defer></script>\n{% if config.build_search_index %}\n  <script type=\"text/javascript\" src=\"{{ get_url(path=\"plugins/elasticlunr.min.js\") | safe }}\" defer></script>\n  <script type=\"text/javascript\" src=\"{{ get_url(path=\"search_index.\" ~ lang ~ \".js\") | safe }}\" defer></script>\n  <script type=\"text/javascript\" src=\"{{ get_url(path=\"js/search.js\") | safe }}\" defer></script>\n{% endif %}\n{% endmacro %}"
  },
  {
    "path": "docs-site/templates/macros/page-publish-metadata.html",
    "content": "{% macro page_publish_metadata(page) %}\n<div class=\"mb-8 text-sm text-gray-500\">\n    {{ page.date | date(format=config.extra.timeformat | default(value=\"%A, %B %e, %Y\"),\n        timezone=config.extra.timezone | default(value=\"America/New_York\")) }}\n</div>\n{% endmacro %}\n"
  },
  {
    "path": "docs-site/templates/macros/youtube.html",
    "content": "{% macro youtube(id, autoplay=false, class=false) %}\n  <div class=\"video-container\">\n    <iframe\n        src=\"https://www.youtube.com/embed/{{id}}{% if autoplay %}?autoplay=1{% endif %}\"\n        webkitallowfullscreen\n        mozallowfullscreen\n        allowfullscreen>\n    </iframe>\n  </div>\n{% endmacro %}\n\n{% macro thumb(id, class=false) %}\n<img class=\"max-w-[200px] max-h-[230px] rounded-md\" {% if class %}class=\"{{class}}\"{% endif %} src=\"https://i3.ytimg.com/vi/{{id}}/hqdefault.jpg\" />\n{% endmacro %}\n"
  },
  {
    "path": "docs-site/templates/page.html",
    "content": "{# Default page.html template #}\n\n{% extends \"base.html\" %}\n\n{% block seo %}\n{{ super() }}\n{% set title_addition = \"\" %}\n{% if page.title and config.title %}\n{% set title = page.title %}\n{% set title_addition = title_separator ~ config.title %}\n{% elif page.title %}\n{% set title = page.title %}\n{% else %}\n{% set title = config.title %}\n{% endif %}\n\n{% if page.description %}\n{% set description = page.description %}\n{% else %}\n{% set description = config.description %}\n{% endif %}\n{% set created_time = page.date %}\n{% set updated_time = page.updated %}\n{% if current_section %}\n{% set page_section = current_section %}\n{% else %}\n{% set page_section = \"\" %}\n{% endif %}\n\n{{ macros_head::seo(title=title, title_addition=title_addition, description=description, type=\"article\", is_page=true,\ncreated_time=created_time, updated_time=updated_time, page_section=page_section) }}\n{% endblock seo %}\n\n{% block body %}\n{% if section.extra.class %}\n{% set page_class = page.extra.class %}\n{% else %}\n{% set page_class = \"page single\" %}\n{% endif %}\n{% endblock body %}\n\n{% block content %}\n<div class=\"wrap container\" role=\"document\">\n  <div class=\"content\">\n    <div class=\"row justify-content-center\">\n      <div class=\"col-md-12 col-lg-10 col-xxl-8\">\n        <article>\n          <div class=\"page-header\">\n            <h1>{{ page.title | safe }}</h1>\n          </div>\n          {% if page.extra.lead %}<p class=\"lead\">{{ page.extra.lead }}</p>{% endif %}\n          <div class=\"markdown\">\n            {{ page.content | safe }}\n          </div>\n        </article>\n      </div>\n    </div>\n  </div>\n</div>\n{% endblock content %}\n"
  },
  {
    "path": "docs-site/templates/section.html",
    "content": "{# Default section.html template #}\n\n{% extends \"base.html\" %}\n\n{% block seo %}\n{{ super() }}\n{% set title_addition = \"\" %}\n\n{% if section.title and config.title %}\n{% set title = section.title %}\n{% set title_addition = title_separator ~ config.title %}\n{% elif section.title %}\n{% set title = section.title %}\n{% else %}\n{% set title = config.title %}\n{% endif %}\n\n{% if section.description %}\n{% set description = section.description %}\n{% else %}\n{% set description = config.description %}\n{% endif %}\n\n{{ macros_head::seo(title=title, title_addition=title_addition, description=description) }}\n{% endblock seo %}\n\n{% block body %}\n{% if section.extra.class %}\n{% set page_class = section.extra.class %}\n{% else %}\n{% set page_class = \"page list\" %}\n{% endif %}\n{% endblock body %}\n\n{% block content %}\n<div class=\"wrap container\" role=\"document\">\n    <div class=\"content\">\n        <div class=\"row justify-content-center\">\n            <div class=\"col-md-12 col-lg-10 col-xxl-8\">\n                <article>\n                    <div class=\"page-header\">\n                        <h1>{{ section.title | safe }}</h1>\n                    </div>\n                    {{ section.content | safe }}\n                </article>\n            </div>\n        </div>\n    </div>\n</div>\n{% endblock content %}"
  },
  {
    "path": "docs-site/templates/shortcodes/get_env.html",
    "content": ""
  },
  {
    "path": "docs-site/templates/taxonomy_list.html",
    "content": ""
  },
  {
    "path": "docs-site/templates/taxonomy_single.html",
    "content": ""
  },
  {
    "path": "docs-site/translations/tour-fr.md",
    "content": "+++\ntitle = \"Aperçu rapide\"\ndate = 2021-05-01T08:00:00+00:00\nupdated = 2021-05-01T08:00:00+00:00\ndraft = false\nweight = 2\nsort_by = \"weight\"\ntemplate = \"docs/page.html\"\n\n[extra]\ntoc = true\ntop = false\nflair =[]\n+++\n\n[English](./index.md) - Français\n\n<img style=\"width:100%; max-width:640px\" src=\"tour.png\"/>\n<br/>\n<br/>\n<br/>\nCréons un blog coté serveur sur Loco en quelques minutes. Commençons par installer `loco` et `sea-orm-cli`:\n\n<!-- <snip id=\"quick-installation-command\" inject_from=\"yaml\" template=\"sh\"> -->\n```sh\ncargo install loco\ncargo install sea-orm-cli # Only when DB is needed\n```\n<!-- </snip> -->\n\n\n Vous pouvez maintenant créer votre nouvelle application (choisissez \"`SaaS` app\").\n\n ```sh\n ❯ loco new\n✔ ❯ App name? · myapp\n✔ ❯ What would you like to build? · SaaS app (with DB and user auth)\n✔ ❯ Select a DB Provider · Sqlite\n✔ ❯ Select your background worker type · Async (in-process tokyo async tasks)\n✔ ❯ Select an asset serving configuration · Client (configures assets for frontend serving)\n\n 🚂 Loco app generated successfully in:\n myapp/\n ```\n\nSi vous sélectionnez tous les paramètres par défaut, vous aurez:\n\n* `sqlite` pour la base de données. Découvrez les types de bases de données dans [Sqlite vs Postgres](@/docs/the-app/models.md#sqlite-vs-postgres) dans la section _models_ .\n* `async` pour les _workers_ en arrière-plan. En savoir plus sur la configuration des _workers_  [async vs queue](@/docs/processing/workers.md#async-vs-queue) dans la section _workers_ .\n* `Client` configuration pour la diffusion des ressources. Cela signifie que votre backend servira d'API.\n\n\n Maintenant, faites `cd` dans votre `myapp` et démarrez votre application en exécutant `cargo loco start`:\n\n<!-- <snip id=\"starting-the-server-command-with-output\" inject_from=\"yaml\" template=\"sh\"> -->\n```sh\n$ cargo loco start\n\n                      ▄     ▀\n                                ▀  ▄\n                  ▄       ▀     ▄  ▄ ▄▀\n                                    ▄ ▀▄▄\n                        ▄     ▀    ▀  ▀▄▀█▄\n                                          ▀█▄\n▄▄▄▄▄▄▄  ▄▄▄▄▄▄▄▄▄   ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄ ▀▀█\n██████  █████   ███ █████   ███ █████   ███ ▀█\n██████  █████   ███ █████   ▀▀▀ █████   ███ ▄█▄\n██████  █████   ███ █████       █████   ███ ████▄\n██████  █████   ███ █████   ▄▄▄ █████   ███ █████\n██████  █████   ███  ████   ███ █████   ███ ████▀\n  ▀▀▀██▄ ▀▀▀▀▀▀▀▀▀▀  ▀▀▀▀▀▀▀▀▀▀  ▀▀▀▀▀▀▀▀▀▀ ██▀\n      ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀\n                https://loco.rs\n\nlistening on port 5150\n```\n<!-- </snip> -->\n\n\n <div class=\"infobox\">\n Vous n'êtes pas obligé d'exécuter via `cargo` mais en développement, c'est fortement\nrecommandé. Si vous compilez avec `--release`, votre binaire contiendra tout\ny compris votre code. Ainsi `cargo` ou Rust ne seront plus nécessaire. </div>\n\n## Ajouter une API de type CRUD\n\nNous avons une application SaaS de base avec une authentification utilisateur générée pour nous. Faisons-en un backend de blog en ajoutant un `post` et une API CRUD complète à l'aide de `scaffold` :\n\n```sh\n$ cargo loco generate scaffold post title:string content:text\n\n  :\n  :\nadded: \"src/controllers/post.rs\"\ninjected: \"src/controllers/mod.rs\"\ninjected: \"src/app.rs\"\nadded: \"tests/requests/post.rs\"\ninjected: \"tests/requests/mod.rs\"\n* Migration for `post` added! You can now apply it with `$ cargo loco db migrate`.\n* A test for model `posts` was added. Run with `cargo test`.\n* Controller `post` was added successfully.\n* Tests for controller `post` was added successfully. Run `cargo test`.\n```\n\nVotre base de données a été migrée et le modèle, les entités et un contrôleur CRUD complet ont été générés automatiquement.\n\nRedémarrez votre application :\n<!-- <snip id=\"starting-the-server-command-with-output\" inject_from=\"yaml\" template=\"sh\"> -->\n```sh\n$ cargo loco start\n\n                      ▄     ▀\n                                ▀  ▄\n                  ▄       ▀     ▄  ▄ ▄▀\n                                    ▄ ▀▄▄\n                        ▄     ▀    ▀  ▀▄▀█▄\n                                          ▀█▄\n▄▄▄▄▄▄▄  ▄▄▄▄▄▄▄▄▄   ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄ ▀▀█\n██████  █████   ███ █████   ███ █████   ███ ▀█\n██████  █████   ███ █████   ▀▀▀ █████   ███ ▄█▄\n██████  █████   ███ █████       █████   ███ ████▄\n██████  █████   ███ █████   ▄▄▄ █████   ███ █████\n██████  █████   ███  ████   ███ █████   ███ ████▀\n  ▀▀▀██▄ ▀▀▀▀▀▀▀▀▀▀  ▀▀▀▀▀▀▀▀▀▀  ▀▀▀▀▀▀▀▀▀▀ ██▀\n      ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀\n                https://loco.rs\n\nlistening on port 5150\n```\n<!-- </snip> -->\n\nEnsuite, essayez d’ajouter un `post` avec `curl`:\n\n```sh\n$ curl -X POST -H \"Content-Type: application/json\" -d '{\n  \"title\": \"Your Title\",\n  \"content\": \"Your Content xxx\"\n}' localhost:5150/posts\n```\n\nVous pouvez lister vos publications (posts):\n\n```sh\n$ curl localhost:5150/posts\n```\n\nPour ceux qui comptent -- les commandes pour créer un backend de blog étaient:\n\n1. `cargo install loco`\n2. `cargo install sea-orm-cli`\n3. `loco new`\n4. `cargo loco generate scaffold post title:string content:text`\n\nVoilà! Profitez de votre balade avec `loco` 🚂\n\n## Vérifions l'authentification SaaS\n\nL'application Saas générée contient une suite d’authentification entièrement fonctionnelle, basée sur les JWT.\n\n### Enregistrer un nouvel utilisateur\n\nLe point de terminaison `/api/auth/register` crée un nouvel utilisateur dans la base de données avec un `email_verification_token` pour la vérification du compte. Un e-mail de bienvenue est envoyé à l'utilisateur avec un lien de vérification.\n\n```sh\n$ curl --location '127.0.0.1:5150/api/auth/register' \\\n     --header 'Content-Type: application/json' \\\n     --data-raw '{\n         \"name\": \"Loco user\",\n         \"email\": \"user@loco.rs\",\n         \"password\": \"12341234\"\n     }'\n```\n\nPour des raisons de sécurité, si l'utilisateur est déjà enregistré, aucun nouvel utilisateur n'est créé et un statut 200 est renvoyé sans exposer les détails de l'e-mail de l'utilisateur.\n\n### Login\n\nAprès avoir enregistré un nouvel utilisateur, utilisez la requête suivante pour vous connecter:\n\n```sh\n$ curl --location '127.0.0.1:5150/api/auth/login' \\\n     --header 'Content-Type: application/json' \\\n     --data-raw '{\n         \"email\": \"user@loco.rs\",\n         \"password\": \"12341234\"\n     }'\n```\n\nLa réponse inclut un Token (jeton) JWT pour l’authentification, l’ID utilisateur, le nom et l’état de vérification.\n\n```sh\n{\n    \"token\": \"...\",\n    \"pid\": \"2b20f998-b11e-4aeb-96d7-beca7671abda\",\n    \"name\": \"Loco user\",\n    \"claims\": null\n    \"is_verified\": false\n}\n```\n\nDans votre application côté client, vous enregistrez ce jeton JWT et effectuez les requêtes suivantes avec le jeton en utilisant _bearer token_ (voir ci-dessous) afin que les requêtes soient authentifiées.\n\n### Obtenir l'utilisateur actuel\n\nCe point de terminaison est protégé par un middleware d'authentification. Nous utiliserons le jeton que nous avons obtenu précédemment pour effectuer une requête avec la technique _bearer token_ (remplacez `TOKEN` par le jeton JWT que vous avez obtenu précédemment):\n\n```sh\n$ curl --location --request GET '127.0.0.1:5150/api/auth/current' \\\n     --header 'Content-Type: application/json' \\\n     --header 'Authorization: Bearer TOKEN'\n```\n\nVoilà votre première demande authentifiée !\n\nConsultez le code source de `controllers/auth.rs` pour voir comment utiliser le middleware d'authentification dans vos propres contrôleurs.\n"
  },
  {
    "path": "loco-cli/.gitignore",
    "content": "stateless-template\nsaas-template\n\nCargo.lock\n"
  },
  {
    "path": "loco-cli/.rustfmt.toml",
    "content": "max_width = 100\nuse_small_heuristics = \"Default\"\n"
  },
  {
    "path": "loco-cli/Cargo.toml",
    "content": "[workspace]\n\n[package]\nname = \"loco-cli\"\nversion = \"0.13.0\"\nedition = \"2021\"\ndescription = \"loco cli website generator\"\nlicense = \"Apache-2.0\"\nhomepage = \"https://docs.rs/loco-cli\"\ndocumentation = \"https://docs.rs/loco-cli\"\n\n[profile.release]\nstrip = true\n\n[lib]\nname = \"loco_cli\"\npath = \"src/lib.rs\"\n\n[[bin]]\nname = \"loco\"\npath = \"src/bin/main.rs\"\nrequired-features = []\n\n\n[features]\ndefault = []\n\n[dependencies]\n"
  },
  {
    "path": "loco-cli/README.md",
    "content": "# Loco CLI\n\nLoco CLI is a powerful command-line tool designed to streamline the process of generating Loco websites.\n\n## Installation\n\nTo install Loco CLI, execute the following command in your terminal:\n\n```sh\ncargo install loco-cli\n```\n\n## Usage\n\n### Generating a Website\n\nThis command generates a website in your current working directory:\n\n```sh\nloco new\n```\n\nTo generate the website in a different directory, use the following command:\n\n```sh\nloco new --path /my-work/websites/\n```\n\n\n## Running Locally\n\nWhen working with loco-cli against the local Loco repository, you can utilize the `STARTERS_LOCAL_PATH` environment variable to point the generator to a local starter instead of fetching from GitHub.\n\n```sh\ncd loco-cli\n$ STARTERS_LOCAL_PATH=[FULL_PATH]/loco-rs/loco  cargo run new --path /tmp\n```\n\n## Starters folder\n\nThis CLI depends on a folder with _starters_. Each starter is a folder with a `generator.yaml` in its root.\n\nThe `generator.yaml` file describes:\n\n* _Global replacements_: a regex describing things to replace such as a mock app name with a real app name that the user selected.\n\nFor example:\n```yaml\n...\nrules:\n  - pattern: loco_starter_template\n    kind: LibName\n    file_patterns:\n      - rs\n      - toml\n      - trycmd\n  - pattern: PqRwLF2rhHe8J22oBeHy\n    kind: JwtToken\n    file_patterns:\n      - config/test.yaml\n      - config/development.yaml\n```\n\n* _Starter options_: some starters can configure based on multiple options: which database to use, which asset pipeline, which kind of background worker configuration. Each starter _declares_ what kind of options it subscribes into and is relevant for it.\n\nThe options are picked up in generation, for each option a selection is made for the user to pick.\n\nFor example:\n\n```yaml\n---\ndescription: SaaS app (with DB and user auth)\noptions:\n  - db\n  - bg\n  - assets\nrules:\n    # ...\n```\n\nAs an example, for the `db` option: `postgres` or `sqlite` is offered as a selection.\n\nThe source of truth of _which options_ exist and _which selection for each option_ is based on 2 factors:\n\n1. A set of enums to describe all options (in this project, the CLI)\n2. Support of the options and formatting of the configuration: in the main Loco project\n\nEnabling or disabling options are done by:\n\n* Replacing text with a different text (such as configuration value for background worker type)\n* Enabling or disabling blocks in the configuration by adding or removing comment blocks, using block markers inside the configuration file (`(block-name-start)`, etc)\n"
  },
  {
    "path": "loco-cli/src/bin/main.rs",
    "content": "fn main() {\n    println!(\"==============================================\");\n    println!(\"  ⚠️  DEPRECATION NOTICE: `loco-cli`\");\n    println!(\"==============================================\");\n    println!();\n    println!(\"🌟 `loco-cli` has been replaced with `loco`, a more powerful and flexible CLI for creating Loco apps.\");\n    println!(\"🔗 New crate: https://crates.io/crates/loco\");\n    println!();\n    println!(\"To upgrade to the new CLI, run:\");\n    println!();\n    println!(\"  $ cargo uninstall loco-cli && cargo install loco\");\n    println!();\n    println!(\"Thank you for using Loco! 🌟\");\n}\n"
  },
  {
    "path": "loco-cli/src/lib.rs",
    "content": "\n"
  },
  {
    "path": "loco-gen/.gitattributes",
    "content": "src/templates/* text eol=lf\n"
  },
  {
    "path": "loco-gen/Cargo.toml",
    "content": "[package]\nname = \"loco-gen\"\nversion = \"0.16.4\"\ndescription = \"Loco generators\"\nlicense.workspace = true\nedition.workspace = true\nrust-version.workspace = true\n\n[features]\nwith-db = []\n\n[lib]\npath = \"src/lib.rs\"\n\n[dependencies]\n\ncruet = \"0.14.0\"\nrrgen = \"0.5.6\"\nserde = { workspace = true }\nserde_json = { workspace = true }\nthiserror = { workspace = true }\nregex = { workspace = true }\ntracing = { workspace = true }\nchrono = { workspace = true }\ncolored = { workspace = true }\nheck = { workspace = true }\ntera = { workspace = true }\nduct = { workspace = true }\nclap = { version = \"4.4.7\", features = [\"derive\"] }\ninclude_dir = { version = \"0.7.4\" }\n\n[dev-dependencies]\ntree-fs = { version = \"0.3\" }\nsyn = { version = \"2\", features = [\"full\"] }\nserial_test = \"3.1.1\"\nuuid = { version = \"1.11.0\", features = [\"v4\", \"fast-rng\"] }\ninsta = { version = \"1.41.1\", features = [\"redactions\", \"yaml\", \"filters\"] }\nrstest = \"0.23.0\"\n"
  },
  {
    "path": "loco-gen/src/controller.rs",
    "content": "use super::{AppInfo, GenerateResults, Result};\nuse crate as gen;\nuse rrgen::RRgen;\nuse serde_json::json;\nuse std::path::Path;\n\npub fn generate(\n    rrgen: &RRgen,\n    name: &str,\n    actions: &[String],\n    kind: &gen::ScaffoldKind,\n    appinfo: &AppInfo,\n) -> Result<GenerateResults> {\n    let vars = json!({\"name\": name, \"actions\": actions, \"pkg_name\": appinfo.app_name});\n    match kind {\n        gen::ScaffoldKind::Api => gen::render_template(rrgen, Path::new(\"controller/api\"), &vars),\n        gen::ScaffoldKind::Html => {\n            let mut gen_result =\n                gen::render_template(rrgen, Path::new(\"controller/html/controller.t\"), &vars)?;\n            for action in actions {\n                let vars = json!({\"name\": name, \"action\": action, \"pkg_name\": appinfo.app_name});\n                let res = gen::render_template(rrgen, Path::new(\"controller/html/view.t\"), &vars)?;\n                gen_result.rrgen.extend(res.rrgen);\n                gen_result.local_templates.extend(res.local_templates);\n            }\n            Ok(gen_result)\n        }\n        gen::ScaffoldKind::Htmx => {\n            let mut gen_result =\n                gen::render_template(rrgen, Path::new(\"controller/htmx/controller.t\"), &vars)?;\n            for action in actions {\n                let vars = json!({\"name\": name, \"action\": action, \"pkg_name\": appinfo.app_name});\n                let res = gen::render_template(rrgen, Path::new(\"controller/htmx/view.t\"), &vars)?;\n                gen_result.rrgen.extend(res.rrgen);\n                gen_result.local_templates.extend(res.local_templates);\n            }\n            Ok(gen_result)\n        }\n    }\n}\n"
  },
  {
    "path": "loco-gen/src/infer.rs",
    "content": "use cruet::{case::snake::to_snake_case, Inflector};\n\nuse crate::{Error, Result};\n\n#[derive(Debug, PartialEq, Eq)]\npub enum MigrationType {\n    CreateTable { table: String },\n    AddColumns { table: String },\n    RemoveColumns { table: String },\n    AddReference { table: String },\n    CreateJoinTable { table_a: String, table_b: String },\n    Empty,\n}\n\npub enum FieldType {\n    Reference,\n    ReferenceWithCustomField(String),\n    NullableReference,\n    NullableReferenceWithCustomField(String),\n    Type(String),\n    TypeWithParameters(String, Vec<String>),\n}\n\npub fn parse_field_type(ftype: &str) -> Result<FieldType> {\n    let parts: Vec<&str> = ftype.split(':').collect();\n\n    match parts.as_slice() {\n        [\"references?\"] => Ok(FieldType::NullableReference),\n        [\"references?\", f] => Ok(FieldType::NullableReferenceWithCustomField(\n            (*f).to_string(),\n        )),\n        [\"references\"] => Ok(FieldType::Reference),\n        [\"references\", f] => Ok(FieldType::ReferenceWithCustomField((*f).to_string())),\n        [t] => Ok(FieldType::Type((*t).to_string())),\n        [t, params @ ..] => Ok(FieldType::TypeWithParameters(\n            (*t).to_string(),\n            params.iter().map(ToString::to_string).collect::<Vec<_>>(),\n        )),\n        [] => Err(Error::Message(format!(\"cannot parse type: `{ftype}`\"))),\n    }\n}\npub fn guess_migration_type(migration_name: &str) -> MigrationType {\n    let normalized_name = to_snake_case(migration_name);\n    let parts: Vec<&str> = normalized_name.split('_').collect();\n\n    match parts.as_slice() {\n        [\"create\", table_name] => MigrationType::CreateTable {\n            table: table_name.to_plural(),\n        },\n        [\"add\", _reference_name, \"ref\", \"to\", table_name] => MigrationType::AddReference {\n            table: table_name.to_plural(),\n        },\n        [\"add\", _column_names @ .., \"to\", table_name] => MigrationType::AddColumns {\n            table: table_name.to_plural(),\n        },\n        [\"remove\", _column_names @ .., \"from\", table_name] => MigrationType::RemoveColumns {\n            table: table_name.to_plural(),\n        },\n        [\"create\", \"join\", \"table\", parts @ ..] => parts\n            .iter()\n            .position(|&part| part == \"and\")\n            .map_or(MigrationType::Empty, |and_index| {\n                let first_parts = &parts[..and_index];\n                let second_parts = &parts[and_index + 1..];\n\n                if first_parts.is_empty() || second_parts.is_empty() {\n                    return MigrationType::Empty;\n                }\n\n                let table_a = first_parts.join(\"_\");\n                let table_b = second_parts.join(\"_\");\n\n                let table_a = table_a.to_singular();\n                let table_b = table_b.to_singular();\n                MigrationType::CreateJoinTable { table_a, table_b }\n            }),\n        _ => MigrationType::Empty,\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_infer_create_table() {\n        assert_eq!(\n            guess_migration_type(\"CreateUsers\"),\n            MigrationType::CreateTable {\n                table: \"users\".to_string(),\n            }\n        );\n    }\n\n    #[test]\n    fn test_infer_add_columns() {\n        assert_eq!(\n            guess_migration_type(\"AddNameAndAgeToUsers\"),\n            MigrationType::AddColumns {\n                table: \"users\".to_string(),\n            }\n        );\n    }\n\n    #[test]\n    fn test_infer_remove_columns() {\n        assert_eq!(\n            guess_migration_type(\"RemoveNameAndAgeFromUsers\"),\n            MigrationType::RemoveColumns {\n                table: \"users\".to_string(),\n            }\n        );\n    }\n\n    #[test]\n    fn test_infer_add_reference() {\n        assert_eq!(\n            guess_migration_type(\"AddUserRefToPosts\"),\n            MigrationType::AddReference {\n                table: \"posts\".to_string(),\n            }\n        );\n    }\n\n    #[test]\n    fn test_infer_create_join_table() {\n        assert_eq!(\n            guess_migration_type(\"CreateJoinTableUsersAndGroups\"),\n            MigrationType::CreateJoinTable {\n                table_a: \"user\".to_string(),\n                table_b: \"group\".to_string()\n            }\n        );\n    }\n\n    #[test]\n    fn test_infer_create_join_table_with_underscores() {\n        // Test the specific case that was failing\n        assert_eq!(\n            guess_migration_type(\"CreateJoinTableGlobal_recipesAndGlobal_materials\"),\n            MigrationType::CreateJoinTable {\n                table_a: \"global_recipe\".to_string(),\n                table_b: \"global_material\".to_string()\n            }\n        );\n    }\n\n    #[test]\n    fn test_infer_create_join_table_complex_names() {\n        // Test more complex table names with multiple underscores\n        assert_eq!(\n            guess_migration_type(\"CreateJoinTableUser_profilesAndGroup_members\"),\n            MigrationType::CreateJoinTable {\n                table_a: \"user_profile\".to_string(),\n                table_b: \"group_member\".to_string()\n            }\n        );\n    }\n\n    #[test]\n    fn test_infer_create_join_table_mixed_names() {\n        // Test one simple name and one complex name\n        assert_eq!(\n            guess_migration_type(\"CreateJoinTableUsersAndGroup_members\"),\n            MigrationType::CreateJoinTable {\n                table_a: \"user\".to_string(),\n                table_b: \"group_member\".to_string()\n            }\n        );\n    }\n\n    #[test]\n    fn test_empty_migration() {\n        assert_eq!(\n            guess_migration_type(\"UnknownMigrationType\"),\n            MigrationType::Empty\n        );\n    }\n\n    #[test]\n    fn test_infer_create_join_table_no_and_separator() {\n        // Test case where there's no \"and\" separator\n        assert_eq!(\n            guess_migration_type(\"CreateJoinTableUsersGroups\"),\n            MigrationType::Empty\n        );\n    }\n\n    #[test]\n    fn test_infer_create_join_table_empty_after_and() {\n        // Test case where there are no parts after \"and\"\n        assert_eq!(\n            guess_migration_type(\"CreateJoinTableUsersAnd\"),\n            MigrationType::Empty\n        );\n    }\n\n    #[test]\n    fn test_infer_create_join_table_empty_before_and() {\n        // Test case where there are no parts before \"and\"\n        assert_eq!(\n            guess_migration_type(\"CreateJoinTableAndGroups\"),\n            MigrationType::Empty\n        );\n    }\n\n    #[test]\n    fn test_infer_create_join_table_multiple_ands() {\n        // Test case with multiple \"and\" separators (should use first one)\n        assert_eq!(\n            guess_migration_type(\"CreateJoinTableUsersAndGroupsAndMore\"),\n            MigrationType::CreateJoinTable {\n                table_a: \"user\".to_string(),\n                table_b: \"groups_and_more\".to_string()\n            }\n        );\n    }\n}\n"
  },
  {
    "path": "loco-gen/src/lib.rs",
    "content": "// this is because not using with-db renders some of the structs below unused\n// TODO: should be more properly aligned with extracting out the db-related gen\n// code and then feature toggling it\n#![allow(dead_code)]\npub use rrgen::{GenResult, RRgen};\nuse serde::{Deserialize, Serialize};\nuse serde_json::{json, Value};\nmod controller;\nuse colored::Colorize;\nuse std::fmt::Write;\nuse std::{\n    collections::HashMap,\n    fs,\n    path::{Path, PathBuf},\n    sync::OnceLock,\n};\n\n#[cfg(feature = \"with-db\")]\nmod infer;\n#[cfg(feature = \"with-db\")]\nmod migration;\n#[cfg(feature = \"with-db\")]\nmod model;\n#[cfg(feature = \"with-db\")]\nmod scaffold;\npub mod template;\npub mod tera_ext;\n#[cfg(test)]\nmod testutil;\n\n#[derive(Debug)]\npub struct GenerateResults {\n    rrgen: Vec<rrgen::GenResult>,\n    local_templates: Vec<PathBuf>,\n}\n\n#[derive(thiserror::Error, Debug)]\npub enum Error {\n    #[error(\"{0}\")]\n    Message(String),\n    #[error(\"template {} not found\", path.display())]\n    TemplateNotFound { path: PathBuf },\n    #[error(transparent)]\n    RRgen(#[from] rrgen::Error),\n    #[error(transparent)]\n    IO(#[from] std::io::Error),\n    #[error(transparent)]\n    Any(#[from] Box<dyn std::error::Error + Send + Sync>),\n}\n\nimpl Error {\n    pub fn msg(err: impl std::error::Error + Send + Sync + 'static) -> Self {\n        Self::Message(err.to_string()) //.bt()\n    }\n}\n\npub type Result<T> = std::result::Result<T, Error>;\n\n#[derive(Serialize, Deserialize, Debug)]\nstruct FieldType {\n    name: String,\n    rust: RustType,\n    schema: String,\n    col_type: String,\n    #[serde(default)]\n    arity: usize,\n}\n\n#[derive(Debug, Deserialize, Serialize)]\n#[serde(untagged)]\npub enum RustType {\n    String(String),\n    Map(HashMap<String, String>),\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct Mappings {\n    field_types: Vec<FieldType>,\n}\nimpl Mappings {\n    fn error_unrecognized_default_field(&self, field: &str) -> Error {\n        Self::error_unrecognized(field, &self.all_names())\n    }\n\n    fn error_unrecognized(field: &str, allow_fields: &[&String]) -> Error {\n        Error::Message(format!(\n            \"type: `{}` not found. try any of: `{}`\",\n            field,\n            allow_fields\n                .iter()\n                .map(|&s| s.clone())\n                .collect::<Vec<String>>()\n                .join(\",\")\n        ))\n    }\n\n    /// Resolves the Rust type for a given field with optional parameters.\n    ///\n    /// # Errors\n    ///\n    /// if rust field not exists or invalid parameters\n    pub fn rust_field_with_params(&self, field: &str, params: &Vec<String>) -> Result<&str> {\n        match field {\n            \"array\" | \"array^\" | \"array!\" => {\n                if let RustType::Map(ref map) = self.rust_field_kind(field)? {\n                    if let [single] = params.as_slice() {\n                        let keys: Vec<&String> = map.keys().collect();\n                        Ok(map\n                            .get(single)\n                            .ok_or_else(|| Self::error_unrecognized(field, &keys))?)\n                    } else {\n                        Err(self.error_unrecognized_default_field(field))\n                    }\n                } else {\n                    Err(Error::Message(\n                        \"array field should configured as array\".to_owned(),\n                    ))\n                }\n            }\n\n            _ => self.rust_field(field),\n        }\n    }\n\n    /// Resolves the Rust type for a given field.\n    ///\n    /// # Errors\n    ///\n    /// When the given field not recognized\n    pub fn rust_field_kind(&self, field: &str) -> Result<&RustType> {\n        self.field_types\n            .iter()\n            .find(|f| f.name == field)\n            .map(|f| &f.rust)\n            .ok_or_else(|| self.error_unrecognized_default_field(field))\n    }\n\n    /// Resolves the Rust type for a given field.\n    ///\n    /// # Errors\n    ///\n    /// When the given field not recognized\n    pub fn rust_field(&self, field: &str) -> Result<&str> {\n        self.field_types\n            .iter()\n            .find(|f| f.name == field)\n            .map(|f| &f.rust)\n            .ok_or_else(|| self.error_unrecognized_default_field(field))\n            .and_then(|rust_type| match rust_type {\n                RustType::String(s) => Ok(s),\n                RustType::Map(_) => Err(Error::Message(format!(\n                    \"type `{field}` need params to get the rust field type\"\n                ))),\n            })\n            .map(std::string::String::as_str)\n    }\n\n    /// Retrieves the schema field associated with the given field.\n    ///\n    /// # Errors\n    ///\n    /// When the given field not recognized\n    pub fn schema_field(&self, field: &str) -> Result<&str> {\n        self.field_types\n            .iter()\n            .find(|f| f.name == field)\n            .map(|f| f.schema.as_str())\n            .ok_or_else(|| self.error_unrecognized_default_field(field))\n    }\n\n    /// Retrieves the column type field associated with the given field.\n    ///\n    /// # Errors\n    ///\n    /// When the given field not recognized\n    pub fn col_type_field(&self, field: &str) -> Result<&str> {\n        self.field_types\n            .iter()\n            .find(|f| f.name == field)\n            .map(|f| f.col_type.as_str())\n            .ok_or_else(|| self.error_unrecognized_default_field(field))\n    }\n\n    /// Retrieves the column type arity associated with the given field.\n    ///\n    /// # Errors\n    ///\n    /// When the given field not recognized\n    pub fn col_type_arity(&self, field: &str) -> Result<usize> {\n        self.field_types\n            .iter()\n            .find(|f| f.name == field)\n            .map(|f| f.arity)\n            .ok_or_else(|| self.error_unrecognized_default_field(field))\n    }\n\n    #[must_use]\n    pub fn all_names(&self) -> Vec<&String> {\n        self.field_types.iter().map(|f| &f.name).collect::<Vec<_>>()\n    }\n}\n\nstatic MAPPINGS: OnceLock<Mappings> = OnceLock::new();\n\n/// Get type mapping for generation\n///\n/// # Panics\n///\n/// Panics if loading fails\npub fn get_mappings() -> &'static Mappings {\n    MAPPINGS.get_or_init(|| {\n        let json_data = include_str!(\"./mappings.json\");\n        serde_json::from_str(json_data).expect(\"JSON was not well-formatted\")\n    })\n}\n\n#[derive(clap::ValueEnum, Clone, Debug)]\npub enum ScaffoldKind {\n    Api,\n    Html,\n    Htmx,\n}\n\n#[derive(Debug, Clone)]\npub enum DeploymentKind {\n    Docker {\n        copy_paths: Vec<PathBuf>,\n        is_client_side_rendering: bool,\n    },\n    Nginx {\n        host: String,\n        port: i32,\n    },\n}\n\n#[derive(Debug)]\npub enum Component {\n    #[cfg(feature = \"with-db\")]\n    Model {\n        /// Name of the thing to generate\n        name: String,\n\n        /// Whether to include timestamps (`created_at``updated_at`at columns) in the model\n        with_tz: bool,\n\n        /// Model fields, eg. title:string hits:int\n        fields: Vec<(String, String)>,\n    },\n    #[cfg(feature = \"with-db\")]\n    Migration {\n        /// Name of the migration file\n        name: String,\n\n        /// Whether to include timestamps (`created_at`, `updated_at` columns) in the migration\n        with_tz: bool,\n\n        /// Params fields, eg. title:string hits:int\n        fields: Vec<(String, String)>,\n    },\n    #[cfg(feature = \"with-db\")]\n    Scaffold {\n        /// Name of the thing to generate\n        name: String,\n\n        /// Whether to include timestamps (`created_at``updated_at`at columns) in the scaffold\n        with_tz: bool,\n\n        /// Model and params fields, eg. title:string hits:int\n        fields: Vec<(String, String)>,\n\n        // k\n        kind: ScaffoldKind,\n    },\n    Controller {\n        /// Name of the thing to generate\n        name: String,\n\n        /// Action names\n        actions: Vec<String>,\n\n        // kind\n        kind: ScaffoldKind,\n    },\n    Task {\n        /// Name of the thing to generate\n        name: String,\n    },\n    Scheduler {},\n    Worker {\n        /// Name of the thing to generate\n        name: String,\n    },\n    Mailer {\n        /// Name of the thing to generate\n        name: String,\n    },\n    Data {\n        /// Name of the thing to generate\n        name: String,\n    },\n    Deployment {\n        kind: DeploymentKind,\n    },\n}\n\npub struct AppInfo {\n    pub app_name: String,\n}\n\n#[must_use]\npub fn new_generator() -> RRgen {\n    RRgen::default().add_template_engine(tera_ext::new())\n}\n\n/// Generate a component\n///\n/// # Errors\n///\n/// This function will return an error if it fails\npub fn generate(rrgen: &RRgen, component: Component, appinfo: &AppInfo) -> Result<GenerateResults> {\n    /*\n    (1)\n    XXX: remove hooks generic from child generator, materialize it here and pass it\n         means each generator accepts a [component, config, context] tuple\n         this will allow us to test without an app instance\n    (2) proceed to test individual generators\n     */\n    let get_result = match component {\n        #[cfg(feature = \"with-db\")]\n        Component::Model {\n            name,\n            with_tz,\n            fields,\n        } => model::generate(rrgen, &name, with_tz, &fields, appinfo)?,\n        #[cfg(feature = \"with-db\")]\n        Component::Scaffold {\n            name,\n            with_tz,\n            fields,\n            kind,\n        } => scaffold::generate(rrgen, &name, with_tz, &fields, &kind, appinfo)?,\n        #[cfg(feature = \"with-db\")]\n        Component::Migration {\n            name,\n            with_tz,\n            fields,\n        } => migration::generate(rrgen, &name, with_tz, &fields, appinfo)?,\n        Component::Controller {\n            name,\n            actions,\n            kind,\n        } => controller::generate(rrgen, &name, &actions, &kind, appinfo)?,\n        Component::Task { name } => {\n            let vars = json!({\"name\": name, \"pkg_name\": appinfo.app_name});\n            render_template(rrgen, Path::new(\"task\"), &vars)?\n        }\n        Component::Scheduler {} => {\n            let vars = json!({\"pkg_name\": appinfo.app_name});\n            render_template(rrgen, Path::new(\"scheduler\"), &vars)?\n        }\n        Component::Worker { name } => {\n            let vars = json!({\"name\": name, \"pkg_name\": appinfo.app_name});\n            render_template(rrgen, Path::new(\"worker\"), &vars)?\n        }\n        Component::Mailer { name } => {\n            let vars = json!({ \"name\": name });\n            render_template(rrgen, Path::new(\"mailer\"), &vars)?\n        }\n        Component::Deployment { kind } => match kind {\n            DeploymentKind::Docker {\n                copy_paths,\n                is_client_side_rendering,\n            } => {\n                let vars = json!({\n                    \"pkg_name\": appinfo.app_name,\n                    \"copy_paths\": copy_paths,\n                    \"is_client_side_rendering\": is_client_side_rendering,\n                });\n                render_template(rrgen, Path::new(\"deployment/docker\"), &vars)?\n            }\n            DeploymentKind::Nginx { host, port } => {\n                let host = host.replace(\"http://\", \"\").replace(\"https://\", \"\");\n                let vars = json!({\n                    \"pkg_name\": appinfo.app_name,\n                    \"domain\": host,\n                    \"port\": port\n                });\n                render_template(rrgen, Path::new(\"deployment/nginx\"), &vars)?\n            }\n        },\n        Component::Data { name } => {\n            let vars = json!({ \"name\": name });\n            render_template(rrgen, Path::new(\"data\"), &vars)?\n        }\n    };\n\n    Ok(get_result)\n}\n\nfn render_template(rrgen: &RRgen, template: &Path, vars: &Value) -> Result<GenerateResults> {\n    let template_files = template::collect_files_from_path(template)?;\n\n    let mut gen_result = vec![];\n    let mut local_templates = vec![];\n    for template in template_files {\n        let custom_template = Path::new(template::DEFAULT_LOCAL_TEMPLATE).join(template.path());\n\n        if custom_template.exists() {\n            let content = fs::read_to_string(&custom_template).map_err(|err| {\n                tracing::error!(custom_template = %custom_template.display(), \"could not read custom template\");\n                err\n            })?;\n            gen_result.push(rrgen.generate(&content, vars)?);\n            local_templates.push(custom_template);\n        } else {\n            let content = template.contents_utf8().ok_or(Error::Message(format!(\n                \"could not get template content: {}\",\n                template.path().display()\n            )))?;\n            gen_result.push(rrgen.generate(content, vars)?);\n        }\n    }\n\n    Ok(GenerateResults {\n        rrgen: gen_result,\n        local_templates,\n    })\n}\n\n#[must_use]\npub fn collect_messages(results: &GenerateResults) -> String {\n    let mut messages = String::new();\n\n    for res in &results.rrgen {\n        if let rrgen::GenResult::Generated {\n            message: Some(message),\n        } = res\n        {\n            let _ = writeln!(messages, \"* {message}\");\n        }\n    }\n\n    if !results.local_templates.is_empty() {\n        let _ = writeln!(messages);\n        let _ = writeln!(\n            messages,\n            \"{}\",\n            \"The following templates were sourced from the local templates:\".green()\n        );\n\n        for f in &results.local_templates {\n            let _ = writeln!(messages, \"* {}\", f.display());\n        }\n    }\n    messages\n}\n\n/// Copies template files to a specified destination directory.\n///\n/// This function copies files from the specified template path to the\n/// destination directory. If the specified path is `/` or `.`, it copies all\n/// files from the templates directory. If the path does not exist in the\n/// templates, it returns an error.\n///\n/// # Errors\n/// when could not copy the given template path\npub fn copy_template(path: &Path, to: &Path) -> Result<Vec<PathBuf>> {\n    let copy_template_path = if path == Path::new(\"/\") || path == Path::new(\".\") {\n        None\n    } else if !template::exists(path) {\n        return Err(Error::TemplateNotFound {\n            path: path.to_path_buf(),\n        });\n    } else {\n        Some(path)\n    };\n\n    let copy_files = if let Some(path) = copy_template_path {\n        template::collect_files_from_path(path)?\n    } else {\n        template::collect_files()\n    };\n\n    let mut copied_files = vec![];\n    for f in copy_files {\n        let copy_to = to.join(f.path());\n        if copy_to.exists() {\n            tracing::debug!(\n                template_file = %copy_to.display(),\n                \"skipping copy template file. already exists\"\n            );\n            continue;\n        }\n        match copy_to.parent() {\n            Some(parent) => {\n                fs::create_dir_all(parent)?;\n            }\n            None => {\n                return Err(Error::Message(format!(\n                    \"could not get parent folder of {}\",\n                    copy_to.display()\n                )))\n            }\n        }\n\n        fs::write(&copy_to, f.contents())?;\n        tracing::trace!(\n            template = %copy_to.display(),\n            \"copy template successfully\"\n        );\n        copied_files.push(copy_to);\n    }\n    Ok(copied_files)\n}\n\n#[cfg(test)]\nmod tests {\n    use std::path::Path;\n\n    use super::*;\n\n    #[test]\n    fn test_template_not_found() {\n        let tree_fs = tree_fs::TreeBuilder::default()\n            .drop(true)\n            .create()\n            .expect(\"create temp file\");\n        let path = Path::new(\"nonexistent-template\");\n\n        let result = copy_template(path, tree_fs.root.as_path());\n        assert!(result.is_err());\n        if let Err(Error::TemplateNotFound { path: p }) = result {\n            assert_eq!(p, path.to_path_buf());\n        } else {\n            panic!(\"Expected TemplateNotFound error\");\n        }\n    }\n\n    #[test]\n    fn test_copy_template_valid_folder_template() {\n        let temp_fs = tree_fs::TreeBuilder::default()\n            .drop(true)\n            .create()\n            .expect(\"Failed to create temporary file system\");\n\n        let template_dir = template::tests::find_first_dir();\n\n        let copy_result = copy_template(template_dir.path(), temp_fs.root.as_path());\n        assert!(\n            copy_result.is_ok(),\n            \"Failed to copy template from directory {:?}\",\n            template_dir.path()\n        );\n\n        let template_files = template::collect_files_from_path(template_dir.path())\n            .expect(\"Failed to collect files from the template directory\");\n\n        assert!(\n            !template_files.is_empty(),\n            \"No files found in the template directory\"\n        );\n\n        for template_file in template_files {\n            let copy_file_path = temp_fs.root.join(template_file.path());\n\n            assert!(\n                copy_file_path.exists(),\n                \"Copy file does not exist: {copy_file_path:?}\"\n            );\n\n            let copy_content =\n                fs::read_to_string(&copy_file_path).expect(\"Failed to read coped file content\");\n\n            assert_eq!(\n                template_file\n                    .contents_utf8()\n                    .expect(\"Failed to get template file content\"),\n                copy_content,\n                \"Content mismatch in file: {copy_file_path:?}\"\n            );\n        }\n    }\n\n    fn test_mapping() -> Mappings {\n        Mappings {\n            field_types: vec![\n                FieldType {\n                    name: \"array\".to_string(),\n                    rust: RustType::Map(HashMap::from([\n                        (\"string\".to_string(), \"Vec<String>\".to_string()),\n                        (\"chat\".to_string(), \"Vec<String>\".to_string()),\n                        (\"int\".to_string(), \"Vec<i32>\".to_string()),\n                    ])),\n                    schema: \"array\".to_string(),\n                    col_type: \"array_null\".to_string(),\n                    arity: 1,\n                },\n                FieldType {\n                    name: \"string^\".to_string(),\n                    rust: RustType::String(\"String\".to_string()),\n                    schema: \"string_uniq\".to_string(),\n                    col_type: \"StringUniq\".to_string(),\n                    arity: 0,\n                },\n            ],\n        }\n    }\n\n    #[test]\n    fn can_get_all_names_from_mapping() {\n        let mapping = test_mapping();\n        assert_eq!(\n            mapping.all_names(),\n            Vec::from([&\"array\".to_string(), &\"string^\".to_string()])\n        );\n    }\n\n    #[test]\n    fn can_get_col_type_arity_from_mapping() {\n        let mapping = test_mapping();\n\n        assert_eq!(mapping.col_type_arity(\"array\").expect(\"Get array arity\"), 1);\n        assert_eq!(\n            mapping\n                .col_type_arity(\"string^\")\n                .expect(\"Get string^ arity\"),\n            0\n        );\n\n        assert!(mapping.col_type_arity(\"unknown\").is_err());\n    }\n\n    #[test]\n    fn can_get_col_type_field_from_mapping() {\n        let mapping = test_mapping();\n\n        assert_eq!(\n            mapping.col_type_field(\"array\").expect(\"Get array field\"),\n            \"array_null\"\n        );\n\n        assert!(mapping.col_type_field(\"unknown\").is_err());\n    }\n\n    #[test]\n    fn can_get_schema_field_from_mapping() {\n        let mapping = test_mapping();\n\n        assert_eq!(\n            mapping.schema_field(\"string^\").expect(\"Get string^ schema\"),\n            \"string_uniq\"\n        );\n\n        assert!(mapping.schema_field(\"unknown\").is_err());\n    }\n\n    #[test]\n    fn can_get_rust_field_from_mapping() {\n        let mapping = test_mapping();\n\n        assert_eq!(\n            mapping\n                .rust_field(\"string^\")\n                .expect(\"Get string^ rust field\"),\n            \"String\"\n        );\n\n        assert!(mapping.rust_field(\"array\").is_err());\n\n        assert!(mapping.rust_field(\"unknown\").is_err(),);\n    }\n\n    #[test]\n    fn can_get_rust_field_kind_from_mapping() {\n        let mapping = test_mapping();\n\n        assert!(mapping.rust_field_kind(\"string^\").is_ok());\n\n        assert!(mapping.rust_field_kind(\"unknown\").is_err(),);\n    }\n\n    #[test]\n    fn can_get_rust_field_with_params_from_mapping() {\n        let mapping = test_mapping();\n\n        assert_eq!(\n            mapping\n                .rust_field_with_params(\"string^\", &vec![\"string\".to_string()])\n                .expect(\"Get string^ rust field\"),\n            \"String\"\n        );\n\n        assert_eq!(\n            mapping\n                .rust_field_with_params(\"array\", &vec![\"string\".to_string()])\n                .expect(\"Get string^ rust field\"),\n            \"Vec<String>\"\n        );\n        assert!(mapping\n            .rust_field_with_params(\"array\", &vec![\"unknown\".to_string()])\n            .is_err());\n\n        assert!(mapping.rust_field_with_params(\"unknown\", &vec![]).is_err());\n    }\n\n    #[test]\n    fn can_collect_messages() {\n        let gen_result = GenerateResults {\n            rrgen: vec![\n                GenResult::Skipped,\n                GenResult::Generated {\n                    message: Some(\"test\".to_string()),\n                },\n                GenResult::Generated {\n                    message: Some(\"test2\".to_string()),\n                },\n                GenResult::Generated { message: None },\n            ],\n            local_templates: vec![\n                PathBuf::from(\"template\").join(\"scheduler.t\"),\n                PathBuf::from(\"template\").join(\"task.t\"),\n            ],\n        };\n\n        let re = regex::Regex::new(r\"\\x1b\\[[0-9;]*m\").unwrap();\n\n        assert_eq!(\n            re.replace_all(&collect_messages(&gen_result), \"\"),\n            r\"* test\n* test2\n\nThe following templates were sourced from the local templates:\n* template/scheduler.t\n* template/task.t\n\"\n        );\n    }\n}\n"
  },
  {
    "path": "loco-gen/src/mappings.json",
    "content": "{\n  \"field_types\": [\n    {\n      \"name\": \"uuid^\",\n      \"rust\": \"Uuid\",\n      \"schema\": \"uuid_uniq\",\n      \"col_type\": \"UuidUniq\"\n    },\n    {\n      \"name\": \"uuid\",\n      \"rust\": \"Option<Uuid>\",\n      \"schema\": \"uuid_null\",\n      \"col_type\": \"UuidNull\"\n    },\n    {\n      \"name\": \"uuid!\",\n      \"rust\": \"Uuid\",\n      \"schema\": \"uuid\",\n      \"col_type\": \"Uuid\"\n    },\n    {\n      \"name\": \"string\",\n      \"rust\": \"Option<String>\",\n      \"schema\": \"string_null\",\n      \"col_type\": \"StringNull\"\n    },\n    {\n      \"name\": \"string!\",\n      \"rust\": \"String\",\n      \"schema\": \"string\",\n      \"col_type\": \"String\"\n    },\n    {\n      \"name\": \"string^\",\n      \"rust\": \"String\",\n      \"schema\": \"string_uniq\",\n      \"col_type\": \"StringUniq\"\n    },\n    {\n      \"name\": \"text\",\n      \"rust\": \"Option<String>\",\n      \"schema\": \"text_null\",\n      \"col_type\": \"TextNull\"\n    },\n    {\n      \"name\": \"text!\",\n      \"rust\": \"String\",\n      \"schema\": \"text\",\n      \"col_type\": \"Text\"\n    },\n    {\n      \"name\": \"text^\",\n      \"rust\": \"String\",\n      \"schema\": \"text_uniq\",\n      \"col_type\": \"TextUniq\"\n    },\n    {\n      \"name\": \"small_unsigned\",\n      \"rust\": \"Option<i16>\",\n      \"schema\": \"small_unsigned_null\",\n      \"col_type\": \"SmallUnsignedNull\"\n    },\n    {\n      \"name\": \"small_unsigned!\",\n      \"rust\": \"i16\",\n      \"schema\": \"small_unsigned\",\n      \"col_type\": \"SmallUnsigned\"\n    },\n    {\n      \"name\": \"small_unsigned^\",\n      \"rust\": \"i16\",\n      \"schema\": \"small_unsigned_uniq\",\n      \"col_type\": \"SmallUnsignedUniq\"\n    },\n    {\n      \"name\": \"big_unsigned\",\n      \"rust\": \"Option<i64>\",\n      \"schema\": \"big_unsigned_null\",\n      \"col_type\": \"BigUnsignedNull\"\n    },\n    {\n      \"name\": \"big_unsigned!\",\n      \"rust\": \"i64\",\n      \"schema\": \"big_unsigned\",\n      \"col_type\": \"BigUnsigned\"\n    },\n    {\n      \"name\": \"big_unsigned^\",\n      \"rust\": \"i64\",\n      \"schema\": \"big_unsigned_uniq\",\n      \"col_type\": \"BigUnsignedUniq\"\n    },\n    {\n      \"name\": \"small_int\",\n      \"rust\": \"Option<i16>\",\n      \"schema\": \"small_integer_null\",\n      \"col_type\": \"SmallIntegerNull\"\n    },\n    {\n      \"name\": \"small_int!\",\n      \"rust\": \"i16\",\n      \"schema\": \"small_integer\",\n      \"col_type\": \"SmallInteger\"\n    },\n    {\n      \"name\": \"small_int^\",\n      \"rust\": \"i16\",\n      \"schema\": \"small_integer_uniq\",\n      \"col_type\": \"SmallIntegerUniq\"\n    },\n    {\n      \"name\": \"int\",\n      \"rust\": \"Option<i32>\",\n      \"schema\": \"integer_null\",\n      \"col_type\": \"IntegerNull\"\n    },\n    {\n      \"name\": \"int!\",\n      \"rust\": \"i32\",\n      \"schema\": \"integer\",\n      \"col_type\": \"Integer\"\n    },\n    {\n      \"name\": \"int^\",\n      \"rust\": \"i32\",\n      \"schema\": \"integer_uniq\",\n      \"col_type\": \"IntegerUniq\"\n    },\n    {\n      \"name\": \"big_int\",\n      \"rust\": \"Option<i64>\",\n      \"schema\": \"big_integer_null\",\n      \"col_type\": \"BigIntegerNull\"\n    },\n    {\n      \"name\": \"big_int!\",\n      \"rust\": \"i64\",\n      \"schema\": \"big_integer\",\n      \"col_type\": \"BigInteger\"\n    },\n    {\n      \"name\": \"big_int^\",\n      \"rust\": \"i64\",\n      \"schema\": \"big_integer_uniq\",\n      \"col_type\": \"BigIntegerUniq\"\n    },\n    {\n      \"name\": \"float\",\n      \"rust\": \"Option<f32>\",\n      \"schema\": \"float_null\",\n      \"col_type\": \"FloatNull\"\n    },\n    {\n      \"name\": \"float!\",\n      \"rust\": \"f32\",\n      \"schema\": \"float\",\n      \"col_type\": \"Float\"\n    },\n    {\n      \"name\": \"float^\",\n      \"rust\": \"f32\",\n      \"schema\": \"float_uniq\",\n      \"col_type\": \"FloatUniq\"\n    },\n    {\n      \"name\": \"double\",\n      \"rust\": \"Option<f64>\",\n      \"schema\": \"double_null\",\n      \"col_type\": \"DoubleNull\"\n    },\n    {\n      \"name\": \"double!\",\n      \"rust\": \"f64\",\n      \"schema\": \"double\",\n      \"col_type\": \"Double\"\n    },\n    {\n      \"name\": \"double^\",\n      \"rust\": \"f64\",\n      \"schema\": \"double_uniq\",\n      \"col_type\": \"DoubleUniq\"\n    },\n    {\n      \"name\": \"decimal\",\n      \"rust\": \"Option<Decimal>\",\n      \"schema\": \"decimal_null\",\n      \"col_type\": \"DecimalNull\"\n    },\n    {\n      \"name\": \"decimal!\",\n      \"rust\": \"Decimal\",\n      \"schema\": \"decimal\",\n      \"col_type\": \"Decimal\"\n    },\n    {\n      \"name\": \"decimal^\",\n      \"rust\": \"Decimal\",\n      \"schema\": \"decimal_uniq\",\n      \"col_type\": \"DecimalUniq\"\n    },\n    {\n      \"name\": \"decimal_len\",\n      \"rust\": \"Option<Decimal>\",\n      \"schema\": \"decimal_len_null\",\n      \"col_type\": \"DecimalLenNull\",\n      \"arity\": 2\n    },\n    {\n      \"name\": \"decimal_len!\",\n      \"rust\": \"Decimal\",\n      \"schema\": \"decimal_len\",\n      \"col_type\": \"DecimalLen\",\n      \"arity\": 2\n    },\n    {\n      \"name\": \"decimal_len^\",\n      \"rust\": \"Decimal\",\n      \"schema\": \"decimal_len_uniq\",\n      \"col_type\": \"DecimalLenUniq\",\n      \"arity\": 2\n    },\n    {\n      \"name\": \"bool\",\n      \"rust\": \"Option<bool>\",\n      \"schema\": \"boolean_null\",\n      \"col_type\": \"BooleanNull\"\n    },\n    {\n      \"name\": \"bool!\",\n      \"rust\": \"bool\",\n      \"schema\": \"boolean\",\n      \"col_type\": \"Boolean\"\n    },\n    {\n      \"name\": \"tstz\",\n      \"rust\": \"Option<DateTimeWithTimeZone>\",\n      \"schema\": \"timestamp_with_time_zone_null\",\n      \"col_type\": \"TimestampWithTimeZoneNull\"\n    },\n    {\n      \"name\": \"tstz!\",\n      \"rust\": \"DateTimeWithTimeZone\",\n      \"schema\": \"timestamp_with_time_zone\",\n      \"col_type\": \"TimestampWithTimeZone\"\n    },\n    {\n      \"name\": \"date\",\n      \"rust\": \"Option<Date>\",\n      \"schema\": \"date_null\",\n      \"col_type\": \"DateNull\"\n    },\n    {\n      \"name\": \"date!\",\n      \"rust\": \"Date\",\n      \"schema\": \"date\",\n      \"col_type\": \"Date\"\n    },\n    {\n      \"name\": \"date^\",\n      \"rust\": \"Date\",\n      \"schema\": \"date_uniq\",\n      \"col_type\": \"DateUniq\"\n    },\n    {\n      \"name\": \"date_time\",\n      \"rust\": \"Option<DateTime>\",\n      \"schema\": \"date_time_null\",\n      \"col_type\": \"DateTimeNull\"\n    },\n    {\n      \"name\": \"date_time!\",\n      \"rust\": \"DateTime\",\n      \"schema\": \"date_time\",\n      \"col_type\": \"DateTime\"\n    },\n    {\n      \"name\": \"date_time^\",\n      \"rust\": \"DateTime\",\n      \"schema\": \"date_time_uniq\",\n      \"col_type\": \"DateTimeUniq\"\n    },\n    {\n      \"name\": \"json\",\n      \"rust\": \"Option<serde_json::Value>\",\n      \"schema\": \"json_null\",\n      \"col_type\": \"JsonNull\"\n    },\n    {\n      \"name\": \"json!\",\n      \"rust\": \"serde_json::Value\",\n      \"schema\": \"json\",\n      \"col_type\": \"Json\"\n    },\n    {\n      \"name\": \"jsonb\",\n      \"rust\": \"Option<serde_json::Value>\",\n      \"schema\": \"json_binary_null\",\n      \"col_type\": \"JsonBinaryNull\"\n    },\n    {\n      \"name\": \"jsonb!\",\n      \"rust\": \"serde_json::Value\",\n      \"schema\": \"json_binary\",\n      \"col_type\": \"JsonBinary\"\n    },\n    {\n      \"name\": \"jsonb^\",\n      \"rust\": \"serde_json::Value\",\n      \"schema\": \"json_binary_uniq\",\n      \"col_type\": \"JsonBinaryUniq\"\n    },\n    {\n      \"name\": \"blob\",\n      \"rust\": \"Option<Vec<u8>>\",\n      \"schema\": \"blob_null\",\n      \"col_type\": \"BlobNull\"\n    },\n    {\n      \"name\": \"blob!\",\n      \"rust\": \"Vec<u8>\",\n      \"schema\": \"blob\",\n      \"col_type\": \"Blob\"\n    },\n    {\n      \"name\": \"blob^\",\n      \"rust\": \"Vec<u8>\",\n      \"schema\": \"blob_uniq\",\n      \"col_type\": \"BlobUniq\"\n    },\n    {\n      \"name\": \"money\",\n      \"rust\": \"Option<Decimal>\",\n      \"schema\": \"money_null\",\n      \"col_type\": \"MoneyNull\"\n    },\n    {\n      \"name\": \"money!\",\n      \"rust\": \"Decimal\",\n      \"schema\": \"money\",\n      \"col_type\": \"Money\"\n    },\n    {\n      \"name\": \"money^\",\n      \"rust\": \"Decimal\",\n      \"schema\": \"money_uniq\",\n      \"col_type\": \"MoneyUniq\"\n    },\n    {\n      \"name\": \"unsigned!\",\n      \"rust\": \"i32\",\n      \"schema\": \"unsigned\",\n      \"col_type\": \"Unsigned\"\n    },\n    {\n      \"name\": \"unsigned\",\n      \"rust\": \"Option<i32>\",\n      \"schema\": \"unsigned_null\",\n      \"col_type\": \"UnsignedNull\"\n    },\n    {\n      \"name\": \"unsigned^\",\n      \"rust\": \"i32\",\n      \"schema\": \"unsigned_uniq\",\n      \"col_type\": \"UnsignedUniq\"\n    },\n    {\n      \"name\": \"binary_len!\",\n      \"rust\": \"Vec<u8>\",\n      \"schema\": \"binary_len\",\n      \"col_type\": \"BinaryLen\",\n      \"arity\": 1\n    },\n    {\n      \"name\": \"binary_len\",\n      \"rust\": \"Option<Vec<u8>>\",\n      \"schema\": \"binary_len_null\",\n      \"col_type\": \"BinaryLenNull\",\n      \"arity\": 1\n    },\n    {\n      \"name\": \"binary_len^\",\n      \"rust\": \"Vec<u8>\",\n      \"schema\": \"binary_len_uniq\",\n      \"col_type\": \"BinaryLenUniq\",\n      \"arity\": 1\n    },\n    {\n      \"name\": \"var_binary!\",\n      \"rust\": \"Vec<u8>\",\n      \"schema\": \"var_binary\",\n      \"col_type\": \"VarBinary\",\n      \"arity\": 1\n    },\n    {\n      \"name\": \"var_binary\",\n      \"rust\": \"Option<Vec<u8>>\",\n      \"schema\": \"var_binary_null\",\n      \"col_type\": \"VarBinaryNull\",\n      \"arity\": 1\n    },\n    {\n      \"name\": \"var_binary^\",\n      \"rust\": \"Vec<u8>\",\n      \"schema\": \"var_binary_uniq\",\n      \"col_type\": \"VarBinaryUniq\",\n      \"arity\": 1\n    },\n    {\n      \"name\": \"array!\",\n      \"rust\": {\n        \"string\": \"Option<Vec<String>>\",\n        \"int\": \"Option<Vec<i32>>\",\n        \"big_int\": \"Option<Vec<i64>>\",\n        \"float\": \"Option<Vec<f32>>\",\n        \"double\": \"Option<Vec<f64>>\",\n        \"bool\": \"Option<Vec<bool>>\"\n      },\n      \"schema\": \"array\",\n      \"col_type\": \"array\",\n      \"arity\": 1\n    },\n    {\n      \"name\": \"array\",\n      \"rust\": {\n        \"string\": \"Option<Vec<String>>\",\n        \"int\": \"Option<Vec<i32>>\",\n        \"big_int\": \"Option<Vec<i64>>\",\n        \"float\": \"Option<Vec<f32>>\",\n        \"double\": \"Option<Vec<f64>>\",\n        \"bool\": \"Option<Vec<bool>>\"\n      },\n      \"schema\": \"array\",\n      \"col_type\": \"array_null\",\n      \"arity\": 1\n    },\n    {\n      \"name\": \"array^\",\n      \"rust\": {\n        \"string\": \"Option<Vec<String>>\",\n        \"int\": \"Option<Vec<i32>>\",\n        \"big_int\": \"Option<Vec<i64>>\",\n        \"float\": \"Option<Vec<f32>>\",\n        \"double\": \"Option<Vec<f64>>\",\n        \"bool\": \"Option<Vec<bool>>\"\n      },\n      \"schema\": \"array\",\n      \"col_type\": \"array_uniq\",\n      \"arity\": 1\n    }\n  ]\n}"
  },
  {
    "path": "loco-gen/src/migration.rs",
    "content": "use std::path::Path;\n\nuse chrono::Utc;\nuse rrgen::RRgen;\nuse serde_json::json;\n\nuse crate::{\n    infer, model::get_columns_and_references, render_template, AppInfo, GenerateResults, Result,\n};\n\n/// skipping some fields from the generated models.\n/// For example, the `created_at` and `updated_at` fields are automatically\n/// generated by the Loco app and should be given\npub const IGNORE_FIELDS: &[&str] = &[\"created_at\", \"updated_at\", \"create_at\", \"update_at\"];\n\npub fn generate(\n    rrgen: &RRgen,\n    name: &str,\n    with_tz: bool,\n    fields: &[(String, String)],\n    appinfo: &AppInfo,\n) -> Result<GenerateResults> {\n    let pkg_name: &str = &appinfo.app_name;\n    let ts = Utc::now();\n\n    let res = infer::guess_migration_type(name);\n    match res {\n        // NOTE: re-uses the 'new model' migration template!\n        infer::MigrationType::CreateTable { table } => {\n            let (columns, references) = get_columns_and_references(fields)?;\n            let vars = json!({\"name\": table, \"ts\": ts, \"with_tz\": with_tz,\"pkg_name\": pkg_name, \"is_link\": false, \"columns\": columns, \"references\": references});\n            render_template(rrgen, Path::new(\"model/model.t\"), &vars)\n        }\n        infer::MigrationType::AddColumns { table } => {\n            let (columns, references) = get_columns_and_references(fields)?;\n            let vars = json!({\"name\": name, \"table\": table, \"ts\": ts, \"pkg_name\": pkg_name, \"is_link\": false, \"columns\": columns, \"references\": references});\n            render_template(rrgen, Path::new(\"migration/add_columns.t\"), &vars)\n        }\n        infer::MigrationType::RemoveColumns { table } => {\n            let (columns, _references) = get_columns_and_references(fields)?;\n            let vars = json!({\"name\": name, \"table\": table, \"ts\": ts, \"pkg_name\": pkg_name, \"columns\": columns});\n            render_template(rrgen, Path::new(\"migration/remove_columns.t\"), &vars)\n        }\n        infer::MigrationType::AddReference { table } => {\n            let (columns, references) = get_columns_and_references(fields)?;\n            let vars = json!({\"name\": name, \"table\": table, \"ts\": ts, \"pkg_name\": pkg_name, \"columns\": columns, \"references\": references});\n            render_template(rrgen, Path::new(\"migration/add_references.t\"), &vars)\n        }\n        infer::MigrationType::CreateJoinTable { table_a, table_b } => {\n            let table = format!(\"{table_a}_{table_b}\");\n            let (columns, extra_references) = get_columns_and_references(fields)?;\n\n            let references = [(table_a, String::new()), (table_b, String::new())]\n                .into_iter()\n                .chain(extra_references)\n                .collect::<Vec<_>>();\n\n            let vars = json!({\"name\": name, \"table\": table, \"ts\": ts, \"pkg_name\": pkg_name, \"columns\": columns, \"references\": references});\n            render_template(rrgen, Path::new(\"migration/join_table.t\"), &vars)\n        }\n        infer::MigrationType::Empty => {\n            let vars = json!({\"name\": name, \"ts\": ts, \"pkg_name\": pkg_name});\n            render_template(rrgen, Path::new(\"migration/empty.t\"), &vars)\n        }\n    }\n}\n"
  },
  {
    "path": "loco-gen/src/model.rs",
    "content": "use std::{collections::HashMap, env::current_dir, path::Path};\n\nuse chrono::Utc;\nuse duct::cmd;\nuse heck::ToUpperCamelCase;\nuse rrgen::RRgen;\nuse serde_json::json;\n\nuse crate::{\n    get_mappings, infer::parse_field_type, render_template, AppInfo, Error, GenerateResults, Result,\n};\n\n/// skipping some fields from the generated models.\n/// For example, the `created_at` and `updated_at` fields are automatically\n/// generated by the Loco app and should be given\npub const IGNORE_FIELDS: &[&str] = &[\"created_at\", \"updated_at\", \"create_at\", \"update_at\"];\n\n/// columns are <name>, <dbtype>: (\"content\", \"string\")\n/// references are <to table, id col in from table>: (\"user\", `user_id`)\n///  parsed from e.g.: model article content:string user:references\n///  puts a `user_id` in articles, then fk to users\n#[allow(clippy::type_complexity)]\npub fn get_columns_and_references(\n    fields: &[(String, String)],\n) -> Result<(Vec<(String, String)>, Vec<(String, String)>)> {\n    let mut columns = Vec::new();\n    let mut references = Vec::new();\n    for (fname, ftype) in fields {\n        if IGNORE_FIELDS.contains(&fname.as_str()) {\n            tracing::warn!(\n                field = fname,\n                \"note that a redundant field was specified, it is already generated automatically\"\n            );\n            continue;\n        }\n        let field_type = parse_field_type(ftype)?;\n        match field_type {\n            crate::infer::FieldType::Reference => {\n                // (users, \"\")\n                references.push((fname.clone(), String::new()));\n            }\n            crate::infer::FieldType::ReferenceWithCustomField(refname) => {\n                references.push((fname.clone(), refname.clone()));\n            }\n            crate::infer::FieldType::NullableReference => {\n                references.push((format!(\"{fname}?\"), String::new()));\n            }\n            crate::infer::FieldType::NullableReferenceWithCustomField(refname) => {\n                references.push((format!(\"{fname}?\"), refname.clone()));\n            }\n            crate::infer::FieldType::Type(ftype) => {\n                let mappings = get_mappings();\n                let col_type = mappings.col_type_field(ftype.as_str())?;\n                columns.push((fname.clone(), col_type.to_string()));\n            }\n            crate::infer::FieldType::TypeWithParameters(ftype, params) => {\n                let mappings = get_mappings();\n                let col_type = mappings.col_type_field(ftype.as_str())?;\n                let arity = mappings.col_type_arity(ftype.as_str()).unwrap_or_default();\n                if params.len() != arity {\n                    return Err(Error::Message(format!(\n                        \"type: `{ftype}` requires specifying {arity} parameters, but only {} were \\\n                         given (`{}`).\",\n                        params.len(),\n                        params.join(\",\")\n                    )));\n                }\n\n                let col = match ftype.as_ref() {\n                    \"array\" | \"array^\" | \"array!\" => {\n                        let array_kind = match params.as_slice() {\n                            [array_kind] => Ok(array_kind),\n                            _ => Err(Error::Message(format!(\n                                    \"type: `{ftype}` requires exactly {arity} parameter{}, but {} were given (`{}`).\",\n                                    if arity == 1 { \"\" } else { \"s\" },\n                                    params.len(),\n                                    params.join(\",\")\n                                ))),\n                        }?;\n\n                        format!(\n                            r\"{}(ArrayColType::{})\",\n                            col_type,\n                            array_kind.to_upper_camel_case()\n                        )\n                    }\n                    &_ => {\n                        format!(\"{}({})\", col_type, params.join(\",\"))\n                    }\n                };\n\n                columns.push((fname.clone(), col));\n            }\n        }\n    }\n    Ok((columns, references))\n}\n\npub fn generate(\n    rrgen: &RRgen,\n    name: &str,\n    with_tz: bool,\n    fields: &[(String, String)],\n    appinfo: &AppInfo,\n) -> Result<GenerateResults> {\n    let pkg_name: &str = &appinfo.app_name;\n    let ts = Utc::now();\n\n    let (columns, references) = get_columns_and_references(fields)?;\n\n    let vars = json!({\"name\": name, \"ts\": ts, \"with_tz\": with_tz,\"pkg_name\": pkg_name, \"columns\": columns, \"references\": references});\n    let gen_result = render_template(rrgen, Path::new(\"model\"), &vars)?;\n\n    if std::env::var(\"SKIP_MIGRATION\").is_err() {\n        // generate the model files by migrating and re-running seaorm\n        let cwd = current_dir()?;\n        let env_map: HashMap<_, _> = std::env::vars().collect();\n\n        let _ = cmd!(\"cargo\", \"loco-tool\", \"db\", \"migrate\",)\n            .stderr_to_stdout()\n            .dir(cwd.as_path())\n            .full_env(&env_map)\n            .run()\n            .map_err(|err| {\n                Error::Message(format!(\n                    \"failed to run loco db migration. error details: `{err}`\",\n                ))\n            })?;\n        let _ = cmd!(\"cargo\", \"loco-tool\", \"db\", \"entities\",)\n            .stderr_to_stdout()\n            .dir(cwd.as_path())\n            .full_env(&env_map)\n            .run()\n            .map_err(|err| {\n                Error::Message(format!(\n                    \"failed to run loco db entities. error details: `{err}`\",\n                ))\n            })?;\n    }\n\n    Ok(gen_result)\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    fn to_field(name: &str, field_type: &str) -> (String, String) {\n        (name.to_string(), field_type.to_string())\n    }\n\n    #[test]\n    fn test_get_columns_with_field_types() {\n        let fields = [\n            to_field(\"expect_string_null\", \"string\"),\n            to_field(\"expect_string\", \"string!\"),\n            to_field(\"expect_unique\", \"string^\"),\n        ];\n        let res = get_columns_and_references(&fields).expect(\"Failed to parse fields\");\n\n        let expected_columns = vec![\n            to_field(\"expect_string_null\", \"StringNull\"),\n            to_field(\"expect_string\", \"String\"),\n            to_field(\"expect_unique\", \"StringUniq\"),\n        ];\n        let expected_references: Vec<(String, String)> = vec![];\n\n        assert_eq!(res, (expected_columns, expected_references));\n    }\n    #[test]\n    fn test_get_columns_with_array_types() {\n        let fields = [\n            to_field(\"expect_array_null\", \"array:string\"),\n            to_field(\"expect_array\", \"array!:string\"),\n            to_field(\"expect_array_uniq\", \"array^:string\"),\n        ];\n        let res = get_columns_and_references(&fields).expect(\"Failed to parse fields\");\n\n        let expected_columns = vec![\n            to_field(\"expect_array_null\", \"array_null(ArrayColType::String)\"),\n            to_field(\"expect_array\", \"array(ArrayColType::String)\"),\n            to_field(\"expect_array_uniq\", \"array_uniq(ArrayColType::String)\"),\n        ];\n        let expected_references: Vec<(String, String)> = vec![];\n\n        assert_eq!(res, (expected_columns, expected_references));\n    }\n\n    #[test]\n    fn test_get_references_from_fields() {\n        let fields = [\n            to_field(\"user\", \"references\"),\n            to_field(\"post\", \"references\"),\n        ];\n        let res = get_columns_and_references(&fields).expect(\"Failed to parse fields\");\n\n        let expected_columns: Vec<(String, String)> = vec![];\n        let expected_references = vec![to_field(\"user\", \"\"), to_field(\"post\", \"\")];\n\n        assert_eq!(res, (expected_columns, expected_references));\n    }\n\n    #[test]\n    fn test_ignore_fields_are_filtered_out() {\n        let mut fields = vec![to_field(\"name\", \"string\")];\n\n        for ignore_field in IGNORE_FIELDS {\n            fields.push(to_field(ignore_field, \"string\"));\n        }\n\n        let res = get_columns_and_references(&fields).expect(\"Failed to parse fields\");\n\n        let expected_columns = vec![to_field(\"name\", \"StringNull\")];\n        let expected_references: Vec<(String, String)> = vec![];\n\n        assert_eq!(res, (expected_columns, expected_references));\n    }\n\n    #[test]\n    fn validate_arity() {\n        // field not expected arity, but given 2\n        let fields = vec![to_field(\"name\", \"string:2\")];\n        let res = get_columns_and_references(&fields);\n        if let Err(err) = res {\n            assert_eq!(\n                err.to_string(),\n                \"type: `string` requires specifying 0 parameters, but only 1 were given (`2`).\"\n            );\n        } else {\n            panic!(\"Expected Err, but got Ok: {res:?}\");\n        }\n\n        // references not expected arity, but given 2\n        let references = vec![to_field(\"post:2\", \"\")];\n        let res = get_columns_and_references(&references);\n        if let Err(err) = res {\n            let mappings = get_mappings();\n            assert_eq!(\n                err.to_string(),\n                mappings.error_unrecognized_default_field(\"\").to_string()\n            );\n        } else {\n            panic!(\"Expected Err, but got Ok: {res:?}\");\n        }\n    }\n}\n"
  },
  {
    "path": "loco-gen/src/scaffold.rs",
    "content": "use std::path::Path;\n\nuse rrgen::RRgen;\nuse serde_json::json;\n\nuse crate::{\n    get_mappings, infer::parse_field_type, model, render_template, AppInfo, Error, GenerateResults,\n    Result, ScaffoldKind,\n};\n\npub fn generate(\n    rrgen: &RRgen,\n    name: &str,\n    with_tz: bool,\n    fields: &[(String, String)],\n    kind: &ScaffoldKind,\n    appinfo: &AppInfo,\n) -> Result<GenerateResults> {\n    // - scaffold is never a link table\n    // - never run with migration_only, because the controllers will refer to the\n    //   models. the models only arrive after migration and entities sync.\n    let mut gen_result = model::generate(rrgen, name, with_tz, fields, appinfo)?;\n\n    let mut columns = Vec::new();\n    for (fname, ftype) in fields {\n        if model::IGNORE_FIELDS.contains(&fname.as_str()) {\n            tracing::warn!(\n                field = fname,\n                \"note that a redundant field was specified, it is already generated automatically\"\n            );\n            continue;\n        }\n\n        let field_type = parse_field_type(ftype)?;\n        match field_type {\n            crate::infer::FieldType::Reference => {\n                let col_name = format!(\"{fname}_id\");\n                columns.push((col_name, \"i32\".to_string(), \"Integer\".to_string()));\n            }\n            crate::infer::FieldType::ReferenceWithCustomField(refname) => {\n                columns.push((refname.clone(), \"i32\".to_string(), \"Integer\".to_string()));\n            }\n            crate::infer::FieldType::NullableReference => {\n                let col_name = format!(\"{fname}_id\");\n                columns.push((col_name, \"i32\".to_string(), \"IntegerNull\".to_string()));\n            }\n            crate::infer::FieldType::NullableReferenceWithCustomField(refname) => {\n                columns.push((\n                    refname.clone(),\n                    \"i32\".to_string(),\n                    \"IntegerNull\".to_string(),\n                ));\n            }\n            crate::infer::FieldType::Type(ftype) => {\n                let mappings = get_mappings();\n                let rust_type = mappings.rust_field(ftype.as_str())?;\n                columns.push((fname.clone(), rust_type.to_string(), ftype));\n            }\n            crate::infer::FieldType::TypeWithParameters(ftype, params) => {\n                let mappings = get_mappings();\n                let rust_type = mappings.rust_field_with_params(ftype.as_str(), &params)?;\n                let arity = mappings.col_type_arity(ftype.as_str()).unwrap_or_default();\n                if params.len() != arity {\n                    return Err(Error::Message(format!(\n                        \"type: `{ftype}` requires specifying {arity} parameters, but only {} were \\\n                         given (`{}`).\",\n                        params.len(),\n                        params.join(\",\")\n                    )));\n                }\n\n                columns.push((fname.clone(), rust_type.to_string(), ftype));\n            }\n        }\n    }\n\n    let vars = json!({\"name\": name, \"columns\": columns, \"pkg_name\": appinfo.app_name});\n    match kind {\n        ScaffoldKind::Api => {\n            let res = render_template(rrgen, Path::new(\"scaffold/api\"), &vars)?;\n            gen_result.rrgen.extend(res.rrgen);\n            gen_result.local_templates.extend(res.local_templates);\n        }\n        ScaffoldKind::Html => {\n            let res = render_template(rrgen, Path::new(\"scaffold/html\"), &vars)?;\n            gen_result.rrgen.extend(res.rrgen);\n            gen_result.local_templates.extend(res.local_templates);\n        }\n        ScaffoldKind::Htmx => {\n            let res = render_template(rrgen, Path::new(\"scaffold/htmx\"), &vars)?;\n            gen_result.rrgen.extend(res.rrgen);\n            gen_result.local_templates.extend(res.local_templates);\n        }\n    }\n    Ok(gen_result)\n}\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_array!_big_int].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">array!</label>\n    <button type=\"button\" class=\"text-xs py-1 px-3 rounded-lg bg-gray-900 text-white add-more\" data-group=\"array!\">Add More</button>\n    <div id=\"array!-inputs\" class=\"space-y-2\">\n    {% if item.array! %}\n        {% for val in item.array! %}\n            <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-9223372036854775808\" max=\"9223372036854775807\" id=\"array!\" name=\"array!\" type=\"number\" value=\"{{val}}\" required custom_type=\"array\" />\n        {% endfor -%}\n    {%- else -%}\n        <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-9223372036854775808\" max=\"9223372036854775807\" id=\"array!\" name=\"array!\" type=\"number\" value=\"\" required custom_type=\"array\" />\n    {%- endif -%}\n    </div>\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">array!</label>\n    <button type=\"button\" class=\"text-xs py-1 px-3 rounded-lg bg-gray-900 text-white add-more\" data-group=\"array!\">Add More</button>\n    <div id=\"array!-inputs\" class=\"space-y-2\">\n    {% if item.array! %}\n        {% for val in item.array! %}\n            <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-9223372036854775808\" max=\"9223372036854775807\" id=\"array!\" name=\"array!\" type=\"number\" value=\"{{val}}\" required custom_type=\"array\" />\n        {% endfor -%}\n    {%- else -%}\n        <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-9223372036854775808\" max=\"9223372036854775807\" id=\"array!\" name=\"array!\" type=\"number\" value=\"{{item.array!}}\" required custom_type=\"array\" />\n    {%- endif -%}\n    </div>\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_array!_bool].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">array!</label>\n    \n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">array!</label>\n    \n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_array!_double].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">array!</label>\n    <button type=\"button\" class=\"text-xs py-1 px-3 rounded-lg bg-gray-900 text-white add-more\" data-group=\"array!\">Add More</button>\n    <div id=\"array!-inputs\" class=\"space-y-2\">\n    {% if item.array! %}\n        {% for val in item.array! %}\n            <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\" max=\"179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\" id=\"array!\" name=\"array!\" type=\"number\" value=\"{{val}}\" required custom_type=\"array\" />\n        {% endfor -%}\n    {%- else -%}\n        <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\" max=\"179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\" id=\"array!\" name=\"array!\" type=\"number\" value=\"\" required custom_type=\"array\" />\n    {%- endif -%}\n    </div>\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">array!</label>\n    <button type=\"button\" class=\"text-xs py-1 px-3 rounded-lg bg-gray-900 text-white add-more\" data-group=\"array!\">Add More</button>\n    <div id=\"array!-inputs\" class=\"space-y-2\">\n    {% if item.array! %}\n        {% for val in item.array! %}\n            <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\" max=\"179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\" id=\"array!\" name=\"array!\" type=\"number\" value=\"{{val}}\" required custom_type=\"array\" />\n        {% endfor -%}\n    {%- else -%}\n        <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\" max=\"179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\" id=\"array!\" name=\"array!\" type=\"number\" value=\"{{item.array!}}\" required custom_type=\"array\" />\n    {%- endif -%}\n    </div>\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_array!_float].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">array!</label>\n    <button type=\"button\" class=\"text-xs py-1 px-3 rounded-lg bg-gray-900 text-white add-more\" data-group=\"array!\">Add More</button>\n    <div id=\"array!-inputs\" class=\"space-y-2\">\n    {% if item.array! %}\n        {% for val in item.array! %}\n            <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-340282350000000000000000000000000000000\" max=\"340282350000000000000000000000000000000\" id=\"array!\" name=\"array!\" type=\"number\" value=\"{{val}}\" required custom_type=\"array\" step=\"0.1\" />\n        {% endfor -%}\n    {%- else -%}\n        <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-340282350000000000000000000000000000000\" max=\"340282350000000000000000000000000000000\" id=\"array!\" name=\"array!\" type=\"number\" value=\"\" required custom_type=\"array\" step=\"0.1\" />\n    {%- endif -%}\n    </div>\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">array!</label>\n    <button type=\"button\" class=\"text-xs py-1 px-3 rounded-lg bg-gray-900 text-white add-more\" data-group=\"array!\">Add More</button>\n    <div id=\"array!-inputs\" class=\"space-y-2\">\n    {% if item.array! %}\n        {% for val in item.array! %}\n            <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-340282350000000000000000000000000000000\" max=\"340282350000000000000000000000000000000\" id=\"array!\" name=\"array!\" type=\"number\" value=\"{{val}}\" required custom_type=\"array\" step=\"0.1\" />\n        {% endfor -%}\n    {%- else -%}\n        <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-340282350000000000000000000000000000000\" max=\"340282350000000000000000000000000000000\" id=\"array!\" name=\"array!\" type=\"number\" value=\"{{item.array!}}\" required custom_type=\"array\" step=\"0.1\" />\n    {%- endif -%}\n    </div>\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_array!_int].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">array!</label>\n    <button type=\"button\" class=\"text-xs py-1 px-3 rounded-lg bg-gray-900 text-white add-more\" data-group=\"array!\">Add More</button>\n    <div id=\"array!-inputs\" class=\"space-y-2\">\n    {% if item.array! %}\n        {% for val in item.array! %}\n            <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-2147483648\" max=\"2147483647\" id=\"array!\" name=\"array!\" type=\"number\" value=\"{{val}}\" required custom_type=\"array\" />\n        {% endfor -%}\n    {%- else -%}\n        <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-2147483648\" max=\"2147483647\" id=\"array!\" name=\"array!\" type=\"number\" value=\"\" required custom_type=\"array\" />\n    {%- endif -%}\n    </div>\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">array!</label>\n    <button type=\"button\" class=\"text-xs py-1 px-3 rounded-lg bg-gray-900 text-white add-more\" data-group=\"array!\">Add More</button>\n    <div id=\"array!-inputs\" class=\"space-y-2\">\n    {% if item.array! %}\n        {% for val in item.array! %}\n            <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-2147483648\" max=\"2147483647\" id=\"array!\" name=\"array!\" type=\"number\" value=\"{{val}}\" required custom_type=\"array\" />\n        {% endfor -%}\n    {%- else -%}\n        <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-2147483648\" max=\"2147483647\" id=\"array!\" name=\"array!\" type=\"number\" value=\"{{item.array!}}\" required custom_type=\"array\" />\n    {%- endif -%}\n    </div>\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_array!_string].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">array!</label>\n    <button type=\"button\" class=\"text-xs py-1 px-3 rounded-lg bg-gray-900 text-white add-more\" data-group=\"array!\">Add More</button>\n    <div id=\"array!-inputs\" class=\"space-y-2\">\n    {% if item.array! %}\n        {% for val in item.array! %}\n            <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" name=\"array!\" type=\"text\" value=\"{{val}}\" required custom_type=\"array\"/>\n        {% endfor -%}\n    {%- else -%}\n        <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" name=\"array!\" type=\"text\" value=\"\" required custom_type=\"array\"/>\n    {%- endif -%}\n    </div>\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">array!</label>\n    <button type=\"button\" class=\"text-xs py-1 px-3 rounded-lg bg-gray-900 text-white add-more\" data-group=\"array!\">Add More</button>\n    <div id=\"array!-inputs\" class=\"space-y-2\">\n    {% if item.array! %}\n        {% for val in item.array! %}\n            <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" name=\"array!\" type=\"text\" value=\"{{val}}\" required custom_type=\"array\"/>\n        {% endfor -%}\n    {%- else -%}\n        <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" name=\"array!\" type=\"text\" value=\"{{item.array!}}\" required custom_type=\"array\"/>\n    {%- endif -%}\n    </div>\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_array^_big_int].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">array^</label>\n    <button type=\"button\" class=\"text-xs py-1 px-3 rounded-lg bg-gray-900 text-white add-more\" data-group=\"array^\">Add More</button>\n    <div id=\"array^-inputs\" class=\"space-y-2\">\n    {% if item.array^ %}\n        {% for val in item.array^ %}\n            <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-9223372036854775808\" max=\"9223372036854775807\" id=\"array^\" name=\"array^\" type=\"number\" value=\"{{val}}\" required custom_type=\"array\" />\n        {% endfor -%}\n    {%- else -%}\n        <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-9223372036854775808\" max=\"9223372036854775807\" id=\"array^\" name=\"array^\" type=\"number\" value=\"\" required custom_type=\"array\" />\n    {%- endif -%}\n    </div>\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">array^</label>\n    <button type=\"button\" class=\"text-xs py-1 px-3 rounded-lg bg-gray-900 text-white add-more\" data-group=\"array^\">Add More</button>\n    <div id=\"array^-inputs\" class=\"space-y-2\">\n    {% if item.array^ %}\n        {% for val in item.array^ %}\n            <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-9223372036854775808\" max=\"9223372036854775807\" id=\"array^\" name=\"array^\" type=\"number\" value=\"{{val}}\" required custom_type=\"array\" />\n        {% endfor -%}\n    {%- else -%}\n        <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-9223372036854775808\" max=\"9223372036854775807\" id=\"array^\" name=\"array^\" type=\"number\" value=\"{{item.array^}}\" required custom_type=\"array\" />\n    {%- endif -%}\n    </div>\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_array^_bool].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">array^</label>\n    \n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">array^</label>\n    \n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_array^_double].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">array^</label>\n    <button type=\"button\" class=\"text-xs py-1 px-3 rounded-lg bg-gray-900 text-white add-more\" data-group=\"array^\">Add More</button>\n    <div id=\"array^-inputs\" class=\"space-y-2\">\n    {% if item.array^ %}\n        {% for val in item.array^ %}\n            <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\" max=\"179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\" id=\"array^\" name=\"array^\" type=\"number\" value=\"{{val}}\" required custom_type=\"array\" />\n        {% endfor -%}\n    {%- else -%}\n        <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\" max=\"179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\" id=\"array^\" name=\"array^\" type=\"number\" value=\"\" required custom_type=\"array\" />\n    {%- endif -%}\n    </div>\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">array^</label>\n    <button type=\"button\" class=\"text-xs py-1 px-3 rounded-lg bg-gray-900 text-white add-more\" data-group=\"array^\">Add More</button>\n    <div id=\"array^-inputs\" class=\"space-y-2\">\n    {% if item.array^ %}\n        {% for val in item.array^ %}\n            <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\" max=\"179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\" id=\"array^\" name=\"array^\" type=\"number\" value=\"{{val}}\" required custom_type=\"array\" />\n        {% endfor -%}\n    {%- else -%}\n        <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\" max=\"179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\" id=\"array^\" name=\"array^\" type=\"number\" value=\"{{item.array^}}\" required custom_type=\"array\" />\n    {%- endif -%}\n    </div>\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_array^_float].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">array^</label>\n    <button type=\"button\" class=\"text-xs py-1 px-3 rounded-lg bg-gray-900 text-white add-more\" data-group=\"array^\">Add More</button>\n    <div id=\"array^-inputs\" class=\"space-y-2\">\n    {% if item.array^ %}\n        {% for val in item.array^ %}\n            <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-340282350000000000000000000000000000000\" max=\"340282350000000000000000000000000000000\" id=\"array^\" name=\"array^\" type=\"number\" value=\"{{val}}\" required custom_type=\"array\" step=\"0.1\" />\n        {% endfor -%}\n    {%- else -%}\n        <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-340282350000000000000000000000000000000\" max=\"340282350000000000000000000000000000000\" id=\"array^\" name=\"array^\" type=\"number\" value=\"\" required custom_type=\"array\" step=\"0.1\" />\n    {%- endif -%}\n    </div>\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">array^</label>\n    <button type=\"button\" class=\"text-xs py-1 px-3 rounded-lg bg-gray-900 text-white add-more\" data-group=\"array^\">Add More</button>\n    <div id=\"array^-inputs\" class=\"space-y-2\">\n    {% if item.array^ %}\n        {% for val in item.array^ %}\n            <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-340282350000000000000000000000000000000\" max=\"340282350000000000000000000000000000000\" id=\"array^\" name=\"array^\" type=\"number\" value=\"{{val}}\" required custom_type=\"array\" step=\"0.1\" />\n        {% endfor -%}\n    {%- else -%}\n        <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-340282350000000000000000000000000000000\" max=\"340282350000000000000000000000000000000\" id=\"array^\" name=\"array^\" type=\"number\" value=\"{{item.array^}}\" required custom_type=\"array\" step=\"0.1\" />\n    {%- endif -%}\n    </div>\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_array^_int].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">array^</label>\n    <button type=\"button\" class=\"text-xs py-1 px-3 rounded-lg bg-gray-900 text-white add-more\" data-group=\"array^\">Add More</button>\n    <div id=\"array^-inputs\" class=\"space-y-2\">\n    {% if item.array^ %}\n        {% for val in item.array^ %}\n            <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-2147483648\" max=\"2147483647\" id=\"array^\" name=\"array^\" type=\"number\" value=\"{{val}}\" required custom_type=\"array\" />\n        {% endfor -%}\n    {%- else -%}\n        <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-2147483648\" max=\"2147483647\" id=\"array^\" name=\"array^\" type=\"number\" value=\"\" required custom_type=\"array\" />\n    {%- endif -%}\n    </div>\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">array^</label>\n    <button type=\"button\" class=\"text-xs py-1 px-3 rounded-lg bg-gray-900 text-white add-more\" data-group=\"array^\">Add More</button>\n    <div id=\"array^-inputs\" class=\"space-y-2\">\n    {% if item.array^ %}\n        {% for val in item.array^ %}\n            <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-2147483648\" max=\"2147483647\" id=\"array^\" name=\"array^\" type=\"number\" value=\"{{val}}\" required custom_type=\"array\" />\n        {% endfor -%}\n    {%- else -%}\n        <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-2147483648\" max=\"2147483647\" id=\"array^\" name=\"array^\" type=\"number\" value=\"{{item.array^}}\" required custom_type=\"array\" />\n    {%- endif -%}\n    </div>\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_array^_string].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">array^</label>\n    <button type=\"button\" class=\"text-xs py-1 px-3 rounded-lg bg-gray-900 text-white add-more\" data-group=\"array^\">Add More</button>\n    <div id=\"array^-inputs\" class=\"space-y-2\">\n    {% if item.array^ %}\n        {% for val in item.array^ %}\n            <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" name=\"array^\" type=\"text\" value=\"{{val}}\" required custom_type=\"array\"/>\n        {% endfor -%}\n    {%- else -%}\n        <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" name=\"array^\" type=\"text\" value=\"\" required custom_type=\"array\"/>\n    {%- endif -%}\n    </div>\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">array^</label>\n    <button type=\"button\" class=\"text-xs py-1 px-3 rounded-lg bg-gray-900 text-white add-more\" data-group=\"array^\">Add More</button>\n    <div id=\"array^-inputs\" class=\"space-y-2\">\n    {% if item.array^ %}\n        {% for val in item.array^ %}\n            <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" name=\"array^\" type=\"text\" value=\"{{val}}\" required custom_type=\"array\"/>\n        {% endfor -%}\n    {%- else -%}\n        <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" name=\"array^\" type=\"text\" value=\"{{item.array^}}\" required custom_type=\"array\"/>\n    {%- endif -%}\n    </div>\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_array_big_int].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">array</label>\n    <button type=\"button\" class=\"text-xs py-1 px-3 rounded-lg bg-gray-900 text-white add-more\" data-group=\"array\">Add More</button>\n    <div id=\"array-inputs\" class=\"space-y-2\">\n    {% if item.array %}\n        {% for val in item.array %}\n            <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-9223372036854775808\" max=\"9223372036854775807\" id=\"array\" name=\"array\" type=\"number\" value=\"{{val}}\"  custom_type=\"array\" />\n        {% endfor -%}\n    {%- else -%}\n        <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-9223372036854775808\" max=\"9223372036854775807\" id=\"array\" name=\"array\" type=\"number\" value=\"\"  custom_type=\"array\" />\n    {%- endif -%}\n    </div>\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">array</label>\n    <button type=\"button\" class=\"text-xs py-1 px-3 rounded-lg bg-gray-900 text-white add-more\" data-group=\"array\">Add More</button>\n    <div id=\"array-inputs\" class=\"space-y-2\">\n    {% if item.array %}\n        {% for val in item.array %}\n            <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-9223372036854775808\" max=\"9223372036854775807\" id=\"array\" name=\"array\" type=\"number\" value=\"{{val}}\"  custom_type=\"array\" />\n        {% endfor -%}\n    {%- else -%}\n        <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-9223372036854775808\" max=\"9223372036854775807\" id=\"array\" name=\"array\" type=\"number\" value=\"{{item.array}}\"  custom_type=\"array\" />\n    {%- endif -%}\n    </div>\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_array_bool].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">array</label>\n    \n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">array</label>\n    \n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_array_double].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">array</label>\n    <button type=\"button\" class=\"text-xs py-1 px-3 rounded-lg bg-gray-900 text-white add-more\" data-group=\"array\">Add More</button>\n    <div id=\"array-inputs\" class=\"space-y-2\">\n    {% if item.array %}\n        {% for val in item.array %}\n            <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\" max=\"179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\" id=\"array\" name=\"array\" type=\"number\" value=\"{{val}}\"  custom_type=\"array\" />\n        {% endfor -%}\n    {%- else -%}\n        <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\" max=\"179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\" id=\"array\" name=\"array\" type=\"number\" value=\"\"  custom_type=\"array\" />\n    {%- endif -%}\n    </div>\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">array</label>\n    <button type=\"button\" class=\"text-xs py-1 px-3 rounded-lg bg-gray-900 text-white add-more\" data-group=\"array\">Add More</button>\n    <div id=\"array-inputs\" class=\"space-y-2\">\n    {% if item.array %}\n        {% for val in item.array %}\n            <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\" max=\"179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\" id=\"array\" name=\"array\" type=\"number\" value=\"{{val}}\"  custom_type=\"array\" />\n        {% endfor -%}\n    {%- else -%}\n        <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\" max=\"179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\" id=\"array\" name=\"array\" type=\"number\" value=\"{{item.array}}\"  custom_type=\"array\" />\n    {%- endif -%}\n    </div>\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_array_float].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">array</label>\n    <button type=\"button\" class=\"text-xs py-1 px-3 rounded-lg bg-gray-900 text-white add-more\" data-group=\"array\">Add More</button>\n    <div id=\"array-inputs\" class=\"space-y-2\">\n    {% if item.array %}\n        {% for val in item.array %}\n            <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-340282350000000000000000000000000000000\" max=\"340282350000000000000000000000000000000\" id=\"array\" name=\"array\" type=\"number\" value=\"{{val}}\"  custom_type=\"array\" step=\"0.1\" />\n        {% endfor -%}\n    {%- else -%}\n        <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-340282350000000000000000000000000000000\" max=\"340282350000000000000000000000000000000\" id=\"array\" name=\"array\" type=\"number\" value=\"\"  custom_type=\"array\" step=\"0.1\" />\n    {%- endif -%}\n    </div>\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">array</label>\n    <button type=\"button\" class=\"text-xs py-1 px-3 rounded-lg bg-gray-900 text-white add-more\" data-group=\"array\">Add More</button>\n    <div id=\"array-inputs\" class=\"space-y-2\">\n    {% if item.array %}\n        {% for val in item.array %}\n            <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-340282350000000000000000000000000000000\" max=\"340282350000000000000000000000000000000\" id=\"array\" name=\"array\" type=\"number\" value=\"{{val}}\"  custom_type=\"array\" step=\"0.1\" />\n        {% endfor -%}\n    {%- else -%}\n        <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-340282350000000000000000000000000000000\" max=\"340282350000000000000000000000000000000\" id=\"array\" name=\"array\" type=\"number\" value=\"{{item.array}}\"  custom_type=\"array\" step=\"0.1\" />\n    {%- endif -%}\n    </div>\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_array_int].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">array</label>\n    <button type=\"button\" class=\"text-xs py-1 px-3 rounded-lg bg-gray-900 text-white add-more\" data-group=\"array\">Add More</button>\n    <div id=\"array-inputs\" class=\"space-y-2\">\n    {% if item.array %}\n        {% for val in item.array %}\n            <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-2147483648\" max=\"2147483647\" id=\"array\" name=\"array\" type=\"number\" value=\"{{val}}\"  custom_type=\"array\" />\n        {% endfor -%}\n    {%- else -%}\n        <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-2147483648\" max=\"2147483647\" id=\"array\" name=\"array\" type=\"number\" value=\"\"  custom_type=\"array\" />\n    {%- endif -%}\n    </div>\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">array</label>\n    <button type=\"button\" class=\"text-xs py-1 px-3 rounded-lg bg-gray-900 text-white add-more\" data-group=\"array\">Add More</button>\n    <div id=\"array-inputs\" class=\"space-y-2\">\n    {% if item.array %}\n        {% for val in item.array %}\n            <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-2147483648\" max=\"2147483647\" id=\"array\" name=\"array\" type=\"number\" value=\"{{val}}\"  custom_type=\"array\" />\n        {% endfor -%}\n    {%- else -%}\n        <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-2147483648\" max=\"2147483647\" id=\"array\" name=\"array\" type=\"number\" value=\"{{item.array}}\"  custom_type=\"array\" />\n    {%- endif -%}\n    </div>\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_array_string].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">array</label>\n    <button type=\"button\" class=\"text-xs py-1 px-3 rounded-lg bg-gray-900 text-white add-more\" data-group=\"array\">Add More</button>\n    <div id=\"array-inputs\" class=\"space-y-2\">\n    {% if item.array %}\n        {% for val in item.array %}\n            <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" name=\"array\" type=\"text\" value=\"{{val}}\"  custom_type=\"array\"/>\n        {% endfor -%}\n    {%- else -%}\n        <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" name=\"array\" type=\"text\" value=\"\"  custom_type=\"array\"/>\n    {%- endif -%}\n    </div>\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">array</label>\n    <button type=\"button\" class=\"text-xs py-1 px-3 rounded-lg bg-gray-900 text-white add-more\" data-group=\"array\">Add More</button>\n    <div id=\"array-inputs\" class=\"space-y-2\">\n    {% if item.array %}\n        {% for val in item.array %}\n            <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" name=\"array\" type=\"text\" value=\"{{val}}\"  custom_type=\"array\"/>\n        {% endfor -%}\n    {%- else -%}\n        <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" name=\"array\" type=\"text\" value=\"{{item.array}}\"  custom_type=\"array\"/>\n    {%- endif -%}\n    </div>\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_big_int!_big_int!].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">big_int!</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-9223372036854775808\" max=\"9223372036854775807\" id=\"big_int!\" name=\"big_int!\" type=\"number\" value=\"\" required step=\"1\" />\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">big_int!</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-9223372036854775808\" max=\"9223372036854775807\" id=\"big_int!\" name=\"big_int!\" type=\"number\" value=\"{{item.big_int!}}\" required step=\"1\" />\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_big_int^_big_int^].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">big_int^</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-9223372036854775808\" max=\"9223372036854775807\" id=\"big_int^\" name=\"big_int^\" type=\"number\" value=\"\" required step=\"1\" />\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">big_int^</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-9223372036854775808\" max=\"9223372036854775807\" id=\"big_int^\" name=\"big_int^\" type=\"number\" value=\"{{item.big_int^}}\" required step=\"1\" />\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_big_int_big_int].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">big_int</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-9223372036854775808\" max=\"9223372036854775807\" id=\"big_int\" name=\"big_int\" type=\"number\" value=\"\"  step=\"1\" />\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">big_int</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-9223372036854775808\" max=\"9223372036854775807\" id=\"big_int\" name=\"big_int\" type=\"number\" value=\"{{item.big_int}}\"  step=\"1\" />\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_big_unsigned!_big_unsigned!].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">big_unsigned!</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-9223372036854775808\" max=\"9223372036854775807\" id=\"big_unsigned!\" name=\"big_unsigned!\" type=\"number\" value=\"\" required step=\"1\" />\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">big_unsigned!</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-9223372036854775808\" max=\"9223372036854775807\" id=\"big_unsigned!\" name=\"big_unsigned!\" type=\"number\" value=\"{{item.big_unsigned!}}\" required step=\"1\" />\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_big_unsigned^_big_unsigned^].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">big_unsigned^</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-9223372036854775808\" max=\"9223372036854775807\" id=\"big_unsigned^\" name=\"big_unsigned^\" type=\"number\" value=\"\" required step=\"1\" />\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">big_unsigned^</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-9223372036854775808\" max=\"9223372036854775807\" id=\"big_unsigned^\" name=\"big_unsigned^\" type=\"number\" value=\"{{item.big_unsigned^}}\" required step=\"1\" />\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_big_unsigned_big_unsigned].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">big_unsigned</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-9223372036854775808\" max=\"9223372036854775807\" id=\"big_unsigned\" name=\"big_unsigned\" type=\"number\" value=\"\"  step=\"1\" />\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">big_unsigned</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-9223372036854775808\" max=\"9223372036854775807\" id=\"big_unsigned\" name=\"big_unsigned\" type=\"number\" value=\"{{item.big_unsigned}}\"  step=\"1\" />\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_binary_len!_binary_len!].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">binary_len!</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"binary_len!\" name=\"binary_len!\" value=\"\" custom_type=\"blob\" pattern=\"^[0-9]+(,[0-9]+)*$\" required />\n    <p id=\":rh:-form-item-description\" class=\"text-[0.8rem] text-muted-foreground\">e.g: 123,123,123 .</p>\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">binary_len!</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"binary_len!\" name=\"binary_len!\" value=\"{{item.binary_len!}}\" custom_type=\"blob\" pattern=\"^[0-9]+(,[0-9]+)*$\" required />\n    <p id=\":rh:-form-item-description\" class=\"text-[0.8rem] text-muted-foreground\">e.g: 123,123,123 .</p>\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_binary_len^_binary_len^].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">binary_len^</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"binary_len^\" name=\"binary_len^\" value=\"\" custom_type=\"blob\" pattern=\"^[0-9]+(,[0-9]+)*$\" required />\n    <p id=\":rh:-form-item-description\" class=\"text-[0.8rem] text-muted-foreground\">e.g: 123,123,123 .</p>\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">binary_len^</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"binary_len^\" name=\"binary_len^\" value=\"{{item.binary_len^}}\" custom_type=\"blob\" pattern=\"^[0-9]+(,[0-9]+)*$\" required />\n    <p id=\":rh:-form-item-description\" class=\"text-[0.8rem] text-muted-foreground\">e.g: 123,123,123 .</p>\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_binary_len_binary_len].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">binary_len</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"binary_len\" name=\"binary_len\" value=\"\" custom_type=\"blob\" pattern=\"^[0-9]+(,[0-9]+)*$\"  />\n    <p id=\":rh:-form-item-description\" class=\"text-[0.8rem] text-muted-foreground\">e.g: 123,123,123 .</p>\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">binary_len</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"binary_len\" name=\"binary_len\" value=\"{{item.binary_len}}\" custom_type=\"blob\" pattern=\"^[0-9]+(,[0-9]+)*$\"  />\n    <p id=\":rh:-form-item-description\" class=\"text-[0.8rem] text-muted-foreground\">e.g: 123,123,123 .</p>\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_blob!_blob!].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">blob!</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"blob!\" name=\"blob!\" value=\"\" custom_type=\"blob\" pattern=\"^[0-9]+(,[0-9]+)*$\" required />\n    <p id=\":rh:-form-item-description\" class=\"text-[0.8rem] text-muted-foreground\">e.g: 123,123,123 .</p>\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">blob!</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"blob!\" name=\"blob!\" value=\"{{item.blob!}}\" custom_type=\"blob\" pattern=\"^[0-9]+(,[0-9]+)*$\" required />\n    <p id=\":rh:-form-item-description\" class=\"text-[0.8rem] text-muted-foreground\">e.g: 123,123,123 .</p>\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_blob^_blob^].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">blob^</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"blob^\" name=\"blob^\" value=\"\" custom_type=\"blob\" pattern=\"^[0-9]+(,[0-9]+)*$\" required />\n    <p id=\":rh:-form-item-description\" class=\"text-[0.8rem] text-muted-foreground\">e.g: 123,123,123 .</p>\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">blob^</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"blob^\" name=\"blob^\" value=\"{{item.blob^}}\" custom_type=\"blob\" pattern=\"^[0-9]+(,[0-9]+)*$\" required />\n    <p id=\":rh:-form-item-description\" class=\"text-[0.8rem] text-muted-foreground\">e.g: 123,123,123 .</p>\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_blob_blob].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">blob</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"blob\" name=\"blob\" value=\"\" custom_type=\"blob\" pattern=\"^[0-9]+(,[0-9]+)*$\"  />\n    <p id=\":rh:-form-item-description\" class=\"text-[0.8rem] text-muted-foreground\">e.g: 123,123,123 .</p>\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">blob</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"blob\" name=\"blob\" value=\"{{item.blob}}\" custom_type=\"blob\" pattern=\"^[0-9]+(,[0-9]+)*$\"  />\n    <p id=\":rh:-form-item-description\" class=\"text-[0.8rem] text-muted-foreground\">e.g: 123,123,123 .</p>\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_bool!_bool!].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">bool!</label>\n    <input class=\"flex rounded-md border border-input bg-transparent text-base shadow-sm md:text-sm\" id=\"bool!\" name=\"bool!\" type=\"checkbox\" value=\"true\"  required />\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">bool!</label>\n    <input class=\"flex rounded-md border border-input bg-transparent text-base shadow-sm md:text-sm\" id=\"bool!\" name=\"bool!\" type=\"checkbox\" value=\"true\" {% if item.bool! %}checked{%endif %} required />\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_bool_bool].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">bool</label>\n    <input class=\"flex rounded-md border border-input bg-transparent text-base shadow-sm md:text-sm\" id=\"bool\" name=\"bool\" type=\"checkbox\" value=\"true\"   />\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">bool</label>\n    <input class=\"flex rounded-md border border-input bg-transparent text-base shadow-sm md:text-sm\" id=\"bool\" name=\"bool\" type=\"checkbox\" value=\"true\" {% if item.bool %}checked{%endif %}  />\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_date!_date!].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">date!</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"date!\" name=\"date!\" type=\"date\" value=\"\" required />\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">date!</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"date!\" name=\"date!\" type=\"date\" value=\"{{item.date!}}\" required />\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_date^_date^].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">date^</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"date^\" name=\"date^\" type=\"date\" value=\"\" required />\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">date^</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"date^\" name=\"date^\" type=\"date\" value=\"{{item.date^}}\" required />\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_date_date].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">date</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"date\" name=\"date\" type=\"date\" value=\"\"  />\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">date</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"date\" name=\"date\" type=\"date\" value=\"{{item.date}}\"  />\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_date_time!_date_time!].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">date_time!</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"date_time!\" name=\"date_time!\" type=\"datetime-local\" value=\"\" required />\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">date_time!</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"date_time!\" name=\"date_time!\" type=\"datetime-local\" value=\"{{item.date_time!}}\" required />\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_date_time^_date_time^].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">date_time^</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"date_time^\" name=\"date_time^\" type=\"datetime-local\" value=\"\" required />\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">date_time^</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"date_time^\" name=\"date_time^\" type=\"datetime-local\" value=\"{{item.date_time^}}\" required />\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_date_time_date_time].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">date_time</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"date_time\" name=\"date_time\" type=\"datetime-local\" value=\"\"  />\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">date_time</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"date_time\" name=\"date_time\" type=\"datetime-local\" value=\"{{item.date_time}}\"  />\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_decimal!_decimal!].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">decimal!</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-79228162514264337593543950335\" max=\"79228162514264337593543950335\" id=\"decimal!\" name=\"decimal!\" type=\"number\" value=\"\" required step=\"0.1\" />\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">decimal!</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-79228162514264337593543950335\" max=\"79228162514264337593543950335\" id=\"decimal!\" name=\"decimal!\" type=\"number\" value=\"{{item.decimal!}}\" required step=\"0.1\" />\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_decimal^_decimal^].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">decimal^</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-79228162514264337593543950335\" max=\"79228162514264337593543950335\" id=\"decimal^\" name=\"decimal^\" type=\"number\" value=\"\" required step=\"0.1\" />\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">decimal^</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-79228162514264337593543950335\" max=\"79228162514264337593543950335\" id=\"decimal^\" name=\"decimal^\" type=\"number\" value=\"{{item.decimal^}}\" required step=\"0.1\" />\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_decimal_decimal].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">decimal</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-79228162514264337593543950335\" max=\"79228162514264337593543950335\" id=\"decimal\" name=\"decimal\" type=\"number\" value=\"\"  step=\"0.1\" />\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">decimal</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-79228162514264337593543950335\" max=\"79228162514264337593543950335\" id=\"decimal\" name=\"decimal\" type=\"number\" value=\"{{item.decimal}}\"  step=\"0.1\" />\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_decimal_len!_decimal_len!].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">decimal_len!</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-79228162514264337593543950335\" max=\"79228162514264337593543950335\" id=\"decimal_len!\" name=\"decimal_len!\" type=\"number\" value=\"\" required step=\"0.1\" />\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">decimal_len!</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-79228162514264337593543950335\" max=\"79228162514264337593543950335\" id=\"decimal_len!\" name=\"decimal_len!\" type=\"number\" value=\"{{item.decimal_len!}}\" required step=\"0.1\" />\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_decimal_len^_decimal_len^].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">decimal_len^</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-79228162514264337593543950335\" max=\"79228162514264337593543950335\" id=\"decimal_len^\" name=\"decimal_len^\" type=\"number\" value=\"\" required step=\"0.1\" />\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">decimal_len^</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-79228162514264337593543950335\" max=\"79228162514264337593543950335\" id=\"decimal_len^\" name=\"decimal_len^\" type=\"number\" value=\"{{item.decimal_len^}}\" required step=\"0.1\" />\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_decimal_len_decimal_len].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">decimal_len</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-79228162514264337593543950335\" max=\"79228162514264337593543950335\" id=\"decimal_len\" name=\"decimal_len\" type=\"number\" value=\"\"  step=\"0.1\" />\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">decimal_len</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-79228162514264337593543950335\" max=\"79228162514264337593543950335\" id=\"decimal_len\" name=\"decimal_len\" type=\"number\" value=\"{{item.decimal_len}}\"  step=\"0.1\" />\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_double!_double!].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">double!</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\" max=\"179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\" id=\"double!\" name=\"double!\" type=\"number\" value=\"\" required step=\"0.1\" />\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">double!</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\" max=\"179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\" id=\"double!\" name=\"double!\" type=\"number\" value=\"{{item.double!}}\" required step=\"0.1\" />\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_double^_double^].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">double^</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\" max=\"179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\" id=\"double^\" name=\"double^\" type=\"number\" value=\"\" required step=\"0.1\" />\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">double^</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\" max=\"179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\" id=\"double^\" name=\"double^\" type=\"number\" value=\"{{item.double^}}\" required step=\"0.1\" />\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_double_double].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">double</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\" max=\"179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\" id=\"double\" name=\"double\" type=\"number\" value=\"\"  step=\"0.1\" />\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">double</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\" max=\"179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\" id=\"double\" name=\"double\" type=\"number\" value=\"{{item.double}}\"  step=\"0.1\" />\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_float!_float!].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">float!</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-340282350000000000000000000000000000000\" max=\"340282350000000000000000000000000000000\" id=\"float!\" name=\"float!\" type=\"number\" value=\"\" required step=\"0.1\" />\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">float!</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-340282350000000000000000000000000000000\" max=\"340282350000000000000000000000000000000\" id=\"float!\" name=\"float!\" type=\"number\" value=\"{{item.float!}}\" required step=\"0.1\" />\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_float^_float^].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">float^</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-340282350000000000000000000000000000000\" max=\"340282350000000000000000000000000000000\" id=\"float^\" name=\"float^\" type=\"number\" value=\"\" required step=\"0.1\" />\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">float^</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-340282350000000000000000000000000000000\" max=\"340282350000000000000000000000000000000\" id=\"float^\" name=\"float^\" type=\"number\" value=\"{{item.float^}}\" required step=\"0.1\" />\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_float_float].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">float</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-340282350000000000000000000000000000000\" max=\"340282350000000000000000000000000000000\" id=\"float\" name=\"float\" type=\"number\" value=\"\"  step=\"0.1\" />\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">float</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-340282350000000000000000000000000000000\" max=\"340282350000000000000000000000000000000\" id=\"float\" name=\"float\" type=\"number\" value=\"{{item.float}}\"  step=\"0.1\" />\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_int!_int!].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">int!</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-2147483648\" max=\"2147483647\" id=\"int!\" name=\"int!\" type=\"number\" value=\"\" required step=\"1\" />\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">int!</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-2147483648\" max=\"2147483647\" id=\"int!\" name=\"int!\" type=\"number\" value=\"{{item.int!}}\" required step=\"1\" />\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_int^_int^].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">int^</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-2147483648\" max=\"2147483647\" id=\"int^\" name=\"int^\" type=\"number\" value=\"\" required step=\"1\" />\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">int^</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-2147483648\" max=\"2147483647\" id=\"int^\" name=\"int^\" type=\"number\" value=\"{{item.int^}}\" required step=\"1\" />\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_int_int].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">int</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-2147483648\" max=\"2147483647\" id=\"int\" name=\"int\" type=\"number\" value=\"\"  step=\"1\" />\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">int</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-2147483648\" max=\"2147483647\" id=\"int\" name=\"int\" type=\"number\" value=\"{{item.int}}\"  step=\"1\" />\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_json!_json!].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">json!</label>\n    <textarea class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"json!\" name=\"json!\" type=\"text\" rows=\"10\" cols=\"50\" required></textarea>\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">json!</label>\n    <textarea class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"json!\" name=\"json!\" type=\"text\" rows=\"10\" cols=\"50\" required>{{item.json!}}</textarea>\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_json_json].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">json</label>\n    <textarea class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"json\" name=\"json\" type=\"text\" rows=\"10\" cols=\"50\" ></textarea>\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">json</label>\n    <textarea class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"json\" name=\"json\" type=\"text\" rows=\"10\" cols=\"50\" >{{item.json}}</textarea>\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_jsonb!_jsonb!].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">jsonb!</label>\n    <textarea class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"jsonb!\" name=\"jsonb!\" type=\"text\" rows=\"10\" cols=\"50\" required></textarea>\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">jsonb!</label>\n    <textarea class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"jsonb!\" name=\"jsonb!\" type=\"text\" rows=\"10\" cols=\"50\" required>{{item.jsonb!}}</textarea>\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_jsonb^_jsonb^].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">jsonb^</label>\n    <textarea class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"jsonb^\" name=\"jsonb^\" type=\"text\" rows=\"10\" cols=\"50\" required></textarea>\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">jsonb^</label>\n    <textarea class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"jsonb^\" name=\"jsonb^\" type=\"text\" rows=\"10\" cols=\"50\" required>{{item.jsonb^}}</textarea>\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_jsonb_jsonb].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">jsonb</label>\n    <textarea class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"jsonb\" name=\"jsonb\" type=\"text\" rows=\"10\" cols=\"50\" ></textarea>\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">jsonb</label>\n    <textarea class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"jsonb\" name=\"jsonb\" type=\"text\" rows=\"10\" cols=\"50\" >{{item.jsonb}}</textarea>\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_money!_money!].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">money!</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-79228162514264337593543950335\" max=\"79228162514264337593543950335\" id=\"money!\" name=\"money!\" type=\"number\" value=\"\" required step=\"0.1\" />\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">money!</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-79228162514264337593543950335\" max=\"79228162514264337593543950335\" id=\"money!\" name=\"money!\" type=\"number\" value=\"{{item.money!}}\" required step=\"0.1\" />\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_money^_money^].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">money^</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-79228162514264337593543950335\" max=\"79228162514264337593543950335\" id=\"money^\" name=\"money^\" type=\"number\" value=\"\" required step=\"0.1\" />\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">money^</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-79228162514264337593543950335\" max=\"79228162514264337593543950335\" id=\"money^\" name=\"money^\" type=\"number\" value=\"{{item.money^}}\" required step=\"0.1\" />\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_money_money].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">money</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-79228162514264337593543950335\" max=\"79228162514264337593543950335\" id=\"money\" name=\"money\" type=\"number\" value=\"\"  step=\"0.1\" />\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">money</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-79228162514264337593543950335\" max=\"79228162514264337593543950335\" id=\"money\" name=\"money\" type=\"number\" value=\"{{item.money}}\"  step=\"0.1\" />\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_small_int!_small_int!].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">small_int!</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-32768\" max=\"32767\" id=\"small_int!\" name=\"small_int!\" type=\"number\" value=\"\" required step=\"1\" />\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">small_int!</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-32768\" max=\"32767\" id=\"small_int!\" name=\"small_int!\" type=\"number\" value=\"{{item.small_int!}}\" required step=\"1\" />\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_small_int^_small_int^].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">small_int^</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-32768\" max=\"32767\" id=\"small_int^\" name=\"small_int^\" type=\"number\" value=\"\" required step=\"1\" />\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">small_int^</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-32768\" max=\"32767\" id=\"small_int^\" name=\"small_int^\" type=\"number\" value=\"{{item.small_int^}}\" required step=\"1\" />\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_small_int_small_int].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">small_int</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-32768\" max=\"32767\" id=\"small_int\" name=\"small_int\" type=\"number\" value=\"\"  step=\"1\" />\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">small_int</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-32768\" max=\"32767\" id=\"small_int\" name=\"small_int\" type=\"number\" value=\"{{item.small_int}}\"  step=\"1\" />\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_small_unsigned!_small_unsigned!].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">small_unsigned!</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-32768\" max=\"32767\" id=\"small_unsigned!\" name=\"small_unsigned!\" type=\"number\" value=\"\" required step=\"1\" />\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">small_unsigned!</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-32768\" max=\"32767\" id=\"small_unsigned!\" name=\"small_unsigned!\" type=\"number\" value=\"{{item.small_unsigned!}}\" required step=\"1\" />\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_small_unsigned^_small_unsigned^].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">small_unsigned^</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-32768\" max=\"32767\" id=\"small_unsigned^\" name=\"small_unsigned^\" type=\"number\" value=\"\" required step=\"1\" />\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">small_unsigned^</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-32768\" max=\"32767\" id=\"small_unsigned^\" name=\"small_unsigned^\" type=\"number\" value=\"{{item.small_unsigned^}}\" required step=\"1\" />\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_small_unsigned_small_unsigned].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">small_unsigned</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-32768\" max=\"32767\" id=\"small_unsigned\" name=\"small_unsigned\" type=\"number\" value=\"\"  step=\"1\" />\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">small_unsigned</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-32768\" max=\"32767\" id=\"small_unsigned\" name=\"small_unsigned\" type=\"number\" value=\"{{item.small_unsigned}}\"  step=\"1\" />\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_string!_string!].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">string!</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"string!\" name=\"string!\" type=\"text\" value=\"\" required />\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">string!</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"string!\" name=\"string!\" type=\"text\" value=\"{{item.string!}}\" required />\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_string^_string^].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">string^</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"string^\" name=\"string^\" type=\"text\" value=\"\" required />\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">string^</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"string^\" name=\"string^\" type=\"text\" value=\"{{item.string^}}\" required />\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_string_string].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">string</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"string\" name=\"string\" type=\"text\" value=\"\"  />\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">string</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"string\" name=\"string\" type=\"text\" value=\"{{item.string}}\"  />\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_text!_text!].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">text!</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"text!\" name=\"text!\" type=\"text\" value=\"\" required />\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">text!</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"text!\" name=\"text!\" type=\"text\" value=\"{{item.text!}}\" required />\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_text^_text^].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">text^</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"text^\" name=\"text^\" type=\"text\" value=\"\" required />\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">text^</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"text^\" name=\"text^\" type=\"text\" value=\"{{item.text^}}\" required />\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_text_text].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">text</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"text\" name=\"text\" type=\"text\" value=\"\"  />\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">text</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"text\" name=\"text\" type=\"text\" value=\"{{item.text}}\"  />\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_tstz!_tstz!].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">tstz!</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"tstz!\" name=\"tstz!\" type=\"datetime-local\" value=\"\" required />\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">tstz!</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"tstz!\" name=\"tstz!\" type=\"datetime-local\" value=\"{{item.tstz!}}\" required />\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_tstz_tstz].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">tstz</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"tstz\" name=\"tstz\" type=\"datetime-local\" value=\"\"  />\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">tstz</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"tstz\" name=\"tstz\" type=\"datetime-local\" value=\"{{item.tstz}}\"  />\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_unsigned!_unsigned!].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">unsigned!</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-2147483648\" max=\"2147483647\" id=\"unsigned!\" name=\"unsigned!\" type=\"number\" value=\"\" required step=\"1\" />\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">unsigned!</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-2147483648\" max=\"2147483647\" id=\"unsigned!\" name=\"unsigned!\" type=\"number\" value=\"{{item.unsigned!}}\" required step=\"1\" />\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_unsigned^_unsigned^].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">unsigned^</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-2147483648\" max=\"2147483647\" id=\"unsigned^\" name=\"unsigned^\" type=\"number\" value=\"\" required step=\"1\" />\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">unsigned^</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-2147483648\" max=\"2147483647\" id=\"unsigned^\" name=\"unsigned^\" type=\"number\" value=\"{{item.unsigned^}}\" required step=\"1\" />\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_unsigned_unsigned].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">unsigned</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-2147483648\" max=\"2147483647\" id=\"unsigned\" name=\"unsigned\" type=\"number\" value=\"\"  step=\"1\" />\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">unsigned</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-2147483648\" max=\"2147483647\" id=\"unsigned\" name=\"unsigned\" type=\"number\" value=\"{{item.unsigned}}\"  step=\"1\" />\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_uuid!_uuid!].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\nsnapshot_kind: text\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">uuid!</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"uuid!\" name=\"uuid!\" type=\"text\" value=\"\" required pattern=\"[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}\"/>\n    <p id=\":rh:-form-item-description\" class=\"text-[0.8rem] text-muted-foreground\">e.g: 11111111-1111-1111-1111-111111111111..</p>\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">uuid!</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"uuid!\" name=\"uuid!\" type=\"text\" value=\"{{item.uuid!}}\" required pattern=\"[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}\"/>\n    <p id=\":rh:-form-item-description\" class=\"text-[0.8rem] text-muted-foreground\">e.g: 11111111-1111-1111-1111-111111111111..</p>\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_uuid^_uuid^].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\nsnapshot_kind: text\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">uuid^</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"uuid^\" name=\"uuid^\" type=\"text\" value=\"\" required pattern=\"[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}\"/>\n    <p id=\":rh:-form-item-description\" class=\"text-[0.8rem] text-muted-foreground\">e.g: 11111111-1111-1111-1111-111111111111..</p>\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">uuid^</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"uuid^\" name=\"uuid^\" type=\"text\" value=\"{{item.uuid^}}\" required pattern=\"[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}\"/>\n    <p id=\":rh:-form-item-description\" class=\"text-[0.8rem] text-muted-foreground\">e.g: 11111111-1111-1111-1111-111111111111..</p>\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_uuid_uuid].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\nsnapshot_kind: text\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">uuid</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"uuid\" name=\"uuid\" type=\"text\" value=\"\"  pattern=\"[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}\"/>\n    <p id=\":rh:-form-item-description\" class=\"text-[0.8rem] text-muted-foreground\">e.g: 11111111-1111-1111-1111-111111111111..</p>\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">uuid</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"uuid\" name=\"uuid\" type=\"text\" value=\"{{item.uuid}}\"  pattern=\"[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}\"/>\n    <p id=\":rh:-form-item-description\" class=\"text-[0.8rem] text-muted-foreground\">e.g: 11111111-1111-1111-1111-111111111111..</p>\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_var_binary!_var_binary!].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">var_binary!</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"var_binary!\" name=\"var_binary!\" value=\"\" custom_type=\"blob\" pattern=\"^[0-9]+(,[0-9]+)*$\" required />\n    <p id=\":rh:-form-item-description\" class=\"text-[0.8rem] text-muted-foreground\">e.g: 123,123,123 .</p>\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">var_binary!</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"var_binary!\" name=\"var_binary!\" value=\"{{item.var_binary!}}\" custom_type=\"blob\" pattern=\"^[0-9]+(,[0-9]+)*$\" required />\n    <p id=\":rh:-form-item-description\" class=\"text-[0.8rem] text-muted-foreground\">e.g: 123,123,123 .</p>\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_var_binary^_var_binary^].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">var_binary^</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"var_binary^\" name=\"var_binary^\" value=\"\" custom_type=\"blob\" pattern=\"^[0-9]+(,[0-9]+)*$\" required />\n    <p id=\":rh:-form-item-description\" class=\"text-[0.8rem] text-muted-foreground\">e.g: 123,123,123 .</p>\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">var_binary^</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"var_binary^\" name=\"var_binary^\" value=\"{{item.var_binary^}}\" custom_type=\"blob\" pattern=\"^[0-9]+(,[0-9]+)*$\" required />\n    <p id=\":rh:-form-item-description\" class=\"text-[0.8rem] text-muted-foreground\">e.g: 123,123,123 .</p>\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_form_field_[form_var_binary_var_binary].snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"format!(\\\"Create form\\\\n\\\\n{create_form}\\\\n\\\\nEdit Form\\\\n\\\\n{edit_form}\\\")\"\n---\nCreate form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">var_binary</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"var_binary\" name=\"var_binary\" value=\"\" custom_type=\"blob\" pattern=\"^[0-9]+(,[0-9]+)*$\"  />\n    <p id=\":rh:-form-item-description\" class=\"text-[0.8rem] text-muted-foreground\">e.g: 123,123,123 .</p>\n</div>\n\nEdit Form\n\n<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">var_binary</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"var_binary\" name=\"var_binary\" value=\"{{item.var_binary}}\" custom_type=\"blob\" pattern=\"^[0-9]+(,[0-9]+)*$\"  />\n    <p id=\":rh:-form-item-description\" class=\"text-[0.8rem] text-muted-foreground\">e.g: 123,123,123 .</p>\n</div>\n"
  },
  {
    "path": "loco-gen/src/snapshots/loco_gen__tera_ext__tests__can_render_view_field.snap",
    "content": "---\nsource: loco-gen/src/tera_ext.rs\nexpression: \"all_results.join(\\\"\\\\n\\\")\"\nsnapshot_kind: text\n---\nField: array!.big_int (type: Option<Vec<i64>>)\n{{item.array!}}\n\nField: array!.bool (type: Option<Vec<bool>>)\n{{item.array!}}\n\nField: array!.double (type: Option<Vec<f64>>)\n{{item.array!}}\n\nField: array!.float (type: Option<Vec<f32>>)\n{{item.array!}}\n\nField: array!.int (type: Option<Vec<i32>>)\n{{item.array!}}\n\nField: array!.string (type: Option<Vec<String>>)\n{{item.array!}}\n\nField: array.big_int (type: Option<Vec<i64>>)\n{{item.array}}\n\nField: array.bool (type: Option<Vec<bool>>)\n{{item.array}}\n\nField: array.double (type: Option<Vec<f64>>)\n{{item.array}}\n\nField: array.float (type: Option<Vec<f32>>)\n{{item.array}}\n\nField: array.int (type: Option<Vec<i32>>)\n{{item.array}}\n\nField: array.string (type: Option<Vec<String>>)\n{{item.array}}\n\nField: array^.big_int (type: Option<Vec<i64>>)\n{{item.array^}}\n\nField: array^.bool (type: Option<Vec<bool>>)\n{{item.array^}}\n\nField: array^.double (type: Option<Vec<f64>>)\n{{item.array^}}\n\nField: array^.float (type: Option<Vec<f32>>)\n{{item.array^}}\n\nField: array^.int (type: Option<Vec<i32>>)\n{{item.array^}}\n\nField: array^.string (type: Option<Vec<String>>)\n{{item.array^}}\n\nField: big_int!.big_int! (type: i64)\n{{item.big_int!}}\n\nField: big_int.big_int (type: Option<i64>)\n{{item.big_int}}\n\nField: big_int^.big_int^ (type: i64)\n{{item.big_int^}}\n\nField: big_unsigned!.big_unsigned! (type: i64)\n{{item.big_unsigned!}}\n\nField: big_unsigned.big_unsigned (type: Option<i64>)\n{{item.big_unsigned}}\n\nField: big_unsigned^.big_unsigned^ (type: i64)\n{{item.big_unsigned^}}\n\nField: binary_len!.binary_len! (type: Vec<u8>)\n{{item.binary_len!}}\n\nField: binary_len.binary_len (type: Option<Vec<u8>>)\n{{item.binary_len}}\n\nField: binary_len^.binary_len^ (type: Vec<u8>)\n{{item.binary_len^}}\n\nField: blob!.blob! (type: Vec<u8>)\n{{item.blob!}}\n\nField: blob.blob (type: Option<Vec<u8>>)\n{{item.blob}}\n\nField: blob^.blob^ (type: Vec<u8>)\n{{item.blob^}}\n\nField: bool!.bool! (type: bool)\n{% if item.bool! %}{{item.bool!}}{% else %}false{% endif %}\n\nField: bool.bool (type: Option<bool>)\n{% if item.bool %}{{item.bool}}{% else %}false{% endif %}\n\nField: date!.date! (type: Date)\n{{item.date!}}\n\nField: date.date (type: Option<Date>)\n{{item.date}}\n\nField: date^.date^ (type: Date)\n{{item.date^}}\n\nField: date_time!.date_time! (type: DateTime)\n{{item.date_time!}}\n\nField: date_time.date_time (type: Option<DateTime>)\n{{item.date_time}}\n\nField: date_time^.date_time^ (type: DateTime)\n{{item.date_time^}}\n\nField: decimal!.decimal! (type: Decimal)\n{{item.decimal!}}\n\nField: decimal.decimal (type: Option<Decimal>)\n{{item.decimal}}\n\nField: decimal^.decimal^ (type: Decimal)\n{{item.decimal^}}\n\nField: decimal_len!.decimal_len! (type: Decimal)\n{{item.decimal_len!}}\n\nField: decimal_len.decimal_len (type: Option<Decimal>)\n{{item.decimal_len}}\n\nField: decimal_len^.decimal_len^ (type: Decimal)\n{{item.decimal_len^}}\n\nField: double!.double! (type: f64)\n{{item.double!}}\n\nField: double.double (type: Option<f64>)\n{{item.double}}\n\nField: double^.double^ (type: f64)\n{{item.double^}}\n\nField: float!.float! (type: f32)\n{{item.float!}}\n\nField: float.float (type: Option<f32>)\n{{item.float}}\n\nField: float^.float^ (type: f32)\n{{item.float^}}\n\nField: int!.int! (type: i32)\n{{item.int!}}\n\nField: int.int (type: Option<i32>)\n{{item.int}}\n\nField: int^.int^ (type: i32)\n{{item.int^}}\n\nField: json!.json! (type: serde_json::Value)\n{{item.json! | escape }}\n\nField: json.json (type: Option<serde_json::Value>)\n{{item.json | escape }}\n\nField: jsonb!.jsonb! (type: serde_json::Value)\n{{item.jsonb! | escape }}\n\nField: jsonb.jsonb (type: Option<serde_json::Value>)\n{{item.jsonb | escape }}\n\nField: jsonb^.jsonb^ (type: serde_json::Value)\n{{item.jsonb^ | escape }}\n\nField: money!.money! (type: Decimal)\n{{item.money!}}\n\nField: money.money (type: Option<Decimal>)\n{{item.money}}\n\nField: money^.money^ (type: Decimal)\n{{item.money^}}\n\nField: small_int!.small_int! (type: i16)\n{{item.small_int!}}\n\nField: small_int.small_int (type: Option<i16>)\n{{item.small_int}}\n\nField: small_int^.small_int^ (type: i16)\n{{item.small_int^}}\n\nField: small_unsigned!.small_unsigned! (type: i16)\n{{item.small_unsigned!}}\n\nField: small_unsigned.small_unsigned (type: Option<i16>)\n{{item.small_unsigned}}\n\nField: small_unsigned^.small_unsigned^ (type: i16)\n{{item.small_unsigned^}}\n\nField: string!.string! (type: String)\n{{item.string! | escape }}\n\nField: string.string (type: Option<String>)\n{{item.string | escape }}\n\nField: string^.string^ (type: String)\n{{item.string^ | escape }}\n\nField: text!.text! (type: String)\n{{item.text! | escape }}\n\nField: text.text (type: Option<String>)\n{{item.text | escape }}\n\nField: text^.text^ (type: String)\n{{item.text^ | escape }}\n\nField: tstz!.tstz! (type: DateTimeWithTimeZone)\n{{item.tstz!}}\n\nField: tstz.tstz (type: Option<DateTimeWithTimeZone>)\n{{item.tstz}}\n\nField: unsigned!.unsigned! (type: i32)\n{{item.unsigned!}}\n\nField: unsigned.unsigned (type: Option<i32>)\n{{item.unsigned}}\n\nField: unsigned^.unsigned^ (type: i32)\n{{item.unsigned^}}\n\nField: uuid!.uuid! (type: Uuid)\n{{item.uuid! | escape }}\n\nField: uuid.uuid (type: Option<Uuid>)\n{{item.uuid | escape }}\n\nField: uuid^.uuid^ (type: Uuid)\n{{item.uuid^ | escape }}\n\nField: var_binary!.var_binary! (type: Vec<u8>)\n{{item.var_binary!}}\n\nField: var_binary.var_binary (type: Option<Vec<u8>>)\n{{item.var_binary}}\n\nField: var_binary^.var_binary^ (type: Vec<u8>)\n{{item.var_binary^}}\n"
  },
  {
    "path": "loco-gen/src/template.rs",
    "content": "use std::path::{Path, PathBuf};\n\nuse include_dir::{include_dir, Dir, DirEntry, File};\n\nuse crate::{Error, Result};\n\nstatic TEMPLATES: Dir<'_> = include_dir!(\"$CARGO_MANIFEST_DIR/src/templates\");\npub const DEFAULT_LOCAL_TEMPLATE: &str = \".loco-templates\";\n\n/// Returns a list of paths that should be ignored during file collection.\n#[must_use]\npub fn get_ignored_paths() -> Vec<&'static Path> {\n    vec![\n        #[cfg(not(feature = \"with-db\"))]\n        Path::new(\"scaffold\"),\n        #[cfg(not(feature = \"with-db\"))]\n        Path::new(\"migration\"),\n        #[cfg(not(feature = \"with-db\"))]\n        Path::new(\"model\"),\n    ]\n}\n\n/// Checks whether a specific path exists in the included templates.\n#[must_use]\npub fn exists(path: &Path) -> bool {\n    TEMPLATES.get_entry(path).is_some()\n}\n\n/// Determines whether a given path should be ignored based on the ignored paths\n/// list.\n#[must_use]\nfn is_path_ignored(path: &Path, ignored_paths: &[&Path]) -> bool {\n    ignored_paths\n        .iter()\n        .any(|&ignored| path.starts_with(ignored))\n}\n\n/// Collects all file paths from the included templates directory recursively.\n#[must_use]\npub fn collect() -> Vec<PathBuf> {\n    collect_files_path_recursively(&TEMPLATES)\n}\n\n/// Collects all files from the included templates directory recursively.\n#[must_use]\npub fn collect_files() -> Vec<&'static File<'static>> {\n    collect_files_recursively(&TEMPLATES)\n}\n\n/// Collects all file paths within a specific directory in the templates.\n///\n/// # Errors\n/// Returns [`Error::TemplateNotFound`] if the directory is not found.\npub fn collect_files_path(path: &Path) -> Result<Vec<PathBuf>> {\n    TEMPLATES.get_entry(path).map_or_else(\n        || {\n            Err(Error::TemplateNotFound {\n                path: path.to_path_buf(),\n            })\n        },\n        |entry| match entry {\n            DirEntry::Dir(dir) => Ok(collect_files_path_recursively(dir)),\n            DirEntry::File(file) => Ok(vec![file.path().to_path_buf()]),\n        },\n    )\n}\n\n/// Collects all files within a specific directory in the templates.\n///\n/// # Errors\n/// Returns [`Error::TemplateNotFound`] if the directory is not found.\npub fn collect_files_from_path(path: &Path) -> Result<Vec<&File<'_>>> {\n    TEMPLATES.get_entry(path).map_or_else(\n        || {\n            Err(Error::TemplateNotFound {\n                path: path.to_path_buf(),\n            })\n        },\n        |entry| match entry {\n            DirEntry::Dir(dir) => Ok(collect_files_recursively(dir)),\n            DirEntry::File(file) => Ok(vec![file]),\n        },\n    )\n}\n\n/// Recursively collects all file paths from a directory, skipping ignored\n/// paths.\nfn collect_files_path_recursively(dir: &Dir<'_>) -> Vec<PathBuf> {\n    let mut file_paths = Vec::new();\n\n    for entry in dir.entries() {\n        match entry {\n            DirEntry::File(file) => file_paths.push(file.path().to_path_buf()),\n            DirEntry::Dir(subdir) => {\n                if !is_path_ignored(subdir.path(), &get_ignored_paths()) {\n                    file_paths.extend(collect_files_path_recursively(subdir));\n                }\n            }\n        }\n    }\n    file_paths\n}\n\n/// Recursively collects all files from a directory, skipping ignored paths.\nfn collect_files_recursively<'a>(dir: &'a Dir<'a>) -> Vec<&'a File<'a>> {\n    let mut files = Vec::new();\n\n    for entry in dir.entries() {\n        match entry {\n            DirEntry::File(file) => files.push(file),\n            DirEntry::Dir(subdir) => {\n                if !is_path_ignored(subdir.path(), &get_ignored_paths()) {\n                    files.extend(collect_files_recursively(subdir));\n                }\n            }\n        }\n    }\n    files\n}\n\n#[cfg(test)]\npub mod tests {\n    use std::path::Path;\n\n    use super::*;\n\n    /// .\n    ///\n    /// # Panics\n    ///\n    /// Panics if .\n    #[must_use]\n    pub fn find_first_dir() -> &'static Dir<'static> {\n        TEMPLATES.dirs().next().expect(\"first folder\")\n    }\n\n    /// .\n    #[must_use]\n    pub fn find_first_file<'a>(dir: &'a Dir<'a>) -> Option<&'a File<'a>> {\n        for entry in dir.entries() {\n            match entry {\n                DirEntry::File(file) => return Some(file),\n                DirEntry::Dir(sub_dir) => {\n                    if let Some(file) = find_first_file(sub_dir) {\n                        return Some(file);\n                    }\n                }\n            }\n        }\n        None\n    }\n\n    #[test]\n    fn test_get_ignored_paths() {\n        let ignored_paths = get_ignored_paths();\n        #[cfg(not(feature = \"with-db\"))]\n        {\n            assert!(ignored_paths.contains(&Path::new(\"scaffold\")));\n            assert!(ignored_paths.contains(&Path::new(\"migration\")));\n            assert!(ignored_paths.contains(&Path::new(\"model\")));\n        }\n        #[cfg(feature = \"with-db\")]\n        {\n            assert!(ignored_paths.is_empty());\n        }\n    }\n\n    #[test]\n    fn test_exists() {\n        // test existing folder\n        let test_folder = TEMPLATES.dirs().next().expect(\"first folder\");\n        assert!(exists(test_folder.path()));\n        assert!(!exists(Path::new(\"none-folder\")));\n\n        // test existing file\n        let test_file = find_first_file(&TEMPLATES).expect(\"find file\");\n        println!(\"==== {:#?}\", test_file.path());\n        assert!(exists(test_file.path()));\n        assert!(!exists(Path::new(\"none.rs.t\")));\n    }\n\n    #[test]\n    fn test_collect() {\n        let file_paths = collect();\n        assert!(!file_paths.is_empty());\n        for path in file_paths {\n            assert!(TEMPLATES.get_entry(&path).is_some());\n        }\n    }\n\n    #[test]\n    fn test_collect_files() {\n        let files = collect_files();\n        assert!(!files.is_empty());\n        for file in files {\n            assert!(TEMPLATES.get_entry(file.path()).is_some());\n        }\n    }\n\n    #[test]\n    fn test_is_path_ignored() {\n        let path = Path::new(\"/home/user/project/src/main.rs\");\n        let ignores = vec![\n            Path::new(\"/home/user/project/target\"),\n            Path::new(\"/home/user/project/src\"),\n        ];\n\n        assert!(is_path_ignored(path, &ignores));\n\n        let non_ignored_path = Path::new(\"/home/user/project/docs/readme.md\");\n        assert!(!is_path_ignored(non_ignored_path, &ignores));\n\n        let empty_ignores: &[&Path] = &[];\n        assert!(!is_path_ignored(path, empty_ignores));\n    }\n}\n"
  },
  {
    "path": "loco-gen/src/templates/controller/api/controller.t",
    "content": "{% set file_name = name |  snake_case -%}\n{% set module_name = file_name | pascal_case -%}\nto: src/controllers/{{ file_name }}.rs\nskip_exists: true\nmessage: \"Controller `{{module_name}}` was added successfully.\"\ninjections:\n- into: src/controllers/mod.rs\n  append: true\n  content: \"pub mod {{ file_name }};\"\n- into: src/app.rs\n  after: \"AppRoutes::\"\n  content: \"            .add_route(controllers::{{ file_name }}::routes())\"\n---\n#![allow(clippy::missing_errors_doc)]\n#![allow(clippy::unnecessary_struct_initialization)]\n#![allow(clippy::unused_async)]\nuse loco_rs::prelude::*;\n\n#[debug_handler]\npub async fn index(State(_ctx): State<AppContext>) -> Result<Response> {\n    format::empty()\n}\n\n{% for action in actions -%}\n#[debug_handler]\npub async fn {{action}}(State(_ctx): State<AppContext>) -> Result<Response> {\n    format::empty()\n}\n\n{% endfor -%}\n\npub fn routes() -> Routes {\n    Routes::new()\n        .prefix(\"api/{{file_name | plural}}/\")\n        .add(\"/\", get(index))\n        {%- for action in actions %}\n        .add(\"{{action}}\", get({{action}}))\n        {%- endfor %}\n}\n"
  },
  {
    "path": "loco-gen/src/templates/controller/api/test.t",
    "content": "{% set file_name = name |  snake_case -%}\n{% set module_name = file_name | pascal_case -%}\nto: tests/requests/{{ file_name }}.rs\nskip_exists: true\nmessage: \"Tests for controller `{{module_name}}` was added successfully. Run `cargo test`.\"\ninjections:\n- into: tests/requests/mod.rs\n  append: true\n  content: \"pub mod {{ file_name }};\"\n---\nuse {{pkg_name}}::app::App;\nuse loco_rs::testing::prelude::*;\nuse serial_test::serial;\n\n#[tokio::test]\n#[serial]\nasync fn can_get_{{ name | plural | snake_case }}() {\n    request::<App, _, _>(|request, _ctx| async move {\n        let res = request.get(\"/api/{{ name | plural | snake_case }}/\").await;\n        assert_eq!(res.status_code(), 200);\n\n        // you can assert content like this:\n        // assert_eq!(res.text(), \"content\");\n    })\n    .await;\n}\n\n{% for action in actions -%}\n#[tokio::test]\n#[serial]\nasync fn can_get_{{action}}() {\n    request::<App, _, _>(|request, _ctx| async move {\n        let res = request.get(\"/{{ name | plural | snake_case }}/{{action}}\").await;\n        assert_eq!(res.status_code(), 200);\n    })\n    .await;\n}\n\n{% endfor -%}\n"
  },
  {
    "path": "loco-gen/src/templates/controller/html/controller.t",
    "content": "{% set file_name = name |  snake_case -%}\n{% set module_name = file_name | pascal_case -%}\nto: src/controllers/{{ file_name }}.rs\nskip_exists: true\nmessage: \"Controller `{{module_name}}` was added successfully.\"\ninjections:\n- into: src/controllers/mod.rs\n  append: true\n  content: \"pub mod {{ file_name }};\"\n- into: src/app.rs\n  after: \"AppRoutes::\"\n  content: \"            .add_route(controllers::{{ file_name }}::routes())\"\n---\n#![allow(clippy::missing_errors_doc)]\n#![allow(clippy::unnecessary_struct_initialization)]\n#![allow(clippy::unused_async)]\nuse loco_rs::prelude::*;\n\n{% for action in actions -%}\n#[debug_handler]\npub async fn {{action}}(\n    ViewEngine(v): ViewEngine<TeraView>,\n    State(_ctx): State<AppContext>\n) -> Result<Response> {\n    format::render().view(&v, \"{{file_name}}/{{action}}.html\", data!({}))\n}\n\n{% endfor -%}\n\npub fn routes() -> Routes {\n    Routes::new()\n        .prefix(\"{{file_name | plural}}/\")\n        {%- for action in actions %}\n        .add(\"{{action}}\", get({{action}}))\n        {%- endfor %}\n}\n"
  },
  {
    "path": "loco-gen/src/templates/controller/html/view.t",
    "content": "{% set file_name = name |  snake_case -%}\n{% set module_name = file_name | pascal_case -%}\nto: assets/views/{{file_name}}/{{action}}.html\nskip_exists: true\nmessage: \"{{file_name}}/{{action}} view was added successfully.\"\n---\n<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n    <script src=\"https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio,line-clamp\"></script>\n</head>\n\n<body class=\"prose p-10\">\n    <h1>View {{action}}</h1>\n    Find me in <code>{{file_name}}/{{action}}</code>\n</body>\n\n</html>\n"
  },
  {
    "path": "loco-gen/src/templates/controller/htmx/controller.t",
    "content": "{% set file_name = name |  snake_case -%}\n{% set module_name = file_name | pascal_case -%}\nto: src/controllers/{{ file_name }}.rs\nskip_exists: true\nmessage: \"Controller `{{module_name}}` was added successfully.\"\ninjections:\n- into: src/controllers/mod.rs\n  append: true\n  content: \"pub mod {{ file_name }};\"\n- into: src/app.rs\n  after: \"AppRoutes::\"\n  content: \"            .add_route(controllers::{{ file_name }}::routes())\"\n---\n#![allow(clippy::missing_errors_doc)]\n#![allow(clippy::unnecessary_struct_initialization)]\n#![allow(clippy::unused_async)]\nuse loco_rs::prelude::*;\n\n{% for action in actions -%}\n#[debug_handler]\npub async fn {{action}}(\n    ViewEngine(v): ViewEngine<TeraView>,\n    State(_ctx): State<AppContext>\n) -> Result<Response> {\n    format::render().view(&v, \"{{file_name}}/{{action}}.html\", data!({}))\n}\n\n{% endfor -%}\n\npub fn routes() -> Routes {\n    Routes::new()\n        .prefix(\"{{file_name | plural}}\")\n        {%- for action in actions %}\n        .add(\"{{action}}\", get({{action}}))\n        {%- endfor %}\n}\n"
  },
  {
    "path": "loco-gen/src/templates/controller/htmx/view.t",
    "content": "{% set file_name = name |  snake_case -%}\n{% set module_name = file_name | pascal_case -%}\nto: assets/views/{{file_name}}/{{action}}.html\nskip_exists: true\nmessage: \"{{file_name}}/{{action}} view was added successfully.\"\n---\n<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n    <script src=\"https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio,line-clamp\"></script>\n</head>\n\n<body class=\"prose p-10\">\n    <h1>View {{action}}</h1>\n    Find me in <code>{{file_name}}/{{action}}</code>\n</body>\n\n</html>\n"
  },
  {
    "path": "loco-gen/src/templates/data/0_mod.t",
    "content": "{% set module_name = name | snake_case -%}\n{% set struct_name = module_name | pascal_case -%}\nto: \"src/data/mod.rs\"\nskip_exists: true\nmessage: \"Data module added\"\ninjections:\n- into: \"src/lib.rs\"\n  append: true\n  content: \"pub mod data;\"\n---\n"
  },
  {
    "path": "loco-gen/src/templates/data/data.t",
    "content": "{% set module_name = name | snake_case -%}\nto: \"data/{{module_name}}/data.json\"\nskip_exists: true\n---\n{ \n  \"is_loaded\": true\n}\n"
  },
  {
    "path": "loco-gen/src/templates/data/mod.t",
    "content": "{% set module_name = name | snake_case -%}\n{% set struct_name = module_name | pascal_case -%}\nto: \"src/data/{{module_name}}.rs\"\nskip_exists: true\nmessage: \"Data loader `{{struct_name}}` was added successfully.\"\ninjections:\n- into: \"src/data/mod.rs\"\n  append: true\n  content: \"pub mod {{ module_name }};\"\n---\nuse loco_rs::{data, Result};\nuse serde::{Deserialize, Serialize};\nuse std::sync::OnceLock;\n\nconst DATA_FILE: &str = \"{{module_name}}/data.json\";\n\n#[derive(Debug, Clone, Serialize, Deserialize, Default)]\npub struct {{struct_name}} {\n    pub is_loaded: bool,\n}\n\n#[allow(dead_code)]\n/// Reads the data from the JSON file.\n///\n/// # Errors\n/// This function returns an error if the file cannot be read or deserialized.\npub async fn read() -> Result<{{struct_name}}> {\n    data::load_json_file(DATA_FILE).await\n}\n\nstatic DATA: OnceLock<{{struct_name}}> = OnceLock::new();\n#[allow(dead_code)]\npub fn get() -> &'static {{struct_name}} {\n    DATA.get_or_init(|| data::load_json_file_sync(DATA_FILE).unwrap_or_default())\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_access() {\n        assert!(&get().is_loaded);\n    }\n}\n"
  },
  {
    "path": "loco-gen/src/templates/data/struct.t",
    "content": "{% set module_name = name | snake_case -%}\n{% set struct_name = module_name | pascal_case -%}\nto: \"src/data/{{module_name}}.rs\"\nskip_exists: true\nmessage: \"Data loader `{{struct_name}}` was added successfully.\"\ninjections:\n- into: \"src/data/mod.rs\"\n  append: true\n  content: \"pub mod {{ module_name }};\"\n---\nuse loco_rs::{data, Result};\nuse serde::{Deserialize, Serialize};\nuse std::sync::OnceLock;\n\nconst DATA_FILE: &str = \"{{module_name}}/data.json\";\n\n#[derive(Debug, Clone, Serialize, Deserialize, Default)]\npub struct {{struct_name}} {\n    pub is_loaded: bool,\n}\n\n#[allow(dead_code)]\npub async fn read() -> Result<{{struct_name}}> {\n    data::load_json_file(DATA_FILE).await\n}\n\nstatic DATA: OnceLock<{{struct_name}}> = OnceLock::new();\n#[allow(dead_code)]\npub fn get() -> &'static {{struct_name}} {\n    DATA.get_or_init(|| data::load_json_file_sync(DATA_FILE).unwrap_or_default())\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_access() {\n        assert!(&get().is_loaded);\n    }\n}\n"
  },
  {
    "path": "loco-gen/src/templates/deployment/docker/docker.t",
    "content": "to: \"Dockerfile\"\nskip_exists: true\nmessage: \"Dockerfile generated successfully.\"\n---\nFROM rust:1.92.0-slim AS builder\n\nWORKDIR /usr/src/\n\nCOPY . .\n\n{% if is_client_side_rendering -%}\nRUN apt-get update && apt-get install -y curl ca-certificates\n\n# Install Node.js using the latest available version from NodeSource.\n# In production, replace \"setup_current.x\" with a specific version\n# to avoid unexpected breaking changes in future releases.\nRUN curl -fsSL https://deb.nodesource.com/setup_current.x | bash - && \\\n    apt-get install -y nodejs\nRUN cd frontend && npm install && npm run build\n{% endif -%}\n\nRUN cargo build --release\n\nFROM debian:bookworm-slim\n\nWORKDIR /usr/app\n\n{% for path in copy_paths -%}\nCOPY --from=builder /usr/src/{{path}} {{path}}\n{% endfor -%}\nCOPY --from=builder /usr/src/config config\nCOPY --from=builder /usr/src/target/release/{{pkg_name}}-cli {{pkg_name}}-cli\n\nENTRYPOINT [\"/usr/app/{{pkg_name}}-cli\"]\n"
  },
  {
    "path": "loco-gen/src/templates/deployment/docker/ignore.t",
    "content": "to: \".dockerignore\"\nskip_exists: true\nmessage: \"Dockerignore generated successfully.\"\n---\ntarget\nDockerfile\n.dockerignore\n.git\n.gitignore\n"
  },
  {
    "path": "loco-gen/src/templates/deployment/nginx/nginx.t",
    "content": "to: \"nginx/default.conf\"\nskip_exists: true\nmessage: \"Nginx generated successfully.\"\n---\nserver {\n  listen 80;\n  server_name ~^(?<subdomain>\\w*)\\.{{domain}}$;\n\n  location / {\n      if ($http_x_subdomain = \"\") {\n          set $http_x_subdomain $subdomain;\n      }\n      proxy_set_header X-Subdomain $http_x_subdomain;\n      proxy_pass http://{{domain}}:{{port}}/;\n  }\n}\n\nserver {\n  listen 80;\n  server_name {{domain}};\n\n  location / {\n      proxy_pass http://{{domain}}:{{port}}/;\n  }\n}\n"
  },
  {
    "path": "loco-gen/src/templates/mailer/html.t",
    "content": "{% set module_name = name | snake_case -%}\n{% set struct_name = module_name | pascal_case -%}\nto: \"src/mailers/{{module_name}}/welcome/html.t\"\nskip_exists: true\n---\nwelcome to <em>acmeworld!</em>\n"
  },
  {
    "path": "loco-gen/src/templates/mailer/mailer.t",
    "content": "{% set module_name = name | snake_case -%}\n{% set struct_name = module_name | pascal_case -%}\nto: \"src/mailers/{{module_name}}.rs\"\nskip_exists: true\nmessage: \"A mailer `{{struct_name}}` was added successfully.\"\ninjections:\n- into: \"src/mailers/mod.rs\"\n  append: true\n  content: \"pub mod {{ module_name }};\"\n---\n#![allow(non_upper_case_globals)]\n\nuse loco_rs::prelude::*;\nuse serde_json::json;\n\nstatic welcome: Dir<'_> = include_dir!(\"src/mailers/{{module_name}}/welcome\");\n\n#[allow(clippy::module_name_repetitions)]\npub struct {{struct_name}} {}\nimpl Mailer for {{struct_name}} {}\nimpl {{struct_name}} {\n    /// Send an email\n    ///\n    /// # Errors\n    /// When email sending is failed\n    pub async fn send_welcome(ctx: &AppContext, to: &str, msg: &str) -> Result<()> {\n        Self::mail_template(\n            ctx,\n            &welcome,\n            mailer::Args {\n                to: to.to_string(),\n                locals: json!({\n                  \"message\": msg,\n                  \"domain\": ctx.config.server.full_url()\n                }),\n                ..Default::default()\n            },\n        )\n        .await?;\n\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "loco-gen/src/templates/mailer/subject.t",
    "content": "{% set module_name = name | snake_case -%}\n{% set struct_name = module_name | pascal_case -%}\nto: \"src/mailers/{{module_name}}/welcome/subject.t\"\nskip_exists: true\n---\nguess what? welcome!\n"
  },
  {
    "path": "loco-gen/src/templates/mailer/text.t",
    "content": "{% set module_name = name | snake_case -%}\n{% set struct_name = module_name | pascal_case -%}\nto: \"src/mailers/{{module_name}}/welcome/text.t\"\nskip_exists: true\n---\nwelcome to acmeworld!\n"
  },
  {
    "path": "loco-gen/src/templates/migration/add_columns.t",
    "content": "{% set mig_ts = ts | date(format=\"%Y%m%d_%H%M%S\") -%}\n{% set mig_name = name | snake_case -%}\n{% set plural_snake = table | plural | snake_case -%}\n{% set module_name = \"m\" ~  mig_ts ~ \"_\" ~ mig_name -%}\nto: \"migration/src/{{module_name}}.rs\"\nskip_glob: \"migration/src/m????????_??????_{{mig_name}}.rs\"\nmessage: \"Migration `{{mig_name}}` added! You can now apply it with `$ cargo loco db migrate && cargo loco db entities`.\"\ninjections:\n- into: \"migration/src/lib.rs\"\n  before: \"inject-above\"\n  content: \"            Box::new({{module_name}}::Migration),\"\n- into: \"migration/src/lib.rs\"\n  before: \"pub struct Migrator\"\n  content: \"mod {{module_name}};\"\n---\nuse loco_rs::schema::*;\nuse sea_orm_migration::prelude::*;\n\n#[derive(DeriveMigrationName)]\npub struct Migration;\n\n#[async_trait::async_trait]\nimpl MigrationTrait for Migration {\n    async fn up(&self, m: &SchemaManager) -> Result<(), DbErr> {\n        {% for column in columns -%}\n        add_column(m, \"{{plural_snake}}\", \"{{column.0}}\", ColType::{{column.1}}).await?;\n        {% endfor -%}\n        Ok(())\n    }\n\n    async fn down(&self, m: &SchemaManager) -> Result<(), DbErr> {\n        {% for column in columns -%}\n        remove_column(m, \"{{plural_snake}}\", \"{{column.0}}\").await?;\n        {% endfor -%}\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "loco-gen/src/templates/migration/add_references.t",
    "content": "{% set mig_ts = ts | date(format=\"%Y%m%d_%H%M%S\") -%}\n{% set mig_name = name | snake_case -%}\n{% set plural_snake = table | plural | snake_case -%}\n{% set module_name = \"m\" ~  mig_ts ~ \"_\" ~ mig_name -%}\nto: \"migration/src/{{module_name}}.rs\"\nskip_glob: \"migration/src/m????????_??????_{{mig_name}}.rs\"\nmessage: \"Migration `{{mig_name}}` added! You can now apply it with `$ cargo loco db migrate && cargo loco db entities`.\"\ninjections:\n- into: \"migration/src/lib.rs\"\n  before: \"inject-above\"\n  content: \"            Box::new({{module_name}}::Migration),\"\n- into: \"migration/src/lib.rs\"\n  before: \"pub struct Migrator\"\n  content: \"mod {{module_name}};\"\n---\nuse loco_rs::schema::*;\nuse sea_orm_migration::prelude::*;\n\n#[derive(DeriveMigrationName)]\npub struct Migration;\n\n#[async_trait::async_trait]\nimpl MigrationTrait for Migration {\n    async fn up(&self, m: &SchemaManager) -> Result<(), DbErr> {\n        {% for column in columns -%}\n        add_column(m, \"{{plural_snake}}\", \"{{column.0}}\", ColType::{{column.1}}).await?;\n        {% endfor -%}\n\n        {% for ref in references -%}\n        add_reference(m, \"{{plural_snake}}\", \"{{ref.0}}\", \"{{ref.1}}\").await?;\n        {% endfor -%}\n\n        Ok(())\n    }\n\n    async fn down(&self, m: &SchemaManager) -> Result<(), DbErr> {\n        {% for ref in references -%}\n        remove_reference(m, \"{{plural_snake}}\", \"{{ref.0}}\", \"{{ref.1}}\").await?;\n        {% endfor -%}\n\n        {% for column in columns -%}\n        remove_column(m, \"{{plural_snake}}\", \"{{column.0}}\").await?;\n        {% endfor -%}\n        \n        Ok(())\n    }\n}\n"
  },
  {
    "path": "loco-gen/src/templates/migration/empty.t",
    "content": "{% set mig_ts = ts | date(format=\"%Y%m%d_%H%M%S\") -%}\n{% set mig_name = name | snake_case -%}\n{% set module_name = \"m\" ~  mig_ts ~ \"_\" ~ mig_name -%}\nto: \"migration/src/{{module_name}}.rs\"\nskip_glob: \"migration/src/*_{{mig_name}}.rs\"\nmessage: \"Migration for `{{name}}` added! You can now apply it with `$ cargo loco db migrate && cargo loco db entities`.\"\ninjections:\n- into: \"migration/src/lib.rs\"\n  before: \"inject-above\"\n  content: \"            Box::new({{module_name}}::Migration),\"\n- into: \"migration/src/lib.rs\"\n  before: \"pub struct Migrator\"\n  content: \"mod {{module_name}};\"\n---\nuse sea_orm_migration::{prelude::*, schema::*};\n\n#[derive(DeriveMigrationName)]\npub struct Migration;\n\n#[async_trait::async_trait]\nimpl MigrationTrait for Migration {\n    async fn up(&self, m: &SchemaManager) -> Result<(), DbErr> {\n        todo!()\n    }\n\n    async fn down(&self, m: &SchemaManager) -> Result<(), DbErr> {\n        Ok(())\n    }\n}\n\n"
  },
  {
    "path": "loco-gen/src/templates/migration/join_table.t",
    "content": "{% set mig_ts = ts | date(format=\"%Y%m%d_%H%M%S\") -%}\n{% set plural_snake = name | plural | snake_case -%}\n{% set module_name = \"m\" ~  mig_ts ~ \"_\" ~ plural_snake -%}\n{% set plural_snake = table | plural | snake_case -%}\n{% if with_tz %}\n{% set join_table_func = \"create_join_table\" %}\n{% else %}\n{% set join_table_func = \"create_join_table_without_timestamps\" %}\n{% endif %}\nto: \"migration/src/{{module_name}}.rs\"\nskip_glob: \"migration/src/m????????_??????_{{plural_snake}}.rs\"\nmessage: \"Migration for `{{name}}` added! You can now apply it with `$ cargo loco db migrate && cargo loco db entities`.\"\ninjections:\n- into: \"migration/src/lib.rs\"\n  before: \"inject-above\"\n  content: \"            Box::new({{module_name}}::Migration),\"\n- into: \"migration/src/lib.rs\"\n  before: \"pub struct Migrator\"\n  content: \"mod {{module_name}};\"\n---\nuse loco_rs::schema::*;\nuse sea_orm_migration::prelude::*;\n\n#[derive(DeriveMigrationName)]\npub struct Migration;\n\n#[async_trait::async_trait]\nimpl MigrationTrait for Migration {\n    async fn up(&self, m: &SchemaManager) -> Result<(), DbErr> {\n        {{join_table_func}}(m, \"{{plural_snake}}\",\n            &[\n            {% for column in columns -%}\n            (\"{{column.0}}\", ColType::{{column.1}}),\n            {% endfor -%}\n            ],\n            &[\n            {% for ref in references -%}\n            (\"{{ref.0}}\", \"{{ref.1}}\"),\n            {% endfor -%}\n            ]\n        ).await\n    }\n\n    async fn down(&self, m: &SchemaManager) -> Result<(), DbErr> {\n        drop_table(m, \"{{plural_snake}}\").await\n    }\n}\n"
  },
  {
    "path": "loco-gen/src/templates/migration/remove_columns.t",
    "content": "{% set mig_ts = ts | date(format=\"%Y%m%d_%H%M%S\") -%}\n{% set mig_name = name | snake_case -%}\n{% set plural_snake = table | plural | snake_case -%}\n{% set module_name = \"m\" ~  mig_ts ~ \"_\" ~ mig_name -%}\nto: \"migration/src/{{module_name}}.rs\"\nskip_glob: \"migration/src/m????????_??????_{{mig_name}}.rs\"\nmessage: \"Migration `{{mig_name}}` added! You can now apply it with `$ cargo loco db migrate && cargo loco db entities`.\"\ninjections:\n- into: \"migration/src/lib.rs\"\n  before: \"inject-above\"\n  content: \"            Box::new({{module_name}}::Migration),\"\n- into: \"migration/src/lib.rs\"\n  before: \"pub struct Migrator\"\n  content: \"mod {{module_name}};\"\n---\nuse loco_rs::schema::*;\nuse sea_orm_migration::prelude::*;\n\n#[derive(DeriveMigrationName)]\npub struct Migration;\n\n#[async_trait::async_trait]\nimpl MigrationTrait for Migration {\n    async fn up(&self, m: &SchemaManager) -> Result<(), DbErr> {\n        {% for column in columns -%}\n        remove_column(m, \"{{plural_snake}}\", \"{{column.0}}\").await?;\n        {% endfor -%}\n        Ok(())\n    }\n\n    async fn down(&self, m: &SchemaManager) -> Result<(), DbErr> {\n        {% for column in columns -%}\n        add_column(m, \"{{plural_snake}}\", \"{{column.0}}\", ColType::{{column.1}}).await?;\n        {% endfor -%}\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "loco-gen/src/templates/model/model.t",
    "content": "{% set mig_ts = ts | date(format=\"%Y%m%d_%H%M%S\") -%}\n{% set plural_snake = name | plural | snake_case -%}\n{% set module_name = \"m\" ~  mig_ts ~ \"_\" ~ plural_snake -%}\n{% set model = name | plural | pascal_case -%}\n{% if with_tz %}\n{% set create_table_func = \"create_table\" %}\n{% else %}\n{% set create_table_func = \"create_table_without_timestamps\" %}\n{% endif %}\nto: \"migration/src/{{module_name}}.rs\"\nskip_glob: \"migration/src/m????????_??????_{{plural_snake}}.rs\"\nmessage: \"Migration for `{{name}}` added! You can now apply it with `$ cargo loco db migrate && cargo loco db entities`.\"\ninjections:\n- into: \"migration/src/lib.rs\"\n  before: \"inject-above\"\n  content: \"            Box::new({{module_name}}::Migration),\"\n- into: \"migration/src/lib.rs\"\n  before: \"pub struct Migrator\"\n  content: \"mod {{module_name}};\"\n---\nuse loco_rs::schema::*;\nuse sea_orm_migration::prelude::*;\n\n#[derive(DeriveMigrationName)]\npub struct Migration;\n\n#[async_trait::async_trait]\nimpl MigrationTrait for Migration {\n    async fn up(&self, m: &SchemaManager) -> Result<(), DbErr> {\n        {{create_table_func}}(m, \"{{plural_snake}}\",\n            &[\n            {% if columns | length > 0 %}\n            (\"id\", ColType::PkAuto),\n            {% endif %}\n            {% for column in columns -%}\n            (\"{{column.0}}\", ColType::{{column.1}}),\n            {% endfor -%}\n            ],\n            &[\n            {% for ref in references -%}\n            (\"{{ref.0}}\", \"{{ref.1}}\"),\n            {% endfor -%}\n            ]\n        ).await\n    }\n\n    async fn down(&self, m: &SchemaManager) -> Result<(), DbErr> {\n        drop_table(m, \"{{plural_snake}}\").await\n    }\n}\n"
  },
  {
    "path": "loco-gen/src/templates/model/test.t",
    "content": "{% set plural_snake = name | plural | snake_case -%}\n{% set model = name | plural | pascal_case -%}\nto: \"tests/models/{{plural_snake}}.rs\"\nmessage: \"A test for model `{{model}}` was added. Run with `cargo test`.\"\nskip_exists: true\ninjections:\n- into: \"tests/models/mod.rs\"\n  append: true\n  content: \"mod {{plural_snake}};\"\n---\nuse {{pkg_name}}::app::App;\nuse loco_rs::testing::prelude::*;\nuse serial_test::serial;\n\nmacro_rules! configure_insta {\n    ($($expr:expr),*) => {\n        let mut settings = insta::Settings::clone_current();\n        settings.set_prepend_module_to_snapshot(false);\n        let _guard = settings.bind_to_scope();\n    };\n}\n\n#[tokio::test]\n#[serial]\nasync fn test_model() {\n    configure_insta!();\n\n    let boot = boot_test::<App>().await.unwrap();\n    seed::<App>(&boot.app_context).await.unwrap();\n\n    // query your model, e.g.:\n    //\n    // let item = models::posts::Model::find_by_pid(\n    //     &boot.app_context.db,\n    //     \"11111111-1111-1111-1111-111111111111\",\n    // )\n    // .await;\n\n    // snapshot the result:\n    // assert_debug_snapshot!(item);\n}\n"
  },
  {
    "path": "loco-gen/src/templates/scaffold/api/controller.t",
    "content": "{% set file_name = name |  snake_case -%}\n{% set module_name = file_name | pascal_case -%}\nto: src/controllers/{{ file_name }}.rs\nskip_exists: true\nmessage: \"Controller `{{module_name}}` was added successfully.\"\ninjections:\n- into: src/controllers/mod.rs\n  append: true\n  content: \"pub mod {{ file_name }};\"\n- into: src/app.rs\n  after: \"AppRoutes::\"\n  content: \"            .add_route(controllers::{{ file_name }}::routes())\"\n---\n#![allow(clippy::missing_errors_doc)]\n#![allow(clippy::unnecessary_struct_initialization)]\n#![allow(clippy::unused_async)]\nuse loco_rs::prelude::*;\nuse serde::{Deserialize, Serialize};\n\nuse crate::models::_entities::{{file_name | plural}}::{ActiveModel, Entity, Model};\n\n#[derive(Clone, Debug, Serialize, Deserialize)]\npub struct Params {\n    {% for column in columns -%}\n    {%- if column.2 == \"IntegerNull\" -%}\n    pub {{column.0}}: Option<i32>,\n    {%- else -%}\n    pub {{column.0}}: {{column.1}},\n    {%- endif %}\n    {% endfor -%}\n}\n\nimpl Params {\n    fn update(&self, item: &mut ActiveModel) {\n      {% for column in columns -%}\n      {%- if \"Vec<\" in column.1 -%}\n      item.{{column.0}} = Set(self.{{column.0}}.clone());\n      {%- elif column.2 == \"IntegerNull\" -%}\n      item.{{column.0}} = Set(self.{{column.0}});\n      {%- elif \"i32\" in column.1 or \"i64\" in column.1 or \"i16\" in column.1 or \"Uuid\" in column.1 or \"f32\" in column.1 or \"f64\" in column.1 or \"Decimal\" in column.1 or \"bool\" in column.1 or \"Date\" in column.1 or \"DateTime\" in column.1 or \"DateTimeWithTimeZone\" in column.1 -%}\n      item.{{column.0}} = Set(self.{{column.0}});\n      {%- else -%}\n      item.{{column.0}} = Set(self.{{column.0}}.clone());\n      {%- endif %}\n      {% endfor -%}\n    }\n}\n\nasync fn load_item(ctx: &AppContext, id: i32) -> Result<Model> {\n    let item = Entity::find_by_id(id).one(&ctx.db).await?;\n    item.ok_or_else(|| Error::NotFound)\n}\n\n#[debug_handler]\npub async fn list(State(ctx): State<AppContext>) -> Result<Response> {\n    format::json(Entity::find().all(&ctx.db).await?)\n}\n\n#[debug_handler]\npub async fn add(State(ctx): State<AppContext>, Json(params): Json<Params>) -> Result<Response> {\n    let mut item = ActiveModel {\n        ..Default::default()\n    };\n    params.update(&mut item);\n    let item = item.insert(&ctx.db).await?;\n    format::json(item)\n}\n\n#[debug_handler]\npub async fn update(\n    Path(id): Path<i32>,\n    State(ctx): State<AppContext>,\n    Json(params): Json<Params>,\n) -> Result<Response> {\n    let item = load_item(&ctx, id).await?;\n    let mut item = item.into_active_model();\n    params.update(&mut item);\n    let item = item.update(&ctx.db).await?;\n    format::json(item)\n}\n\n#[debug_handler]\npub async fn remove(Path(id): Path<i32>, State(ctx): State<AppContext>) -> Result<Response> {\n    load_item(&ctx, id).await?.delete(&ctx.db).await?;\n    format::empty()\n}\n\n#[debug_handler]\npub async fn get_one(Path(id): Path<i32>, State(ctx): State<AppContext>) -> Result<Response> {\n    format::json(load_item(&ctx, id).await?)\n}\n\npub fn routes() -> Routes {\n    Routes::new()\n        .prefix(\"api/{{file_name | plural}}/\")\n        .add(\"/\", get(list))\n        .add(\"/\", post(add))\n        .add(\"{id}\", get(get_one))\n        .add(\"{id}\", delete(remove))\n        .add(\"{id}\", put(update))\n        .add(\"{id}\", patch(update))\n}\n"
  },
  {
    "path": "loco-gen/src/templates/scaffold/api/test.t",
    "content": "{% set file_name = name |  snake_case -%}\n{% set module_name = file_name | pascal_case -%}\nto: tests/requests/{{ file_name }}.rs\nskip_exists: true\nmessage: \"Tests for controller `{{module_name}}` was added successfully. Run `cargo test`.\"\ninjections:\n- into: tests/requests/mod.rs\n  append: true\n  content: \"pub mod {{ file_name }};\"\n---\nuse {{pkg_name}}::app::App;\nuse loco_rs::testing::prelude::*;\nuse serial_test::serial;\n\n#[tokio::test]\n#[serial]\nasync fn can_get_{{ name | plural | snake_case }}() {\n    request::<App, _, _>(|request, _ctx| async move {\n        let res = request.get(\"/api/{{ name | plural | snake_case }}/\").await;\n        assert_eq!(res.status_code(), 200);\n\n        // you can assert content like this:\n        // assert_eq!(res.text(), \"content\");\n    })\n    .await;\n}\n"
  },
  {
    "path": "loco-gen/src/templates/scaffold/html/base.t",
    "content": "to: assets/views/base.html\nskip_exists: true\nmessage: \"Base template was added successfully.\"\n---\n\n<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n  <meta charset=\"utf-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n  <title>{% raw %}{% block title %}{% endblock title %}{% endraw %}</title>\n  <script src=\"https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio,line-clamp\"></script>\n  {% raw %}{% block head %}{% endraw %}\n\n  {% raw %}{% endblock head %}{% endraw %}\n</head>\n\n<body class=\"min-h-screen bg-background font-sans antialiased\">\n    <div class=\"relative flex min-h-screen flex-col bg-background\">\n        <div class=\"themes-wrapper bg-background\">\n                <main class=\"relative flex min-h-svh flex-1 flex-col bg-background peer-data-[variant=inset]:min-h-[calc(100svh-theme(spacing.4))] md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow\">\n                    <div class=\"flex flex-1 flex-col gap-4 p-5 pt-5\">\n                        <h1 class=\"scroll-m-20 text-3xl font-bold tracking-tight\">\n                            {% raw %}{% block page_title %}{% endblock page_title %}{% endraw %}\n                        </h1>\n                        {% raw %}{% block content %}\n                        {% endblock content %}{% endraw %}\n                    </div>\n                </main>\n        </div>\n    </div>\n  {% raw %}{% block js %}\n\n  {% endblock js %}{% endraw %}\n\n  <script>\n    function confirmDelete(event, delete_url, redirect_to) {\n        event.preventDefault();\n        if (confirm(\"Are you sure you want to delete this item?\")) {\n            var xhr = new XMLHttpRequest();\n            xhr.open(\"DELETE\", delete_url, true);\n            xhr.onreadystatechange = function () {\n                if (xhr.readyState == 4 && xhr.status == 200) {\n                    window.location.href = redirect_to;\n                }\n            };\n            xhr.send();\n        }\n    }\n\n    document.addEventListener('DOMContentLoaded', function () {\n            document.querySelectorAll('.add-more').forEach(button => {\n                button.addEventListener('click', function () {\n                    const group = this.getAttribute('data-group');\n                    const first = document.getElementById(`${group}-inputs`).querySelector('input');\n                    if (first) {\n                        const clonedInput = first.cloneNode();\n                        clonedInput.value = '';\n                        const container = document.getElementById(`${group}-inputs`);\n                        container.appendChild(clonedInput);\n                    } \n                });\n            });\n        });\n  </script>\n</body>\n\n</html>"
  },
  {
    "path": "loco-gen/src/templates/scaffold/html/controller.t",
    "content": "{% set file_name = name |  snake_case -%}\n{% set module_name = file_name | pascal_case -%}\nto: src/controllers/{{ file_name }}.rs\nskip_exists: true\nmessage: \"Controller `{{module_name}}` was added successfully.\"\ninjections:\n- into: src/controllers/mod.rs\n  append: true\n  content: \"pub mod {{ file_name }};\"\n- into: src/app.rs\n  after: \"AppRoutes::\"\n  content: \"            .add_route(controllers::{{ file_name }}::routes())\"\n---\n#![allow(clippy::missing_errors_doc)]\n#![allow(clippy::unnecessary_struct_initialization)]\n#![allow(clippy::unused_async)]\nuse loco_rs::prelude::*;\nuse serde::{Deserialize, Serialize};\nuse axum::response::Redirect;\nuse axum_extra::extract::Form;\nuse sea_orm::{sea_query::Order, QueryOrder};\n\nuse crate::{\n    models::_entities::{{file_name | plural}}::{ActiveModel, Column, Entity, Model},\n    views,\n};\n\n#[derive(Clone, Debug, Serialize, Deserialize)]\npub struct Params {\n    {% for column in columns -%}\n    {%- if column.2 == \"IntegerNull\" -%}\n    pub {{column.0}}: Option<i32>,\n    {%- else -%}\n    pub {{column.0}}: {{column.1}},\n    {%- endif %}\n    {% endfor -%}\n}\n\nimpl Params {\n    fn update(&self, item: &mut ActiveModel) {\n      {% for column in columns -%}\n      {%- if \"Vec<\" in column.1 -%}\n      item.{{column.0}} = Set(self.{{column.0}}.clone());\n      {%- elif column.2 == \"IntegerNull\" -%}\n      item.{{column.0}} = Set(self.{{column.0}});\n      {%- elif \"i32\" in column.1 or \"i64\" in column.1 or \"i16\" in column.1 or \"Uuid\" in column.1 or \"f32\" in column.1 or \"f64\" in column.1 or \"Decimal\" in column.1 or \"bool\" in column.1 or \"Date\" in column.1 or \"DateTime\" in column.1 or \"DateTimeWithTimeZone\" in column.1 -%}\n      item.{{column.0}} = Set(self.{{column.0}});\n      {%- else -%}\n      item.{{column.0}} = Set(self.{{column.0}}.clone());\n      {%- endif %}\n      {% endfor -%}\n    }\n}\n\nasync fn load_item(ctx: &AppContext, id: i32) -> Result<Model> {\n    let item = Entity::find_by_id(id).one(&ctx.db).await?;\n    item.ok_or_else(|| Error::NotFound)\n}\n\n#[debug_handler]\npub async fn list(\n    ViewEngine(v): ViewEngine<TeraView>,\n    State(ctx): State<AppContext>,\n) -> Result<Response> {\n    let item = Entity::find()\n        .order_by(Column::Id, Order::Desc)\n        .all(&ctx.db)\n        .await?;\n    views::{{file_name}}::list(&v, &item)\n}\n\n#[debug_handler]\npub async fn new(\n    ViewEngine(v): ViewEngine<TeraView>,\n    State(_ctx): State<AppContext>,\n) -> Result<Response> {\n    views::{{file_name}}::create(&v)\n}\n\n#[debug_handler]\npub async fn update(\n    Path(id): Path<i32>,\n    State(ctx): State<AppContext>,\n    Form(params): Form<Params>,\n) -> Result<Redirect> {\n    let item = load_item(&ctx, id).await?;\n    let mut item = item.into_active_model();\n    params.update(&mut item);\n    item.update(&ctx.db).await?;\n    Ok(Redirect::to(\"../{{file_name | plural}}\"))\n}\n\n#[debug_handler]\npub async fn edit(\n    Path(id): Path<i32>,\n    ViewEngine(v): ViewEngine<TeraView>,\n    State(ctx): State<AppContext>,\n) -> Result<Response> {\n    let item = load_item(&ctx, id).await?;\n    views::{{file_name}}::edit(&v, &item)\n}\n\n#[debug_handler]\npub async fn show(\n    Path(id): Path<i32>,\n    ViewEngine(v): ViewEngine<TeraView>,\n    State(ctx): State<AppContext>,\n) -> Result<Response> {\n    let item = load_item(&ctx, id).await?;\n    views::{{file_name}}::show(&v, &item)\n}\n\n#[debug_handler]\npub async fn add(\n    State(ctx): State<AppContext>,\n    Form(params): Form<Params>,\n) -> Result<Redirect> {\n    let mut item = ActiveModel {\n        ..Default::default()\n    };\n    params.update(&mut item);\n    item.insert(&ctx.db).await?;\n    Ok(Redirect::to(\"{{file_name | plural}}\"))\n}\n\n#[debug_handler]\npub async fn remove(Path(id): Path<i32>, State(ctx): State<AppContext>) -> Result<Response> {\n    load_item(&ctx, id).await?.delete(&ctx.db).await?;\n    format::empty()\n}\n\npub fn routes() -> Routes {\n    Routes::new()\n        .prefix(\"{{file_name | plural}}/\")\n        .add(\"/\", get(list))\n        .add(\"/\", post(add))\n        .add(\"new\", get(new))\n        .add(\"{id}\", get(show))\n        .add(\"{id}/edit\", get(edit))\n        .add(\"{id}\", delete(remove))\n        .add(\"{id}\", post(update))\n}\n"
  },
  {
    "path": "loco-gen/src/templates/scaffold/html/view.t",
    "content": "{% set file_name = name |  snake_case -%}\n{% set module_name = file_name | pascal_case -%}\nto: src/views/{{file_name}}.rs\nskip_exists: true\nmessage: \"{{file_name}} view was added successfully.\"\ninjections:\n- into: src/views/mod.rs\n  append: true\n  content: \"pub mod {{ file_name }};\"\n---\nuse loco_rs::prelude::*;\n\nuse crate::models::_entities::{{file_name | plural}};\n\n/// Render a list view of `{{name | plural}}`.\n///\n/// # Errors\n///\n/// When there is an issue with rendering the view.\npub fn list(v: &impl ViewRenderer, items: &Vec<{{file_name | plural}}::Model>) -> Result<Response> {\n    format::render().view(v, \"{{file_name}}/list.html\", data!({\"items\": items}))\n}\n\n/// Render a single `{{name}}` view.\n///\n/// # Errors\n///\n/// When there is an issue with rendering the view.\npub fn show(v: &impl ViewRenderer, item: &{{file_name | plural}}::Model) -> Result<Response> {\n    format::render().view(v, \"{{file_name}}/show.html\", data!({\"item\": item}))\n}\n\n/// Render a `{{name}}` create form.\n///\n/// # Errors\n///\n/// When there is an issue with rendering the view.\npub fn create(v: &impl ViewRenderer) -> Result<Response> {\n    format::render().view(v, \"{{file_name}}/create.html\", data!({}))\n}\n\n/// Render a `{{name}}` edit form.\n///\n/// # Errors\n///\n/// When there is an issue with rendering the view.\npub fn edit(v: &impl ViewRenderer, item: &{{file_name | plural}}::Model) -> Result<Response> {\n    format::render().view(v, \"{{file_name}}/edit.html\", data!({\"item\": item}))\n}\n"
  },
  {
    "path": "loco-gen/src/templates/scaffold/html/view_create.t",
    "content": "{% set file_name = name |  snake_case -%}\n{% set module_name = file_name | pascal_case -%}\nto: assets/views/{{file_name}}/create.html\nskip_exists: true\nmessage: \"{{file_name}} create view was added successfully.\"\n---\n{% raw %}{% extends \"base.html\" %}{% endraw %}\n\n{% raw %}{% block title %}{% endraw %}\nCreate {{file_name}}\n{% raw %}{% endblock title %}{% endraw %}\n\n{% raw %}{% block page_title %}{% endraw %}\nCreate new {{name}}\n{% raw %}{% endblock page_title %}{% endraw %}\n\n{% raw %}{% block content %}{% endraw %}\n<div class=\"mb-10\">\n    <form action=\"/{{name | plural}}\" method=\"post\" class=\"flex-1 lg:max-w-2xl\">\n    {% for column in columns -%}\n            {{ render_form_field(fname=column.0, rust_type=column.1, ftype=column.2)}}\n        {% endfor -%}\n        <div class=\"mt-5\">\n            <button class=\" text-xs py-3 px-6 rounded-lg bg-gray-900 text-white\" type=\"submit\">Submit</button>\n        </div>\n    </form>\n<br />\n<a href=\"/{{name | plural}}\">Back to {{name | plural}}</a>\n</div>\n{% raw %}{% endblock content %}{% endraw %}\n\n{% raw %}{% block js %}{% endraw %}\n\n{% raw %}{% endblock js %}{% endraw %}"
  },
  {
    "path": "loco-gen/src/templates/scaffold/html/view_edit.t",
    "content": "{% set file_name = name |  snake_case -%}\n{% set module_name = file_name | pascal_case -%}\nto: assets/views/{{file_name}}/edit.html\nskip_exists: true\nmessage: \"{{file_name}} edit view was added successfully.\"\n---\n{% raw %}{% extends \"base.html\" %}{% endraw %}\n\n{% raw %}{% block title %}{% endraw %}\nEdit {{name}}: {% raw %}{{ item.id }}{% endraw %}\n{% raw %}{% endblock title %}{% endraw %}\n\n{% raw %}{% block page_title %}{% endraw %}\nEdit {{name}}: {% raw %}{{ item.id }}{% endraw %}\n{% raw %}{% endblock page_title %}{% endraw %}\n\n{% raw %}{% block content %}{% endraw %}\n<div class=\"mb-10\">\n    <form action=\"/{{name | plural}}/{% raw %}{{ item.id }}{% endraw %}\" method=\"post\" class=\"flex-1 lg:max-w-2xl\">\n    {% for column in columns -%}\n            {{ render_form_field(fname=column.0, rust_type=column.1, ftype=column.2, edit_form=true)}}\n        {% endfor -%}\n        <div>\n            <div class=\"mt-5\">\n                <button class=\" text-xs py-3 px-6 rounded-lg bg-gray-900 text-white\" type=\"submit\">Submit</button>\n                <button class=\"text-xs py-3 px-6 rounded-lg bg-red-600 text-white\"\n                            onclick=\"confirmDelete(event, '/{{name | plural}}/{% raw %}{{ item.id }}{% endraw %}', '/{{name | plural}}' )\">Delete</button>\n            </div>\n        </div> \n    </form>\n    <div id=\"success-message\" class=\"mt-4\"></div>\n    <br />\n    <a href=\"/{{name | plural}}\">Back to {{name}}</a>\n</div>\n{% raw %}{% endblock content %}{% endraw %}\n\n{% raw %}{% block js %}{% endraw %}\n\n{% raw %}{% endblock js %}{% endraw %}"
  },
  {
    "path": "loco-gen/src/templates/scaffold/html/view_list.t",
    "content": "{% set file_name = name |  snake_case -%}\n{% set module_name = file_name | pascal_case -%}\nto: assets/views/{{file_name}}/list.html\nskip_exists: true\nmessage: \"{{file_name}} list view was added successfully.\"\n---\n{% raw %}{% extends \"base.html\" %}{% endraw %}\n\n{% raw %}{% block title %}{% endraw %}\nList of {{file_name}}\n{% raw %}{% endblock title %}{% endraw %}\n\n{% raw %}{% block page_title %}{% endraw %}\n{{file_name}}\n{% raw %}{% endblock page_title %}{% endraw %}\n\n{% raw %}{% block content %}{% endraw %}\n<div class=\"mb-10\">\n\n    {% raw %}{% if items %}{% endraw %}\n\n    <div class=\"mb-5\">\n        <div class=\"relative w-full overflow-auto\">\n            <table class=\"w-full caption-bottom text-sm\">\n                <thead class=\"[&amp;_tr]:border-b\">\n                    <tr class=\"border-b transition-colors hover:bg-muted/50\">\n                        {% for column in columns -%}\n                        <th class=\"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&amp;:has([role=checkbox])]:pr-0 [&amp;>[role=checkbox]]:translate-y-[2px] w-[100px]\">\n                            {% raw %}{{\"{% endraw %}{{column.0}}{% raw %}\" | capitalize }}{% endraw %}\n                        </th>\n                        {% endfor -%}\n                        <th class=\"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&amp;:has([role=checkbox])]:pr-0 [&amp;>[role=checkbox]]:translate-y-[2px] w-[100px]\">\n                           Actions\n                        </th>\n                    </tr>\n                </thead>\n                <tbody class=\"[&amp;_tr:last-child]:border-0\">\n                   {% raw %}{% for item in items %}{% endraw %}\n                    <tr class=\"border-b transition-colors hover:bg-muted/50\">\n                        {% for column in columns -%}\n                          <td\n                            class=\"p-2 align-middle  font-medium\">\n                            {{ render_view_field(fname=column.0, rust_type=column.1) }}\n                        </td>\n                        {% endfor -%}\n                        <td>\n                            <a href=\"/{{name | plural}}/{% raw %}{{ item.id }}{% endraw %}/edit\">Edit</a>\n                        </td>\n                    </tr>\n                    {% raw %}{% endfor %}{% endraw %}\n                </tbody>\n            </table>\n        </div>\n    \n        <div class=\"flex\">\n            <div class=\"ml-auto  p-4\">\n                <a href=\"/{{name | plural}}/new\"\n                    class=\"mt-5 bg-blue-500 text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800\">\n                    Create\n                </a>\n            </div>\n        </div>\n    </div>\n\n    {% raw %}{% else %}{% endraw %}\n\n    <div class=\"mt-10 flex items-center justify-center\">\n        <div class=\"bg-white rounded-lg shadow-lg p-8 max-w-4xl w-full flex flex-col items-center\">\n            <h3 class=\"font-bold text-lg\">Nothing Here Yet</h3>\n            There are no records to display. Add a new record to get started!\n            <a href=\"/{{name | plural}}/new\"\n            class=\"mt-5 bg-blue-500 text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800\">\n            Create\n        </a>\n        </div>\n    </div>\n   \n    {% raw %}{% endif %}{% endraw %}\n\n    \n</div>\n{% raw %}{% endblock content %}{% endraw %}"
  },
  {
    "path": "loco-gen/src/templates/scaffold/html/view_show.t",
    "content": "{% set file_name = name |  snake_case -%}\n{% set module_name = file_name | pascal_case -%}\nto: assets/views/{{file_name}}/show.html\nskip_exists: true\nmessage: \"{{file_name}} view was added successfully.\"\n---\n{% raw %}{% extends \"base.html\" %}{% endraw %}\n\n{% raw %}{% block title %}{% endraw %}\nView {{name}}: {% raw %}{{ item.id }}{% endraw %}\n{% raw %}{% endblock title %}{% endraw %}\n\n{% raw %}{% block content %}{% endraw %}\n<h1>View {{name}}: {% raw %}{{ item.id }}{% endraw %}</h1>\n<div class=\"mb-10\">\n{% for column in columns -%}\n    <div>\n        <label>{{column.0}}: {% raw %}{{item.{% endraw %}{{column.0}}{% raw %}}}{% endraw %}</label>\n    </div>\n{% endfor -%}\n<br />\n<a href=\"/{{name | plural}}\">Back to {{name | plural}}</a>\n</div>\n{% raw %}{% endblock content %}{% endraw %}"
  },
  {
    "path": "loco-gen/src/templates/scaffold/htmx/base.t",
    "content": "to: assets/views/base.html\nskip_exists: true\nmessage: \"Base template was added successfully.\"\n---\n\n<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n  <meta charset=\"utf-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n  <title>{% raw %}{% block title %}{% endblock title %}{% endraw %}</title>\n\n  <script src=\"https://unpkg.com/htmx.org@2.0.0/dist/htmx.min.js\"></script>\n  <script src=\"https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio,line-clamp\"></script>\n  {% raw %}{% block head %}{% endraw %}\n\n  {% raw %}{% endblock head %}{% endraw %}\n</head>\n\n<body class=\"min-h-screen bg-background font-sans antialiased\">\n    <div class=\"relative flex min-h-screen flex-col bg-background\">\n        <div class=\"themes-wrapper bg-background\">\n            <main>\n                <div class=\"flex flex-1 flex-col gap-4 p-5 pt-5\">\n                    <h1 class=\"scroll-m-20 text-3xl font-bold tracking-tight\">\n                        {% raw %}{% block page_title %}{% endblock page_title %}{% endraw %}\n                    </h1>\n                    {% raw %}{% block content %}\n                    {% endblock content %}{% endraw %}\n                </div>\n            </main>\n        </div>\n    </div>\n  {% raw %}{% block js %}\n\n  {% endblock js %}{% endraw %}\n\n  <script>\n  htmx.defineExtension('submitjson', {\n        onEvent: function (name, evt) {\n            if (name === \"htmx:configRequest\") {\n                evt.detail.headers['Content-Type'] = \"application/json\"\n            }\n        },\n        encodeParameters: function (xhr, parameters, elt) {\n                const json = {};\n                for (const [key, inputValue] of Object.entries(parameters)) {\n                    let origInputType = elt.querySelector(`[name=${key}]`).type;\n                    const customType = elt.querySelector(`[name=${key}]`).getAttribute(\"custom_type\");\n\n                    let value = inputValue;\n                    if (customType == \"array\" && !Array.isArray(inputValue)) {\n                        value = [inputValue]\n                    }\n\n                    if (origInputType === 'number') {\n                        if (Array.isArray(value)) {\n                            json[key] = Object.values(value).map(str => parseFloat(str))\n                        } else {\n                            json[key] = parseFloat(value)\n                        }\n                    } else if (origInputType === 'checkbox') {\n                        const val = elt.querySelector(`[name=${key}]`).checked;\n                        json[key] = val\n                    } else if (customType === 'blob') {\n                        json[key] = value.split(\",\").map(num => parseInt(num, 10));\n                    } else {\n                        json[key] = value;\n                    }\n                }\n                return JSON.stringify(json);\n            }\n  })\n  function confirmDelete(event, delete_url, redirect_to) {\n        event.preventDefault();\n        if (confirm(\"Are you sure you want to delete this item?\")) {\n            var xhr = new XMLHttpRequest();\n            xhr.open(\"DELETE\", delete_url, true);\n            xhr.onreadystatechange = function () {\n                if (xhr.readyState == 4 && xhr.status == 200) {\n                    window.location.href = redirect_to;\n                }\n            };\n            xhr.send();\n        }\n    }\n\n    document.addEventListener('DOMContentLoaded', function () {\n            document.querySelectorAll('.add-more').forEach(button => {\n                button.addEventListener('click', function () {\n                    const group = this.getAttribute('data-group');\n                    const first = document.getElementById(`${group}-inputs`).querySelector('input');\n                    if (first) {\n                        const clonedInput = first.cloneNode();\n                        clonedInput.value = '';\n                        const container = document.getElementById(`${group}-inputs`);\n                        container.appendChild(clonedInput);\n                    } \n                });\n            });\n    });\n\n    document.body.addEventListener('htmx:responseError', function (event) {\n        const target = document.querySelector('#error-message');\n        const errorResponse = event.detail.xhr.response;\n        target.innerHTML = errorResponse\n    });\n  </script>\n</body>\n\n</html>"
  },
  {
    "path": "loco-gen/src/templates/scaffold/htmx/controller.t",
    "content": "{% set file_name = name |  snake_case -%}\n{% set module_name = file_name | pascal_case -%}\nto: src/controllers/{{ file_name }}.rs\nskip_exists: true\nmessage: \"Controller `{{module_name}}` was added successfully.\"\ninjections:\n- into: src/controllers/mod.rs\n  append: true\n  content: \"pub mod {{ file_name }};\"\n- into: src/app.rs\n  after: \"AppRoutes::\"\n  content: \"            .add_route(controllers::{{ file_name }}::routes())\"\n---\n#![allow(clippy::missing_errors_doc)]\n#![allow(clippy::unnecessary_struct_initialization)]\n#![allow(clippy::unused_async)]\nuse loco_rs::prelude::*;\nuse serde::{Deserialize, Serialize};\nuse sea_orm::{sea_query::Order, QueryOrder};\n\nuse crate::{\n    models::_entities::{{file_name | plural}}::{ActiveModel, Column, Entity, Model},\n    views,\n};\n\n#[derive(Clone, Debug, Serialize, Deserialize)]\npub struct Params {\n    {% for column in columns -%}\n    {%- if column.2 == \"IntegerNull\" -%}\n    pub {{column.0}}: Option<i32>,\n    {%- else -%}\n    pub {{column.0}}: {{column.1}},\n    {%- endif %}\n    {% endfor -%}\n}\n\nimpl Params {\n    fn update(&self, item: &mut ActiveModel) {\n      {% for column in columns -%}\n      {%- if \"Vec<\" in column.1 -%}\n      item.{{column.0}} = Set(self.{{column.0}}.clone());\n      {%- elif column.2 == \"IntegerNull\" -%}\n      item.{{column.0}} = Set(self.{{column.0}});\n      {%- elif \"i32\" in column.1 or \"i64\" in column.1 or \"i16\" in column.1 or \"Uuid\" in column.1 or \"f32\" in column.1 or \"f64\" in column.1 or \"Decimal\" in column.1 or \"bool\" in column.1 or \"Date\" in column.1 or \"DateTime\" in column.1 or \"DateTimeWithTimeZone\" in column.1 -%}\n      item.{{column.0}} = Set(self.{{column.0}});\n      {%- else -%}\n      item.{{column.0}} = Set(self.{{column.0}}.clone());\n      {%- endif %}\n      {% endfor -%}\n    }\n}\n\nasync fn load_item(ctx: &AppContext, id: i32) -> Result<Model> {\n    let item = Entity::find_by_id(id).one(&ctx.db).await?;\n    item.ok_or_else(|| Error::NotFound)\n}\n\n#[debug_handler]\npub async fn list(\n    ViewEngine(v): ViewEngine<TeraView>,\n    State(ctx): State<AppContext>,\n) -> Result<Response> {\n    let item = Entity::find()\n        .order_by(Column::Id, Order::Desc)\n        .all(&ctx.db)\n        .await?;\n    views::{{file_name}}::list(&v, &item)\n}\n\n#[debug_handler]\npub async fn new(\n    ViewEngine(v): ViewEngine<TeraView>,\n    State(_ctx): State<AppContext>,\n) -> Result<Response> {\n    views::{{file_name}}::create(&v)\n}\n\n#[debug_handler]\npub async fn update(\n    Path(id): Path<i32>,\n    State(ctx): State<AppContext>,\n    Json(params): Json<Params>,\n) -> Result<Response> {\n    let item = load_item(&ctx, id).await?;\n    let mut item = item.into_active_model();\n    params.update(&mut item);\n    let _ = item.update(&ctx.db).await?;\n    format::render().redirect_with_header_key(\"HX-Redirect\", \"/{{name | plural}}\")\n}\n\n#[debug_handler]\npub async fn edit(\n    Path(id): Path<i32>,\n    ViewEngine(v): ViewEngine<TeraView>,\n    State(ctx): State<AppContext>,\n) -> Result<Response> {\n    let item = load_item(&ctx, id).await?;\n    views::{{file_name}}::edit(&v, &item)\n}\n\n#[debug_handler]\npub async fn show(\n    Path(id): Path<i32>,\n    ViewEngine(v): ViewEngine<TeraView>,\n    State(ctx): State<AppContext>,\n) -> Result<Response> {\n    let item = load_item(&ctx, id).await?;\n    views::{{file_name}}::show(&v, &item)\n}\n\n#[debug_handler]\npub async fn add(\n    State(ctx): State<AppContext>,\n    Json(params): Json<Params>,\n) -> Result<Response> {\n    let mut item = ActiveModel {\n        ..Default::default()\n    };\n    params.update(&mut item);\n    let _ = item.insert(&ctx.db).await?;\n    format::render().redirect_with_header_key(\"HX-Redirect\", \"/{{name | plural}}\")\n}\n\n#[debug_handler]\npub async fn remove(Path(id): Path<i32>, State(ctx): State<AppContext>) -> Result<Response> {\n    load_item(&ctx, id).await?.delete(&ctx.db).await?;\n    format::empty()\n}\n\npub fn routes() -> Routes {\n    Routes::new()\n        .prefix(\"{{file_name | plural}}/\")\n        .add(\"/\", get(list))\n        .add(\"/\", post(add))\n        .add(\"new\", get(new))\n        .add(\"{id}\", get(show))\n        .add(\"{id}/edit\", get(edit))\n        .add(\"{id}\", delete(remove))\n        .add(\"{id}\", put(update))\n        .add(\"{id}\", patch(update))\n}\n"
  },
  {
    "path": "loco-gen/src/templates/scaffold/htmx/view.t",
    "content": "{% set file_name = name |  snake_case -%}\n{% set module_name = file_name | pascal_case -%}\nto: src/views/{{file_name}}.rs\nskip_exists: true\nmessage: \"{{file_name}} view was added successfully.\"\ninjections:\n- into: src/views/mod.rs\n  append: true\n  content: \"pub mod {{ file_name }};\"\n---\nuse loco_rs::prelude::*;\n\nuse crate::models::_entities::{{file_name | plural}};\n\n/// Render a list view of `{{name | plural}}`.\n///\n/// # Errors\n///\n/// When there is an issue with rendering the view.\npub fn list(v: &impl ViewRenderer, items: &Vec<{{file_name | plural}}::Model>) -> Result<Response> {\n    format::render().view(v, \"{{file_name}}/list.html\", data!({\"items\": items}))\n}\n\n/// Render a single `{{name}}` view.\n///\n/// # Errors\n///\n/// When there is an issue with rendering the view.\npub fn show(v: &impl ViewRenderer, item: &{{file_name | plural}}::Model) -> Result<Response> {\n    format::render().view(v, \"{{file_name}}/show.html\", data!({\"item\": item}))\n}\n\n/// Render a `{{name}}` create form.\n///\n/// # Errors\n///\n/// When there is an issue with rendering the view.\npub fn create(v: &impl ViewRenderer) -> Result<Response> {\n    format::render().view(v, \"{{file_name}}/create.html\", data!({}))\n}\n\n/// Render a `{{name}}` edit form.\n///\n/// # Errors\n///\n/// When there is an issue with rendering the view.\npub fn edit(v: &impl ViewRenderer, item: &{{file_name | plural}}::Model) -> Result<Response> {\n    format::render().view(v, \"{{file_name}}/edit.html\", data!({\"item\": item}))\n}\n"
  },
  {
    "path": "loco-gen/src/templates/scaffold/htmx/view_create.t",
    "content": "{% set file_name = name |  snake_case -%}\n{% set module_name = file_name | pascal_case -%}\nto: assets/views/{{file_name}}/create.html\nskip_exists: true\nmessage: \"{{file_name}} create view was added successfully.\"\n---\n{% raw %}{% extends \"base.html\" %}{% endraw %}\n\n{% raw %}{% block title %}{% endraw %}\nCreate {{file_name}}\n{% raw %}{% endblock title %}{% endraw %}\n\n{% raw %}{% block page_title %}{% endraw %}\nCreate new {{name}}\n{% raw %}{% endblock page_title %}{% endraw %}\n\n{% raw %}{% block content %}{% endraw %}\n<div class=\"mb-10\">\n    <div id=\"error-message\" class=\"mt-4 text-sm text-red-600\"></div>\n    <form hx-post=\"/{{name | plural}}\" hx-ext=\"submitjson\" class=\"flex-1 lg:max-w-2xl\">\n        {% for column in columns -%}\n            {{ render_form_field(fname=column.0, rust_type=column.1, ftype=column.2)}}\n        {% endfor -%}\n        <div class=\"mt-5\">\n            <button class=\" text-xs py-3 px-6 rounded-lg bg-gray-900 text-white\" type=\"submit\">Submit</button>\n        </div>\n\n    </form>\n</div>\n{% raw %}{% endblock content %}{% endraw %}\n\n{% raw %}{% block js %}{% endraw %}\n\n{% raw %}{% endblock js %}{% endraw %}"
  },
  {
    "path": "loco-gen/src/templates/scaffold/htmx/view_edit.t",
    "content": "{% set file_name = name |  snake_case -%}\n{% set module_name = file_name | pascal_case -%}\nto: assets/views/{{file_name}}/edit.html\nskip_exists: true\nmessage: \"{{file_name}} edit view was added successfully.\"\n---\n{% raw %}{% extends \"base.html\" %}{% endraw %}\n\n{% raw %}{% block title %}{% endraw %}\nEdit {{name}}: {% raw %}{{ item.id }}{% endraw %}\n{% raw %}{% endblock title %}{% endraw %}\n\n{% raw %}{% block page_title %}{% endraw %}\nEdit {{name}}: {% raw %}{{ item.id }}{% endraw %}\n{% raw %}{% endblock page_title %}{% endraw %}\n\n{% raw %}{% block content %}{% endraw %}\n<div class=\"mb-10\">\n    <div id=\"error-message\" class=\"mt-4 text-sm text-red-600\"></div>\n    <form hx-put=\"/{{name | plural}}/{% raw %}{{ item.id }}{% endraw %}\" hx-ext=\"submitjson\" hx-target=\"#success-message\" class=\"flex-1 lg:max-w-2xl\">\n        {% for column in columns -%}\n            {{ render_form_field(fname=column.0, rust_type=column.1, ftype=column.2, edit_form=true)}}\n        {% endfor -%}\n        <div>\n            <div class=\"mt-5\">\n                <button class=\" text-xs py-3 px-6 rounded-lg bg-gray-900 text-white\" type=\"submit\">Submit</button>\n                <button class=\"text-xs py-3 px-6 rounded-lg bg-red-600 text-white\"\n                            onclick=\"confirmDelete(event, '/{{name | plural}}/{% raw %}{{ item.id }}{% endraw %}', '/{{name | plural}}' )\">Delete</button>\n            </div>\n        </div>\n    </form>\n    <div id=\"success-message\" class=\"mt-4\"></div>\n    <br />\n    <a href=\"/{{name | plural}}\">Back to {{name}}</a>\n</div>\n{% raw %}{% endblock content %}{% endraw %}\n\n{% raw %}{% block js %}{% endraw %}\n\n{% raw %}{% endblock js %}{% endraw %}"
  },
  {
    "path": "loco-gen/src/templates/scaffold/htmx/view_list.t",
    "content": "{% set file_name = name |  snake_case -%}\n{% set module_name = file_name | pascal_case -%}\nto: assets/views/{{file_name}}/list.html\nskip_exists: true\nmessage: \"{{file_name}} list view was added successfully.\"\n---\n{% raw %}{% extends \"base.html\" %}{% endraw %}\n\n{% raw %}{% block title %}{% endraw %}\nList of {{file_name}}\n{% raw %}{% endblock title %}{% endraw %}\n\n{% raw %}{% block page_title %}{% endraw %}\n{{file_name}}\n{% raw %}{% endblock page_title %}{% endraw %}\n\n{% raw %}{% block content %}{% endraw %}\n<div class=\"mb-10\">\n\n    {% raw %}{% if items %}{% endraw %}\n\n    <div class=\"mb-5\">\n        <div class=\"relative w-full overflow-auto\">\n            <table class=\"w-full caption-bottom text-sm\">\n                <thead class=\"[&amp;_tr]:border-b\">\n                    <tr class=\"border-b transition-colors hover:bg-muted/50\">\n                        {% for column in columns -%}\n                        <th class=\"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&amp;:has([role=checkbox])]:pr-0 [&amp;>[role=checkbox]]:translate-y-[2px] w-[100px]\">\n                            {% raw %}{{\"{% endraw %}{{column.0}}{% raw %}\" | capitalize }}{% endraw %}\n                        </th>\n                        {% endfor -%}\n                        <th class=\"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&amp;:has([role=checkbox])]:pr-0 [&amp;>[role=checkbox]]:translate-y-[2px] w-[100px]\">\n                           Actions\n                        </th>\n                    </tr>\n                </thead>\n                <tbody class=\"[&amp;_tr:last-child]:border-0\">\n                   {% raw %}{% for item in items %}{% endraw %}\n                    <tr class=\"border-b transition-colors hover:bg-muted/50\">\n                        {% for column in columns -%}\n                          <td\n                            class=\"p-2 align-middle  font-medium\">\n                            {{ render_view_field(fname=column.0, rust_type=column.1) }}\n                        </td>\n                        {% endfor -%}\n                        <td>\n                            <a href=\"/{{name | plural}}/{% raw %}{{ item.id }}{% endraw %}/edit\">Edit</a>\n                        </td>\n                    </tr>\n                    {% raw %}{% endfor %}{% endraw %}\n                </tbody>\n            </table>\n        </div>\n    \n        <div class=\"flex\">\n            <div class=\"ml-auto  p-4\">\n                <a href=\"/{{name | plural}}/new\"\n                    class=\"mt-5 bg-blue-500 text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800\">\n                    Create\n                </a>\n            </div>\n        </div>\n    </div>\n\n    {% raw %}{% else %}{% endraw %}\n\n    <div class=\"mt-10 flex items-center justify-center\">\n        <div class=\"bg-white rounded-lg shadow-lg p-8 max-w-4xl w-full flex flex-col items-center\">\n            <h3 class=\"font-bold text-lg\">Nothing Here Yet</h3>\n            There are no records to display. Add a new record to get started!\n            <a href=\"/{{name | plural}}/new\"\n            class=\"mt-5 bg-blue-500 text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800\">\n            Create\n        </a>\n        </div>\n    </div>\n   \n    {% raw %}{% endif %}{% endraw %}\n\n    \n</div>\n{% raw %}{% endblock content %}{% endraw %}\n"
  },
  {
    "path": "loco-gen/src/templates/scaffold/htmx/view_show.t",
    "content": "{% set file_name = name |  snake_case -%}\n{% set module_name = file_name | pascal_case -%}\nto: assets/views/{{file_name}}/show.html\nskip_exists: true\nmessage: \"{{file_name}} view was added successfully.\"\n---\n{% raw %}{% extends \"base.html\" %}{% endraw %}\n\n{% raw %}{% block title %}{% endraw %}\nView {{name}}: {% raw %}{{ item.id }}{% endraw %}\n{% raw %}{% endblock title %}{% endraw %}\n\n{% raw %}{% block page_title %}{% endraw %}\nView {{name}}: {% raw %}{{ item.id }}{% endraw %}\n{% raw %}{% endblock page_title %}{% endraw %}\n\n\n{% raw %}{% block content %}{% endraw %}\n<div class=\"mb-10\">\n    {% for column in columns -%}\n    <div>\n    <label><b>{% raw %}{{\"{% endraw %}{{column.0}}{% raw %}\" | capitalize }}{% endraw %}:</b> {% raw %}{{item.{% endraw %}{{column.0}}{% raw %}}}{% endraw %}</label>\n    </div>\n{% endfor -%}\n<br />\n<a href=\"/{{name | plural}}\">Back to {{name | plural}}</a>\n</div>\n{% raw %}{% endblock content %}{% endraw %}\n"
  },
  {
    "path": "loco-gen/src/templates/scheduler/scheduler.t",
    "content": "to: \"config/scheduler.yaml\"\nskip_exists: true\nmessage: \"A Scheduler job configuration was added successfully. Run with `cargo loco scheduler --list`.\"\n\n---\noutput: stdout\njobs:\n  write_content:\n      shell: true\n      run: \"echo loco >> ./scheduler.txt\"\n      schedule: run every 1 second\n      # schedule: \"* * * * * * *\"\n      output: silent\n      tags: ['base', 'infra']\n\n  # run_task:\n  #     run: \"foo\"\n  #     schedule: \"at 10:00 am\"\n"
  },
  {
    "path": "loco-gen/src/templates/task/task.t",
    "content": "{% set file_name = name |  snake_case -%}\n{% set module_name = file_name | pascal_case -%}\nto: \"src/tasks/{{file_name}}.rs\"\nskip_exists: true\nmessage: \"A Task `{{module_name}}` was added successfully. Run with `cargo run task {{name}}`.\"\ninjections:\n- into: \"src/tasks/mod.rs\"\n  append: true\n  content: \"pub mod {{ file_name }};\"\n- into: src/app.rs\n  before: \"// tasks-inject\"\n  content: \"        tasks.register(tasks::{{file_name}}::{{module_name}});\"\n---\nuse loco_rs::prelude::*;\n\npub struct {{module_name}};\n#[async_trait]\nimpl Task for {{module_name}} {\n    fn task(&self) -> TaskInfo {\n        TaskInfo {\n            name: \"{{name}}\".to_string(),\n            detail: \"Task generator\".to_string(),\n        }\n    }\n    async fn run(&self, _app_context: &AppContext, _vars: &task::Vars) -> Result<()> {\n        println!(\"Task {{module_name}} generated\");\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "loco-gen/src/templates/task/test.t",
    "content": "{% set file_name = name |  snake_case -%}\n{% set module_name = file_name | pascal_case -%}\nto: tests/tasks/{{ file_name }}.rs\nskip_exists: true\nmessage: \"Tests for task `{{module_name}}` was added successfully. Run `cargo test`.\"\ninjections:\n- into: tests/tasks/mod.rs\n  append: true\n  content: \"pub mod {{ file_name }};\"\n---\nuse {{pkg_name}}::app::App;\nuse loco_rs::{task, testing::prelude::*};\n\nuse loco_rs::boot::run_task;\nuse serial_test::serial;\n\n#[tokio::test]\n#[serial]\nasync fn test_can_run_{{name | snake_case}}() {\n    let boot = boot_test::<App>().await.unwrap();\n\n    assert!(\n        run_task::<App>(&boot.app_context, Some(&\"{{name}}\".to_string()), &task::Vars::default())\n            .await\n            .is_ok()\n    );\n}\n"
  },
  {
    "path": "loco-gen/src/templates/worker/test.t",
    "content": "{% set module_name = name |  snake_case -%}\n{% set struct_name = module_name | pascal_case -%}\nto: \"tests/workers/{{module_name}}.rs\"\nskip_exists: true\nmessage: \"Test for worker `{{struct_name}}` was added successfully. Run `cargo test`.\"\ninjections:\n- into: tests/workers/mod.rs\n  append: true\n  content: \"pub mod {{ name |  snake_case }};\"\n---\nuse loco_rs::{bgworker::BackgroundWorker, testing::prelude::*};\nuse {{pkg_name}}::{\n    app::App,\n    workers::{{module_name}}::{Worker, WorkerArgs},\n};\nuse serial_test::serial;\n\n#[tokio::test]\n#[serial]\nasync fn test_run_{{module_name}}_worker() {\n    let boot = boot_test::<App>().await.unwrap();\n\n    // Execute the worker ensuring that it operates in 'ForegroundBlocking' mode, which prevents the addition of your worker to the background\n    assert!(\n        Worker::perform_later(&boot.app_context,WorkerArgs {})\n            .await\n            .is_ok()\n    );\n    // Include additional assert validations after the execution of the worker\n}\n"
  },
  {
    "path": "loco-gen/src/templates/worker/worker.t",
    "content": "{% set module_name = name |  snake_case -%}\n{% set struct_name = module_name | pascal_case -%}\nto: \"src/workers/{{module_name}}.rs\"\nskip_exists: true\nmessage: \"A worker `{{struct_name}}` was added successfully. Run with `cargo run start --worker`.\"\ninjections:\n- into: \"src/workers/mod.rs\"\n  append: true\n  content: \"pub mod {{ module_name}};\"\n- into: src/app.rs\n  after: \"fn connect_workers\"\n  content: \"        queue.register(crate::workers::{{module_name}}::Worker::build(ctx)).await?;\"---\nuse serde::{Deserialize, Serialize};\nuse loco_rs::prelude::*;\n\npub struct Worker {\n    pub ctx: AppContext,\n}\n\n#[derive(Deserialize, Debug, Serialize)]\npub struct WorkerArgs {\n}\n\n#[async_trait]\nimpl BackgroundWorker<WorkerArgs> for Worker {\n    /// Creates a new instance of the Worker with the given application context.\n    /// \n    /// This function is called when registering the worker with the queue system.\n    /// \n    /// # Parameters\n    /// * `ctx` - The application context containing shared resources\n    fn build(ctx: &AppContext) -> Self {\n        Self { ctx: ctx.clone() }\n    }\n\n    /// Returns the class name of the worker.\n    /// \n    /// This name is used when enqueueing jobs and identifying the worker in logs.\n    /// The implementation returns the struct name as a string.\n    fn class_name() -> String {\n        \"{{struct_name}}\".to_string()\n    }\n\n    /// Returns tags associated with this worker.\n    /// \n    /// Tags can be used to filter which workers run during startup.\n    /// The default implementation returns an empty vector (no tags).\n    fn tags() -> Vec<String> {\n        Vec::new()\n    }\n    \n    /// Performs the actual work when a job is processed.\n    /// \n    /// This is the main function that contains the worker's logic.\n    /// It gets executed when a job is dequeued from the job queue.\n    /// \n    /// # Returns\n    /// * `Result<()>` - Ok if the job completed successfully, Err otherwise\n    async fn perform(&self, _args: WorkerArgs) -> Result<()> {\n        println!(\"================={{struct_name}}=======================\");\n        // TODO: Some actual work goes here...\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "loco-gen/src/tera_ext.rs",
    "content": "use std::collections::HashMap;\n\nuse tera::{Tera, Value};\n\n#[must_use]\npub fn new() -> Tera {\n    let mut tera = Tera::default();\n    tera.register_function(\"render_form_field\", FormField);\n    tera.register_function(\"render_view_field\", ViewField);\n    tera\n}\n\nconst DEFAULT_INPUT_CLASS: &str = \"flex h-9 w-full rounded-md border border-input bg-transparent \\\n                                   px-3 py-1 text-base shadow-sm md:text-sm\";\nstruct FormField;\nstruct ViewField;\n\nimpl tera::Function for FormField {\n    #[allow(clippy::too_many_lines)]\n    fn call(&self, args: &HashMap<String, Value>) -> tera::Result<Value> {\n        let fname = args\n            .get(\"fname\")\n            .ok_or_else(|| tera::Error::msg(\"fname is mandatory\"))?\n            .as_str()\n            .ok_or_else(|| tera::Error::msg(\"fname must be a string\"))?;\n        let ftype = args\n            .get(\"ftype\")\n            .ok_or_else(|| tera::Error::msg(\"ftype is mandatory\"))?\n            .as_str()\n            .ok_or_else(|| tera::Error::msg(\"ftype must be a string\"))?;\n        let rust_type = args\n            .get(\"rust_type\")\n            .ok_or_else(|| tera::Error::msg(\"rust_type is mandatory\"))?\n            .as_str()\n            .ok_or_else(|| tera::Error::msg(\"rust_type must be a string\"))?;\n\n        let is_edit_form = args\n            .get(\"edit_form\")\n            .unwrap_or(&Value::Bool(false))\n            .as_bool()\n            .unwrap_or_default();\n\n        let value = if is_edit_form {\n            format!(\"{{{{item.{fname}}}}}\")\n        } else {\n            String::new()\n        };\n\n        let input_class = args\n            .get(\"input_class\")\n            .and_then(|c| c.as_str())\n            .unwrap_or(DEFAULT_INPUT_CLASS);\n\n        let is_required = ftype.ends_with('!') || ftype.ends_with('^');\n        let required_value = if is_required { \"required\" } else { \"\" };\n\n        let element = match rust_type {\n            \"Uuid\" | \"Option<Uuid>\" => {\n                let desc = input_description(\"e.g: 11111111-1111-1111-1111-111111111111.\");\n                let input = input_string(\n                    fname,\n                    &value,\n                    is_required,\n                    input_class,\n                    Some(\n                        r#\"pattern=\"[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}\"\"#,\n                    ),\n                );\n                format!(\n                    r\"{input}\n    {desc}\",\n                )\n            }\n            \"serde_json::Value\" | \"Option<serde_json::Value>\" => {\n                format!(\n                    r#\"<textarea class=\"{input_class}\" id=\"{fname}\" name=\"{fname}\" type=\"text\" rows=\"10\" cols=\"50\" {required_value}>{value}</textarea>\"#,\n                )\n            }\n            \"String\" | \"Option<String>\" => {\n                input_string(fname, &value, is_required, input_class, None)\n            }\n\n            \"i8\" | \"Option<i8>\" => input_number(\n                fname,\n                &value,\n                is_required,\n                input_class,\n                Some((i8::MIN, i8::MAX)),\n                Some(r#\"step=\"1\"\"#),\n            ),\n            \"i16\" | \"Option<i16>\" => input_number(\n                fname,\n                &value,\n                is_required,\n                input_class,\n                Some((i16::MIN, i16::MAX)),\n                Some(r#\"step=\"1\"\"#),\n            ),\n            \"i32\" | \"Option<i32>\" => input_number(\n                fname,\n                &value,\n                is_required,\n                input_class,\n                Some((i32::MIN, i32::MAX)),\n                Some(r#\"step=\"1\"\"#),\n            ),\n            \"i64\" | \"Option<i64>\" => input_number(\n                fname,\n                &value,\n                is_required,\n                input_class,\n                Some((i64::MIN, i64::MAX)),\n                Some(r#\"step=\"1\"\"#),\n            ),\n            \"Decimal\" | \"Option<Decimal>\" => input_number::<i128>(\n                fname,\n                &value,\n                is_required,\n                input_class,\n                Some((\n                    -79_228_162_514_264_337_593_543_950_335,\n                    79_228_162_514_264_337_593_543_950_335,\n                )),\n                Some(r#\"step=\"0.1\"\"#),\n            ),\n            \"f32\" | \"Option<f32>\" => input_number(\n                fname,\n                &value,\n                is_required,\n                input_class,\n                Some((f32::MIN, f32::MAX)),\n                Some(r#\"step=\"0.1\"\"#),\n            ),\n            \"f64\" | \"Option<f64>\" => input_number(\n                fname,\n                &value,\n                is_required,\n                input_class,\n                Some((f64::MIN, f64::MAX)),\n                Some(r#\"step=\"0.1\"\"#),\n            ),\n            \"DateTimeWithTimeZone\"\n            | \"Option<DateTimeWithTimeZone>\"\n            | \"DateTime\"\n            | \"Option<DateTime>\"\n            | \"DateTimeUtc\"\n            | \"Option<DateTimeUtc>\" => {\n                format!(\n                    r#\"<input class=\"{input_class}\" id=\"{fname}\" name=\"{fname}\" type=\"datetime-local\" value=\"{value}\" {required_value} />\"#,\n                )\n            }\n            \"Date\" | \"Option<Date>\" => {\n                format!(\n                    r#\"<input class=\"{input_class}\" id=\"{fname}\" name=\"{fname}\" type=\"date\" value=\"{value}\" {required_value} />\"#,\n                )\n            }\n            \"bool\" | \"Option<bool>\" => {\n                let checked = if is_edit_form {\n                    format!(\"{{% if item.{fname} %}}checked{{%endif %}}\")\n                } else {\n                    String::new()\n                };\n                format!(\n                    r#\"<input class=\"flex rounded-md border border-input bg-transparent text-base shadow-sm md:text-sm\" id=\"{fname}\" name=\"{fname}\" type=\"checkbox\" value=\"true\" {checked} {required_value} />\"#,\n                )\n            }\n            \"Vec<u8>\" | \"Option<Vec<u8>>\" => {\n                format!(\n                    r#\"<input class=\"{input_class}\" id=\"{fname}\" name=\"{fname}\" value=\"{value}\" custom_type=\"blob\" pattern=\"^[0-9]+(,[0-9]+)*$\" {required_value} />\n    <p id=\":rh:-form-item-description\" class=\"text-[0.8rem] text-muted-foreground\">e.g: 123,123,123 .</p>\"#,\n                )\n            }\n            \"Vec<String>\" | \"Option<Vec<String>>\" => {\n                format!(\n                    r#\"<button type=\"button\" class=\"text-xs py-1 px-3 rounded-lg bg-gray-900 text-white add-more\" data-group=\"{fname}\">Add More</button>\n    <div id=\"{fname}-inputs\" class=\"space-y-2\">\n    {{% if item.{fname} %}}\n        {{% for val in item.{fname} %}}\n            <input class=\"{input_class}\" name=\"{fname}\" type=\"text\" value=\"{{{{val}}}}\" {required_value} custom_type=\"array\"/>\n        {{% endfor -%}}\n    {{%- else -%}}\n        <input class=\"{input_class}\" name=\"{fname}\" type=\"text\" value=\"{value}\" {required_value} custom_type=\"array\"/>\n    {{%- endif -%}}\n    </div>\"#\n                )\n            }\n            \"Vec<f32>\" | \"Option<Vec<f32>>\" => {\n                let edit_input = input_number(\n                    fname,\n                    \"{{val}}\",\n                    is_required,\n                    input_class,\n                    Some((f32::MIN, f32::MAX)),\n                    Some(r#\"custom_type=\"array\" step=\"0.1\"\"#),\n                );\n                let create_input = input_number(\n                    fname,\n                    &value,\n                    is_required,\n                    input_class,\n                    Some((f32::MIN, f32::MAX)),\n                    Some(r#\"custom_type=\"array\" step=\"0.1\"\"#),\n                );\n                input_group(fname, &create_input, &edit_input)\n            }\n            \"Vec<f64>\" | \"Option<Vec<f64>>\" => {\n                let edit_input = input_number(\n                    fname,\n                    \"{{val}}\",\n                    is_required,\n                    input_class,\n                    Some((f64::MIN, f64::MAX)),\n                    Some(r#\"custom_type=\"array\"\"#),\n                );\n                let create_input = input_number(\n                    fname,\n                    &value,\n                    is_required,\n                    input_class,\n                    Some((f64::MIN, f64::MAX)),\n                    Some(r#\"custom_type=\"array\"\"#),\n                );\n                input_group(fname, &create_input, &edit_input)\n            }\n            \"Vec<i32>\" | \"Option<Vec<i32>>\" => {\n                let edit_input = input_number(\n                    fname,\n                    \"{{val}}\",\n                    is_required,\n                    input_class,\n                    Some((i32::MIN, i32::MAX)),\n                    Some(r#\"custom_type=\"array\"\"#),\n                );\n                let create_input = input_number(\n                    fname,\n                    &value,\n                    is_required,\n                    input_class,\n                    Some((i32::MIN, i32::MAX)),\n                    Some(r#\"custom_type=\"array\"\"#),\n                );\n                input_group(fname, &create_input, &edit_input)\n            }\n            \"Vec<i64>\" | \"Option<Vec<i64>>\" => {\n                let edit_input = input_number(\n                    fname,\n                    \"{{val}}\",\n                    is_required,\n                    input_class,\n                    Some((i64::MIN, i64::MAX)),\n                    Some(r#\"custom_type=\"array\"\"#),\n                );\n                let create_input = input_number(\n                    fname,\n                    &value,\n                    is_required,\n                    input_class,\n                    Some((i64::MIN, i64::MAX)),\n                    Some(r#\"custom_type=\"array\"\"#),\n                );\n                input_group(fname, &create_input, &edit_input)\n            }\n            \"Vec<bool>\" | \"Option<Vec<bool>>\" => String::new(),\n            _ => {\n                return Err(tera::Error::msg(format!(\n                    \"rust_type: `{rust_type}` not implemented\"\n                )))\n            }\n        };\n\n        Ok(Value::String(format!(\n            r#\"<div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">{fname}</label>\n    {element}\n</div>\"#\n        )))\n    }\n\n    fn is_safe(&self) -> bool {\n        true\n    }\n}\n\nfn input_group(fname: &str, create_input: &str, edit_input: &str) -> String {\n    format!(\n        r#\"<button type=\"button\" class=\"text-xs py-1 px-3 rounded-lg bg-gray-900 text-white add-more\" data-group=\"{fname}\">Add More</button>\n    <div id=\"{fname}-inputs\" class=\"space-y-2\">\n    {{% if item.{fname} %}}\n        {{% for val in item.{fname} %}}\n            {edit_input}\n        {{% endfor -%}}\n    {{%- else -%}}\n        {create_input}\n    {{%- endif -%}}\n    </div>\"#\n    )\n}\n\nfn input_string(\n    name: &str,\n    value: &str,\n    is_required: bool,\n    class: &str,\n    attr: Option<&str>,\n) -> String {\n    let attr = attr.unwrap_or_default();\n    let required_value = if is_required { \"required\" } else { \"\" };\n    format!(\n        r#\"<input class=\"{class}\" id=\"{name}\" name=\"{name}\" type=\"text\" value=\"{value}\" {required_value} {attr}/>\"#\n    )\n}\n\nfn input_number<T>(\n    name: &str,\n    value: &str,\n    is_required: bool,\n    class: &str,\n    range: Option<(T, T)>,\n    attr: Option<&str>,\n) -> String\nwhere\n    T: PartialOrd + std::fmt::Display,\n{\n    let required_value = if is_required { \"required\" } else { \"\" };\n\n    let (min_attr, max_attr) = if let Some((min, max)) = range {\n        (format!(r#\"min=\"{min}\"\"#), format!(r#\"max=\"{max}\"\"#))\n    } else {\n        (String::new(), String::new())\n    };\n\n    let attr = attr.unwrap_or_default();\n    format!(\n        r#\"<input class=\"{class}\" {min_attr} {max_attr} id=\"{name}\" name=\"{name}\" type=\"number\" value=\"{value}\" {required_value} {attr} />\"#,\n    )\n}\n\nfn input_description<S: AsRef<str>>(description: S) -> String {\n    format!(\n        r#\"<p id=\":rh:-form-item-description\" class=\"text-[0.8rem] text-muted-foreground\">{}.</p>\"#,\n        description.as_ref()\n    )\n}\n\nimpl tera::Function for ViewField {\n    fn call(&self, args: &HashMap<String, Value>) -> tera::Result<Value> {\n        let fname = args\n            .get(\"fname\")\n            .ok_or_else(|| tera::Error::msg(\"fname is mandatory\"))?\n            .as_str()\n            .ok_or_else(|| tera::Error::msg(\"fname must be a string\"))?;\n        let rust_type = args\n            .get(\"rust_type\")\n            .ok_or_else(|| tera::Error::msg(\"rust_type is mandatory\"))?\n            .as_str()\n            .ok_or_else(|| tera::Error::msg(\"rust_type must be a string\"))?;\n\n        // Generate the appropriate display code based on the rust type\n        // For strings, we escape HTML. For numbers, booleans, dates, and arrays, we don't.\n        let display_code = match rust_type {\n            \"String\"\n            | \"Option<String>\"\n            | \"Uuid\"\n            | \"Option<Uuid>\"\n            | \"serde_json::Value\"\n            | \"Option<serde_json::Value>\" => {\n                // Strings and JSON values need escaping\n                format!(r\"{{{{item.{fname} | escape }}}}\")\n            }\n            \"bool\" | \"Option<bool>\" => {\n                // Booleans: show \"false\" if empty/None, otherwise show the value\n                format!(\n                    r\"{{% if item.{fname} %}}{{{{item.{fname}}}}}{{% else %}}false{{% endif %}}\"\n                )\n            }\n            _ => {\n                // Everything else (numbers, dates, arrays, etc.) doesn't need escaping\n                format!(r\"{{{{item.{fname}}}}}\")\n            }\n        };\n\n        Ok(Value::String(display_code))\n    }\n\n    fn is_safe(&self) -> bool {\n        true\n    }\n}\n\n#[cfg(test)]\npub mod tests {\n    use insta::assert_snapshot;\n\n    use super::*;\n    use crate::get_mappings;\n\n    #[test]\n    fn can_render_form_field() {\n        let mapping = get_mappings();\n\n        let mut template_engine = new();\n\n        template_engine\n                .add_raw_template(\n                    \"template\",\n                    r\"{{ render_form_field(fname=fname_val, ftype=ftype_val, rust_type=rust_type_val, edit_form=edit_form_val)}}\"\n                )\n                .unwrap_or_else(|_| panic!(\"Failed to add raw template\"));\n\n        for field in &mapping.field_types {\n            let rust_fields = match &field.rust {\n                crate::RustType::String(rust_field) => {\n                    HashMap::from([(field.name.clone(), rust_field.clone())])\n                }\n                crate::RustType::Map(data) => data.clone(),\n            };\n\n            for (field_name, rust_field_type) in rust_fields {\n                let mut template_ctx = tera::Context::new();\n                template_ctx.insert(\"fname_val\", &field.name);\n                template_ctx.insert(\"ftype_val\", &field.name);\n                template_ctx.insert(\"rust_type_val\", &rust_field_type);\n                template_ctx.insert(\"edit_form_val\", &false);\n\n                let create_form = template_engine\n                    .render(\"template\", &template_ctx)\n                    .unwrap_or_else(|err| {\n                        panic!(\"Failed to render template. context: {template_ctx:?} .err: {err:?}\")\n                    });\n\n                template_ctx.insert(\"edit_form_val\", &true);\n                let edit_form = template_engine\n                    .render(\"template\", &template_ctx)\n                    .unwrap_or_else(|err| {\n                        panic!(\"Failed to render template. context: {template_ctx:?} .err: {err:?}\")\n                    });\n\n                assert_snapshot!(\n                    format!(\"can_render_form_field_[form_{}_{}]\", field.name, field_name),\n                    format!(\"Create form\\n\\n{create_form}\\n\\nEdit Form\\n\\n{edit_form}\")\n                );\n            }\n        }\n    }\n\n    #[test]\n    fn can_render_view_field() {\n        let mapping = get_mappings();\n\n        let mut template_engine = new();\n\n        template_engine\n            .add_raw_template(\n                \"template\",\n                r\"{{ render_view_field(fname=fname_val, rust_type=rust_type_val)}}\",\n            )\n            .unwrap_or_else(|_| panic!(\"Failed to add raw template\"));\n\n        let mut all_results = Vec::new();\n\n        for field in &mapping.field_types {\n            let rust_fields = match &field.rust {\n                crate::RustType::String(rust_field) => {\n                    HashMap::from([(field.name.clone(), rust_field.clone())])\n                }\n                crate::RustType::Map(data) => data.clone(),\n            };\n\n            for (field_name, rust_field_type) in rust_fields {\n                let mut template_ctx = tera::Context::new();\n                template_ctx.insert(\"fname_val\", &field.name);\n                template_ctx.insert(\"rust_type_val\", &rust_field_type);\n\n                let view_field = template_engine\n                    .render(\"template\", &template_ctx)\n                    .unwrap_or_else(|err| {\n                        panic!(\"Failed to render template. context: {template_ctx:?} .err: {err:?}\")\n                    });\n\n                all_results.push(format!(\n                    \"Field: {}.{} (type: {})\\n{}\\n\",\n                    field.name, field_name, rust_field_type, view_field\n                ));\n            }\n        }\n\n        // Sort results to ensure consistent ordering\n        all_results.sort();\n\n        assert_snapshot!(\"can_render_view_field\", all_results.join(\"\\n\"));\n    }\n}\n"
  },
  {
    "path": "loco-gen/src/testutil.rs",
    "content": "//\n// generator test toolkit\n// to be extracted to a library later.\n//\nuse std::{\n    env,\n    error::Error,\n    fs,\n    path::{Path, PathBuf},\n    process::Command,\n};\n\nuse regex::Regex;\n\n// Define the custom struct to encapsulate file content\npub struct FileContent {\n    content: String,\n}\n\nimpl FileContent {\n    // Method to load content from a file into the struct\n    pub fn from_file(file_path: &str) -> Result<Self, Box<dyn Error>> {\n        let content = fs::read_to_string(file_path)?;\n        Ok(Self { content })\n    }\n\n    // Method to check that the content contains a specific string\n    pub fn check_contains(&self, pattern: &str) -> Result<(), Box<dyn Error>> {\n        if self.content.contains(pattern) {\n            Ok(())\n        } else {\n            Err(Box::from(format!(\"Content does not contain '{pattern}'\")))\n        }\n    }\n\n    // Assert method for check_contains\n    pub fn assert_contains(&self, pattern: &str) {\n        self.check_contains(pattern)\n            .unwrap_or_else(|e| panic!(\"{}\", e));\n    }\n\n    // Method to check that the content matches a regular expression\n    pub fn check_regex_match(&self, pattern: &str) -> Result<(), Box<dyn Error>> {\n        let re = Regex::new(pattern)?;\n        if re.is_match(&self.content) {\n            Ok(())\n        } else {\n            Err(Box::from(format!(\n                \"Content does not match regex '{pattern}'\"\n            )))\n        }\n    }\n\n    // Assert method for check_regex_match\n    pub fn assert_regex_match(&self, pattern: &str) {\n        self.check_regex_match(pattern)\n            .unwrap_or_else(|e| panic!(\"{}\", e));\n    }\n\n    // Method to check that the content does not contain a specific string\n    pub fn check_not_contains(&self, pattern: &str) -> Result<(), Box<dyn Error>> {\n        #[allow(clippy::if_not_else)]\n        if !self.content.contains(pattern) {\n            Ok(())\n        } else {\n            Err(Box::from(format!(\"Content should not contain '{pattern}'\")))\n        }\n    }\n\n    // Assert method for check_not_contains\n    pub fn assert_not_contains(&self, pattern: &str) {\n        self.check_not_contains(pattern)\n            .unwrap_or_else(|e| panic!(\"{}\", e));\n    }\n\n    // Method to check the length of the content\n    pub fn check_length(&self, expected_length: usize) -> Result<(), Box<dyn Error>> {\n        if self.content.len() == expected_length {\n            Ok(())\n        } else {\n            Err(Box::from(format!(\n                \"Content length is {}, expected {}\",\n                self.content.len(),\n                expected_length\n            )))\n        }\n    }\n\n    // Assert method for check_length\n    pub fn assert_length(&self, expected_length: usize) {\n        self.check_length(expected_length)\n            .unwrap_or_else(|e| panic!(\"{}\", e));\n    }\n\n    // Method to check the syntax using rustfmt without creating a temp file\n    pub fn check_syntax(&self) -> Result<(), Box<dyn Error>> {\n        // Parse the file using `syn` to check for valid Rust syntax\n        match syn::parse_file(&self.content) {\n            Ok(_) => Ok(()),\n            Err(err) => Err(Box::from(format!(\"Syntax error: {err}\"))),\n        }\n    }\n\n    // Assert method for check_syntax\n    pub fn assert_syntax(&self) {\n        self.check_syntax().unwrap_or_else(|e| panic!(\"{}\", e));\n    }\n}\n\n// Function that loads the file and applies the provided closure for assertions\npub fn check_file<F>(file_path: &str, assertions: F) -> Result<(), Box<dyn Error>>\nwhere\n    F: Fn(&FileContent) -> Result<(), Box<dyn Error>>,\n{\n    let content = FileContent::from_file(file_path)?;\n    assertions(&content)?;\n    Ok(())\n}\n\n// Assert function for checking the file with a closure for custom assertions\npub fn assert_file<F>(file_path: &str, assertions: F)\nwhere\n    F: Fn(&FileContent),\n{\n    check_file(file_path, |content| {\n        assertions(content);\n        Ok(())\n    })\n    .unwrap_or_else(|e| panic!(\"{}\", e));\n}\n\npub fn check_no_warnings() -> Result<(), Box<dyn std::error::Error>> {\n    let output = Command::new(\"cargo\")\n        .arg(\"check\")\n        .arg(\"--message-format=json\")\n        .output()?;\n\n    let stdout = String::from_utf8(output.stdout)?;\n    if stdout.contains(\"warning:\") {\n        Err(Box::from(\"Compilation produced warnings\"))\n    } else {\n        Ok(())\n    }\n}\n\npub fn assert_no_warnings() {\n    check_no_warnings().unwrap_or_else(|e| panic!(\"{}\", e));\n}\n\npub fn check_cargo_check() -> Result<(), Box<dyn Error>> {\n    let output = Command::new(\"cargo\").arg(\"check\").output()?; // Execute the command and get the output\n\n    // Check if cargo check was successful\n    if output.status.success() {\n        Ok(())\n    } else {\n        // Capture and return the error output if the command failed\n        let error_message = String::from_utf8_lossy(&output.stderr);\n        Err(Box::from(format!(\"cargo check failed: {error_message}\")))\n    }\n}\n\npub fn assert_cargo_check() {\n    check_cargo_check().unwrap_or_else(|e| panic!(\"{}\", e));\n}\n\npub fn check_file_not_exists(file_path: &str) -> Result<(), Box<dyn std::error::Error>> {\n    if std::path::Path::new(file_path).exists() {\n        Err(Box::from(format!(\"File {file_path} should not exist\")))\n    } else {\n        Ok(())\n    }\n}\n\npub fn assert_file_not_exists(file_path: &str) {\n    check_file_not_exists(file_path).unwrap_or_else(|e| panic!(\"{}\", e));\n}\n\npub fn check_file_exists(file_path: &str) -> Result<(), Box<dyn std::error::Error>> {\n    if std::path::Path::new(file_path).exists() {\n        Ok(())\n    } else {\n        Err(Box::from(format!(\"File {file_path} does not exist\")))\n    }\n}\n\npub fn assert_file_exists(file_path: &str) {\n    check_file_exists(file_path).unwrap_or_else(|e| panic!(\"{}\", e));\n}\n\npub fn check_dir_exists(dir_path: &str) -> Result<(), Box<dyn std::error::Error>> {\n    if std::path::Path::new(dir_path).is_dir() {\n        Ok(())\n    } else {\n        Err(Box::from(format!(\"Directory {dir_path} does not exist\")))\n    }\n}\n\npub fn assert_dir_exists(dir_path: &str) {\n    check_dir_exists(dir_path).unwrap_or_else(|e| panic!(\"{}\", e));\n}\n\n/// Checks if there exists exactly one file in the given directory whose name\n/// matches the provided regex pattern.\npub fn check_single_file_match<P: AsRef<Path>>(\n    dir: P,\n    pattern: &str,\n) -> Result<PathBuf, Box<dyn Error>> {\n    // Compile the provided regex pattern\n    let re = Regex::new(pattern)?;\n\n    // Filter files that match the regex\n    let matched_files: Vec<PathBuf> = fs::read_dir(dir)?\n        .filter_map(std::result::Result::ok)\n        .filter_map(|entry| {\n            let path = entry.path();\n\n            #[allow(clippy::option_if_let_else)]\n            if let Some(file_name) = path.file_name().and_then(|f| f.to_str()) {\n                if re.is_match(file_name) {\n                    Some(path) // Return the path if the regex matches\n                } else {\n                    None\n                }\n            } else {\n                None\n            }\n        })\n        .collect();\n\n    // Ensure that there is exactly one match\n    match matched_files.len() {\n        0 => Err(Box::from(\"No file found matching the given pattern.\")),\n        1 => Ok(matched_files.into_iter().next().unwrap()), /* Return the single matching file's */\n        // path\n        _ => Err(Box::from(\"More than one file matches the given pattern.\")),\n    }\n}\n\npub fn assert_single_file_match<P: AsRef<Path>>(dir: P, pattern: &str) -> PathBuf {\n    check_single_file_match(dir, pattern).unwrap_or_else(|e| panic!(\"{}\", e))\n}\n\npub fn with_temp_dir<F>(f: F) -> Result<(), Box<dyn Error>>\nwhere\n    F: FnOnce(&Path, &Path),\n{\n    let previous = env::current_dir()?; // Get the current directory\n    println!(\"Current directory: {previous:?}\");\n\n    let tree_fs = tree_fs::TreeBuilder::default().drop(true).create()?; // Create a temporary directory\n    let current = &tree_fs.root;\n\n    println!(\"Temporary directory: {current:?}\");\n    env::set_current_dir(current)?; // Set the current directory to the temp directory\n\n    // Use catch_unwind to handle panics gracefully\n    f(previous.as_path(), current); // Execute the provided closure\n\n    // Restore the original directory\n    env::set_current_dir(previous)?;\n\n    Ok(())\n}\n"
  },
  {
    "path": "loco-gen/tests/db.rs",
    "content": "use duct::cmd;\nuse insta::assert_snapshot;\nuse loco_gen::get_mappings;\nuse rstest::rstest;\nuse serial_test::serial;\nuse std::{collections::HashMap, env::current_dir, fs::read_to_string};\n\n#[rstest]\n#[serial]\nfn test_migrations_flow(#[values(\"postgres\", \"sqlite\")] db_kind: &str) {\n    if db_kind == \"postgres\" && std::env::var(\"DATABASE_URL\").is_err() {\n        return;\n    }\n    let tree_fs = tree_fs::TreeBuilder::default()\n        .drop(true)\n        .create()\n        .expect(\"Should create temp folder\");\n    let loco_dev_path = current_dir().unwrap();\n    let loco_dev_path = loco_dev_path.parent().unwrap();\n    // 1. install most recent dev cli: cd loco-new; cargo install --path . --force\n    // 2. when running locally set LOCO_DEV_MODE_PATH=<to local loco path>\n    // LOCO_DEV_MODE_PATH=../../ cargo run -- new\n\n    let mut env_map: HashMap<String, String> = std::env::vars().collect();\n    env_map.insert(\n        \"LOCO_DEV_MODE_PATH\".into(),\n        loco_dev_path.to_str().unwrap().to_string(),\n    );\n\n    if db_kind == \"sqlite\" {\n        env_map.remove(\"DATABASE_URL\");\n    }\n\n    cmd!(\n        \"loco\",\n        \"new\",\n        \"-n\",\n        \"myapp\",\n        \"--db\",\n        db_kind,\n        \"--bg\",\n        \"async\",\n        \"--assets\",\n        \"serverside\",\n        \"-a\"\n    )\n    .full_env(&env_map)\n    .dir(&tree_fs.root)\n    .run()\n    .expect(\"new\");\n\n    // build a mega long all-types \"title:string ...\" pairs for all types from\n    // mappings.json name of column is name of type adjusted with unique, or\n    // nonnull, etc arity arguments get manual treatment\n    let mappings = get_mappings();\n    let mut type_names = mappings\n        .all_names()\n        .iter()\n        // only take non-argument types because its easy\n        .filter(|n| mappings.col_type_arity(n).unwrap_or_default() == 0)\n        .map(|t| format!(\"{}:{t}\", t.replace('!', \"_nonull\").replace('^', \"_uniq\")))\n        .collect::<Vec<_>>();\n\n    // push arity arguments manually\n    type_names.push(\"age:decimal_len:8:24\".to_string());\n    type_names.push(\"age_nonull:decimal_len!:8:24\".to_string());\n\n    if db_kind == \"postgres\" {\n        type_names.push(\"array_string:array:string\".to_string());\n        type_names.push(\"array_float:array:float\".to_string());\n        type_names.push(\"array_int:array:int\".to_string());\n        type_names.push(\"array_double:array:double\".to_string());\n        type_names.push(\"array_bool:array:bool\".to_string());\n    }\n\n    let types_line = type_names.join(\" \");\n\n    let script = [\n        \"loco db reset\",\n        \"loco g model user name:string\",\n        \"loco g model user_without_tz name:string --without-tz\",\n        &format!(\"loco g scaffold playlists {types_line} --htmx\"),\n        &format!(\"loco g scaffold playlists_without_tz {types_line} --htmx --without-tz\"),\n        &format!(\"loco g model movies {types_line} playlist:references user:references?\"),\n        \"loco g migration AddContentToMovies content:string\",\n        \"loco g migration CreateActors foobar:string\",\n        // TBD this errors under sqlite because they don`t support alter and uniq\n        //        &format!(\"loco g migration AddAllToActors {types_line}\"),\n        \"loco g migration CreateJoinTableActorsAndMovies minutes:int\",\n        \"loco g migration CreateJoinTableUser_without_tzAndMovies minutes:int --without-tz\",\n        \"loco g migration CreateAwards name:string actor:references\",\n        \"loco g migration RemoveContentFromMovies content:string\",\n        \"loco g migration AddRatingToMovies rating:int\",\n        \"loco db migrate\",\n        \"loco db entities\",\n        \"loco db schema\",\n    ];\n\n    for line in script {\n        cmd(\"cargo\", line.split(' '))\n            .full_env(&env_map)\n            .dir(tree_fs.root.join(\"myapp\"))\n            .run()\n            .unwrap_or_else(|_| panic!(\"command {line} should run successfully\"));\n    }\n    // cargo loco build\n    assert_snapshot!(\n        format!(\"migrations_flow_{db_kind}\"),\n        read_to_string(tree_fs.root.join(\"myapp\").join(\"schema_dump.json\")).unwrap()\n    );\n}\n"
  },
  {
    "path": "loco-gen/tests/mod.rs",
    "content": "mod templates;\n"
  },
  {
    "path": "loco-gen/tests/snapshots/db__migrations_flow_postgres.snap",
    "content": "---\nsource: loco-gen/tests/db.rs\nexpression: \"read_to_string(tree_fs.root.join(\\\"myapp\\\").join(\\\"schema_dump.json\\\")).unwrap()\"\nsnapshot_kind: text\n---\n[\n  {\n    \"column\": \"minutes\",\n    \"table\": \"actor_movies\",\n    \"type\": \"integer\"\n  },\n  {\n    \"column\": \"actor_id\",\n    \"table\": \"actor_movies\",\n    \"type\": \"integer\"\n  },\n  {\n    \"column\": \"movie_id\",\n    \"table\": \"actor_movies\",\n    \"type\": \"integer\"\n  },\n  {\n    \"column\": \"created_at\",\n    \"table\": \"actors\",\n    \"type\": \"timestamp with time zone\"\n  },\n  {\n    \"column\": \"updated_at\",\n    \"table\": \"actors\",\n    \"type\": \"timestamp with time zone\"\n  },\n  {\n    \"column\": \"id\",\n    \"table\": \"actors\",\n    \"type\": \"integer\"\n  },\n  {\n    \"column\": \"foobar\",\n    \"table\": \"actors\",\n    \"type\": \"character varying\"\n  },\n  {\n    \"column\": \"created_at\",\n    \"table\": \"awards\",\n    \"type\": \"timestamp with time zone\"\n  },\n  {\n    \"column\": \"updated_at\",\n    \"table\": \"awards\",\n    \"type\": \"timestamp with time zone\"\n  },\n  {\n    \"column\": \"id\",\n    \"table\": \"awards\",\n    \"type\": \"integer\"\n  },\n  {\n    \"column\": \"name\",\n    \"table\": \"awards\",\n    \"type\": \"character varying\"\n  },\n  {\n    \"column\": \"actor_id\",\n    \"table\": \"awards\",\n    \"type\": \"integer\"\n  },\n  {\n    \"column\": \"created_at\",\n    \"table\": \"movies\",\n    \"type\": \"timestamp with time zone\"\n  },\n  {\n    \"column\": \"updated_at\",\n    \"table\": \"movies\",\n    \"type\": \"timestamp with time zone\"\n  },\n  {\n    \"column\": \"id\",\n    \"table\": \"movies\",\n    \"type\": \"integer\"\n  },\n  {\n    \"column\": \"uuid_uniq\",\n    \"table\": \"movies\",\n    \"type\": \"uuid\"\n  },\n  {\n    \"column\": \"uuid\",\n    \"table\": \"movies\",\n    \"type\": \"uuid\"\n  },\n  {\n    \"column\": \"uuid_nonull\",\n    \"table\": \"movies\",\n    \"type\": \"uuid\"\n  },\n  {\n    \"column\": \"string\",\n    \"table\": \"movies\",\n    \"type\": \"character varying\"\n  },\n  {\n    \"column\": \"string_nonull\",\n    \"table\": \"movies\",\n    \"type\": \"character varying\"\n  },\n  {\n    \"column\": \"string_uniq\",\n    \"table\": \"movies\",\n    \"type\": \"character varying\"\n  },\n  {\n    \"column\": \"text\",\n    \"table\": \"movies\",\n    \"type\": \"text\"\n  },\n  {\n    \"column\": \"text_nonull\",\n    \"table\": \"movies\",\n    \"type\": \"text\"\n  },\n  {\n    \"column\": \"text_uniq\",\n    \"table\": \"movies\",\n    \"type\": \"text\"\n  },\n  {\n    \"column\": \"small_unsigned\",\n    \"table\": \"movies\",\n    \"type\": \"smallint\"\n  },\n  {\n    \"column\": \"small_unsigned_nonull\",\n    \"table\": \"movies\",\n    \"type\": \"smallint\"\n  },\n  {\n    \"column\": \"small_unsigned_uniq\",\n    \"table\": \"movies\",\n    \"type\": \"smallint\"\n  },\n  {\n    \"column\": \"big_unsigned\",\n    \"table\": \"movies\",\n    \"type\": \"bigint\"\n  },\n  {\n    \"column\": \"big_unsigned_nonull\",\n    \"table\": \"movies\",\n    \"type\": \"bigint\"\n  },\n  {\n    \"column\": \"big_unsigned_uniq\",\n    \"table\": \"movies\",\n    \"type\": \"bigint\"\n  },\n  {\n    \"column\": \"small_int\",\n    \"table\": \"movies\",\n    \"type\": \"smallint\"\n  },\n  {\n    \"column\": \"small_int_nonull\",\n    \"table\": \"movies\",\n    \"type\": \"smallint\"\n  },\n  {\n    \"column\": \"small_int_uniq\",\n    \"table\": \"movies\",\n    \"type\": \"smallint\"\n  },\n  {\n    \"column\": \"int\",\n    \"table\": \"movies\",\n    \"type\": \"integer\"\n  },\n  {\n    \"column\": \"int_nonull\",\n    \"table\": \"movies\",\n    \"type\": \"integer\"\n  },\n  {\n    \"column\": \"int_uniq\",\n    \"table\": \"movies\",\n    \"type\": \"integer\"\n  },\n  {\n    \"column\": \"big_int\",\n    \"table\": \"movies\",\n    \"type\": \"bigint\"\n  },\n  {\n    \"column\": \"big_int_nonull\",\n    \"table\": \"movies\",\n    \"type\": \"bigint\"\n  },\n  {\n    \"column\": \"big_int_uniq\",\n    \"table\": \"movies\",\n    \"type\": \"bigint\"\n  },\n  {\n    \"column\": \"float\",\n    \"table\": \"movies\",\n    \"type\": \"real\"\n  },\n  {\n    \"column\": \"float_nonull\",\n    \"table\": \"movies\",\n    \"type\": \"real\"\n  },\n  {\n    \"column\": \"float_uniq\",\n    \"table\": \"movies\",\n    \"type\": \"real\"\n  },\n  {\n    \"column\": \"double\",\n    \"table\": \"movies\",\n    \"type\": \"double precision\"\n  },\n  {\n    \"column\": \"double_nonull\",\n    \"table\": \"movies\",\n    \"type\": \"double precision\"\n  },\n  {\n    \"column\": \"double_uniq\",\n    \"table\": \"movies\",\n    \"type\": \"double precision\"\n  },\n  {\n    \"column\": \"decimal\",\n    \"table\": \"movies\",\n    \"type\": \"numeric\"\n  },\n  {\n    \"column\": \"decimal_nonull\",\n    \"table\": \"movies\",\n    \"type\": \"numeric\"\n  },\n  {\n    \"column\": \"decimal_uniq\",\n    \"table\": \"movies\",\n    \"type\": \"numeric\"\n  },\n  {\n    \"column\": \"bool\",\n    \"table\": \"movies\",\n    \"type\": \"boolean\"\n  },\n  {\n    \"column\": \"bool_nonull\",\n    \"table\": \"movies\",\n    \"type\": \"boolean\"\n  },\n  {\n    \"column\": \"tstz\",\n    \"table\": \"movies\",\n    \"type\": \"timestamp with time zone\"\n  },\n  {\n    \"column\": \"tstz_nonull\",\n    \"table\": \"movies\",\n    \"type\": \"timestamp with time zone\"\n  },\n  {\n    \"column\": \"date\",\n    \"table\": \"movies\",\n    \"type\": \"date\"\n  },\n  {\n    \"column\": \"date_nonull\",\n    \"table\": \"movies\",\n    \"type\": \"date\"\n  },\n  {\n    \"column\": \"date_uniq\",\n    \"table\": \"movies\",\n    \"type\": \"date\"\n  },\n  {\n    \"column\": \"date_time\",\n    \"table\": \"movies\",\n    \"type\": \"timestamp without time zone\"\n  },\n  {\n    \"column\": \"date_time_nonull\",\n    \"table\": \"movies\",\n    \"type\": \"timestamp without time zone\"\n  },\n  {\n    \"column\": \"date_time_uniq\",\n    \"table\": \"movies\",\n    \"type\": \"timestamp without time zone\"\n  },\n  {\n    \"column\": \"json\",\n    \"table\": \"movies\",\n    \"type\": \"json\"\n  },\n  {\n    \"column\": \"json_nonull\",\n    \"table\": \"movies\",\n    \"type\": \"json\"\n  },\n  {\n    \"column\": \"jsonb\",\n    \"table\": \"movies\",\n    \"type\": \"jsonb\"\n  },\n  {\n    \"column\": \"jsonb_nonull\",\n    \"table\": \"movies\",\n    \"type\": \"jsonb\"\n  },\n  {\n    \"column\": \"jsonb_uniq\",\n    \"table\": \"movies\",\n    \"type\": \"jsonb\"\n  },\n  {\n    \"column\": \"blob\",\n    \"table\": \"movies\",\n    \"type\": \"bytea\"\n  },\n  {\n    \"column\": \"blob_nonull\",\n    \"table\": \"movies\",\n    \"type\": \"bytea\"\n  },\n  {\n    \"column\": \"blob_uniq\",\n    \"table\": \"movies\",\n    \"type\": \"bytea\"\n  },\n  {\n    \"column\": \"money\",\n    \"table\": \"movies\",\n    \"type\": \"money\"\n  },\n  {\n    \"column\": \"money_nonull\",\n    \"table\": \"movies\",\n    \"type\": \"money\"\n  },\n  {\n    \"column\": \"money_uniq\",\n    \"table\": \"movies\",\n    \"type\": \"money\"\n  },\n  {\n    \"column\": \"unsigned_nonull\",\n    \"table\": \"movies\",\n    \"type\": \"integer\"\n  },\n  {\n    \"column\": \"unsigned\",\n    \"table\": \"movies\",\n    \"type\": \"integer\"\n  },\n  {\n    \"column\": \"unsigned_uniq\",\n    \"table\": \"movies\",\n    \"type\": \"integer\"\n  },\n  {\n    \"column\": \"age\",\n    \"table\": \"movies\",\n    \"type\": \"numeric\"\n  },\n  {\n    \"column\": \"age_nonull\",\n    \"table\": \"movies\",\n    \"type\": \"numeric\"\n  },\n  {\n    \"column\": \"array_string\",\n    \"table\": \"movies\",\n    \"type\": \"ARRAY\"\n  },\n  {\n    \"column\": \"array_float\",\n    \"table\": \"movies\",\n    \"type\": \"ARRAY\"\n  },\n  {\n    \"column\": \"array_int\",\n    \"table\": \"movies\",\n    \"type\": \"ARRAY\"\n  },\n  {\n    \"column\": \"array_double\",\n    \"table\": \"movies\",\n    \"type\": \"ARRAY\"\n  },\n  {\n    \"column\": \"array_bool\",\n    \"table\": \"movies\",\n    \"type\": \"ARRAY\"\n  },\n  {\n    \"column\": \"playlist_id\",\n    \"table\": \"movies\",\n    \"type\": \"integer\"\n  },\n  {\n    \"column\": \"user_id\",\n    \"table\": \"movies\",\n    \"type\": \"integer\"\n  },\n  {\n    \"column\": \"rating\",\n    \"table\": \"movies\",\n    \"type\": \"integer\"\n  },\n  {\n    \"column\": \"created_at\",\n    \"table\": \"playlists\",\n    \"type\": \"timestamp with time zone\"\n  },\n  {\n    \"column\": \"updated_at\",\n    \"table\": \"playlists\",\n    \"type\": \"timestamp with time zone\"\n  },\n  {\n    \"column\": \"id\",\n    \"table\": \"playlists\",\n    \"type\": \"integer\"\n  },\n  {\n    \"column\": \"uuid_uniq\",\n    \"table\": \"playlists\",\n    \"type\": \"uuid\"\n  },\n  {\n    \"column\": \"uuid\",\n    \"table\": \"playlists\",\n    \"type\": \"uuid\"\n  },\n  {\n    \"column\": \"uuid_nonull\",\n    \"table\": \"playlists\",\n    \"type\": \"uuid\"\n  },\n  {\n    \"column\": \"string\",\n    \"table\": \"playlists\",\n    \"type\": \"character varying\"\n  },\n  {\n    \"column\": \"string_nonull\",\n    \"table\": \"playlists\",\n    \"type\": \"character varying\"\n  },\n  {\n    \"column\": \"string_uniq\",\n    \"table\": \"playlists\",\n    \"type\": \"character varying\"\n  },\n  {\n    \"column\": \"text\",\n    \"table\": \"playlists\",\n    \"type\": \"text\"\n  },\n  {\n    \"column\": \"text_nonull\",\n    \"table\": \"playlists\",\n    \"type\": \"text\"\n  },\n  {\n    \"column\": \"text_uniq\",\n    \"table\": \"playlists\",\n    \"type\": \"text\"\n  },\n  {\n    \"column\": \"small_unsigned\",\n    \"table\": \"playlists\",\n    \"type\": \"smallint\"\n  },\n  {\n    \"column\": \"small_unsigned_nonull\",\n    \"table\": \"playlists\",\n    \"type\": \"smallint\"\n  },\n  {\n    \"column\": \"small_unsigned_uniq\",\n    \"table\": \"playlists\",\n    \"type\": \"smallint\"\n  },\n  {\n    \"column\": \"big_unsigned\",\n    \"table\": \"playlists\",\n    \"type\": \"bigint\"\n  },\n  {\n    \"column\": \"big_unsigned_nonull\",\n    \"table\": \"playlists\",\n    \"type\": \"bigint\"\n  },\n  {\n    \"column\": \"big_unsigned_uniq\",\n    \"table\": \"playlists\",\n    \"type\": \"bigint\"\n  },\n  {\n    \"column\": \"small_int\",\n    \"table\": \"playlists\",\n    \"type\": \"smallint\"\n  },\n  {\n    \"column\": \"small_int_nonull\",\n    \"table\": \"playlists\",\n    \"type\": \"smallint\"\n  },\n  {\n    \"column\": \"small_int_uniq\",\n    \"table\": \"playlists\",\n    \"type\": \"smallint\"\n  },\n  {\n    \"column\": \"int\",\n    \"table\": \"playlists\",\n    \"type\": \"integer\"\n  },\n  {\n    \"column\": \"int_nonull\",\n    \"table\": \"playlists\",\n    \"type\": \"integer\"\n  },\n  {\n    \"column\": \"int_uniq\",\n    \"table\": \"playlists\",\n    \"type\": \"integer\"\n  },\n  {\n    \"column\": \"big_int\",\n    \"table\": \"playlists\",\n    \"type\": \"bigint\"\n  },\n  {\n    \"column\": \"big_int_nonull\",\n    \"table\": \"playlists\",\n    \"type\": \"bigint\"\n  },\n  {\n    \"column\": \"big_int_uniq\",\n    \"table\": \"playlists\",\n    \"type\": \"bigint\"\n  },\n  {\n    \"column\": \"float\",\n    \"table\": \"playlists\",\n    \"type\": \"real\"\n  },\n  {\n    \"column\": \"float_nonull\",\n    \"table\": \"playlists\",\n    \"type\": \"real\"\n  },\n  {\n    \"column\": \"float_uniq\",\n    \"table\": \"playlists\",\n    \"type\": \"real\"\n  },\n  {\n    \"column\": \"double\",\n    \"table\": \"playlists\",\n    \"type\": \"double precision\"\n  },\n  {\n    \"column\": \"double_nonull\",\n    \"table\": \"playlists\",\n    \"type\": \"double precision\"\n  },\n  {\n    \"column\": \"double_uniq\",\n    \"table\": \"playlists\",\n    \"type\": \"double precision\"\n  },\n  {\n    \"column\": \"decimal\",\n    \"table\": \"playlists\",\n    \"type\": \"numeric\"\n  },\n  {\n    \"column\": \"decimal_nonull\",\n    \"table\": \"playlists\",\n    \"type\": \"numeric\"\n  },\n  {\n    \"column\": \"decimal_uniq\",\n    \"table\": \"playlists\",\n    \"type\": \"numeric\"\n  },\n  {\n    \"column\": \"bool\",\n    \"table\": \"playlists\",\n    \"type\": \"boolean\"\n  },\n  {\n    \"column\": \"bool_nonull\",\n    \"table\": \"playlists\",\n    \"type\": \"boolean\"\n  },\n  {\n    \"column\": \"tstz\",\n    \"table\": \"playlists\",\n    \"type\": \"timestamp with time zone\"\n  },\n  {\n    \"column\": \"tstz_nonull\",\n    \"table\": \"playlists\",\n    \"type\": \"timestamp with time zone\"\n  },\n  {\n    \"column\": \"date\",\n    \"table\": \"playlists\",\n    \"type\": \"date\"\n  },\n  {\n    \"column\": \"date_nonull\",\n    \"table\": \"playlists\",\n    \"type\": \"date\"\n  },\n  {\n    \"column\": \"date_uniq\",\n    \"table\": \"playlists\",\n    \"type\": \"date\"\n  },\n  {\n    \"column\": \"date_time\",\n    \"table\": \"playlists\",\n    \"type\": \"timestamp without time zone\"\n  },\n  {\n    \"column\": \"date_time_nonull\",\n    \"table\": \"playlists\",\n    \"type\": \"timestamp without time zone\"\n  },\n  {\n    \"column\": \"date_time_uniq\",\n    \"table\": \"playlists\",\n    \"type\": \"timestamp without time zone\"\n  },\n  {\n    \"column\": \"json\",\n    \"table\": \"playlists\",\n    \"type\": \"json\"\n  },\n  {\n    \"column\": \"json_nonull\",\n    \"table\": \"playlists\",\n    \"type\": \"json\"\n  },\n  {\n    \"column\": \"jsonb\",\n    \"table\": \"playlists\",\n    \"type\": \"jsonb\"\n  },\n  {\n    \"column\": \"jsonb_nonull\",\n    \"table\": \"playlists\",\n    \"type\": \"jsonb\"\n  },\n  {\n    \"column\": \"jsonb_uniq\",\n    \"table\": \"playlists\",\n    \"type\": \"jsonb\"\n  },\n  {\n    \"column\": \"blob\",\n    \"table\": \"playlists\",\n    \"type\": \"bytea\"\n  },\n  {\n    \"column\": \"blob_nonull\",\n    \"table\": \"playlists\",\n    \"type\": \"bytea\"\n  },\n  {\n    \"column\": \"blob_uniq\",\n    \"table\": \"playlists\",\n    \"type\": \"bytea\"\n  },\n  {\n    \"column\": \"money\",\n    \"table\": \"playlists\",\n    \"type\": \"money\"\n  },\n  {\n    \"column\": \"money_nonull\",\n    \"table\": \"playlists\",\n    \"type\": \"money\"\n  },\n  {\n    \"column\": \"money_uniq\",\n    \"table\": \"playlists\",\n    \"type\": \"money\"\n  },\n  {\n    \"column\": \"unsigned_nonull\",\n    \"table\": \"playlists\",\n    \"type\": \"integer\"\n  },\n  {\n    \"column\": \"unsigned\",\n    \"table\": \"playlists\",\n    \"type\": \"integer\"\n  },\n  {\n    \"column\": \"unsigned_uniq\",\n    \"table\": \"playlists\",\n    \"type\": \"integer\"\n  },\n  {\n    \"column\": \"age\",\n    \"table\": \"playlists\",\n    \"type\": \"numeric\"\n  },\n  {\n    \"column\": \"age_nonull\",\n    \"table\": \"playlists\",\n    \"type\": \"numeric\"\n  },\n  {\n    \"column\": \"array_string\",\n    \"table\": \"playlists\",\n    \"type\": \"ARRAY\"\n  },\n  {\n    \"column\": \"array_float\",\n    \"table\": \"playlists\",\n    \"type\": \"ARRAY\"\n  },\n  {\n    \"column\": \"array_int\",\n    \"table\": \"playlists\",\n    \"type\": \"ARRAY\"\n  },\n  {\n    \"column\": \"array_double\",\n    \"table\": \"playlists\",\n    \"type\": \"ARRAY\"\n  },\n  {\n    \"column\": \"array_bool\",\n    \"table\": \"playlists\",\n    \"type\": \"ARRAY\"\n  },\n  {\n    \"column\": \"id\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"integer\"\n  },\n  {\n    \"column\": \"uuid_uniq\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"uuid\"\n  },\n  {\n    \"column\": \"uuid\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"uuid\"\n  },\n  {\n    \"column\": \"uuid_nonull\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"uuid\"\n  },\n  {\n    \"column\": \"string\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"character varying\"\n  },\n  {\n    \"column\": \"string_nonull\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"character varying\"\n  },\n  {\n    \"column\": \"string_uniq\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"character varying\"\n  },\n  {\n    \"column\": \"text\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"text\"\n  },\n  {\n    \"column\": \"text_nonull\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"text\"\n  },\n  {\n    \"column\": \"text_uniq\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"text\"\n  },\n  {\n    \"column\": \"small_unsigned\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"smallint\"\n  },\n  {\n    \"column\": \"small_unsigned_nonull\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"smallint\"\n  },\n  {\n    \"column\": \"small_unsigned_uniq\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"smallint\"\n  },\n  {\n    \"column\": \"big_unsigned\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"bigint\"\n  },\n  {\n    \"column\": \"big_unsigned_nonull\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"bigint\"\n  },\n  {\n    \"column\": \"big_unsigned_uniq\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"bigint\"\n  },\n  {\n    \"column\": \"small_int\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"smallint\"\n  },\n  {\n    \"column\": \"small_int_nonull\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"smallint\"\n  },\n  {\n    \"column\": \"small_int_uniq\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"smallint\"\n  },\n  {\n    \"column\": \"int\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"integer\"\n  },\n  {\n    \"column\": \"int_nonull\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"integer\"\n  },\n  {\n    \"column\": \"int_uniq\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"integer\"\n  },\n  {\n    \"column\": \"big_int\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"bigint\"\n  },\n  {\n    \"column\": \"big_int_nonull\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"bigint\"\n  },\n  {\n    \"column\": \"big_int_uniq\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"bigint\"\n  },\n  {\n    \"column\": \"float\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"real\"\n  },\n  {\n    \"column\": \"float_nonull\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"real\"\n  },\n  {\n    \"column\": \"float_uniq\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"real\"\n  },\n  {\n    \"column\": \"double\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"double precision\"\n  },\n  {\n    \"column\": \"double_nonull\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"double precision\"\n  },\n  {\n    \"column\": \"double_uniq\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"double precision\"\n  },\n  {\n    \"column\": \"decimal\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"numeric\"\n  },\n  {\n    \"column\": \"decimal_nonull\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"numeric\"\n  },\n  {\n    \"column\": \"decimal_uniq\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"numeric\"\n  },\n  {\n    \"column\": \"bool\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"boolean\"\n  },\n  {\n    \"column\": \"bool_nonull\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"boolean\"\n  },\n  {\n    \"column\": \"tstz\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"timestamp with time zone\"\n  },\n  {\n    \"column\": \"tstz_nonull\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"timestamp with time zone\"\n  },\n  {\n    \"column\": \"date\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"date\"\n  },\n  {\n    \"column\": \"date_nonull\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"date\"\n  },\n  {\n    \"column\": \"date_uniq\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"date\"\n  },\n  {\n    \"column\": \"date_time\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"timestamp without time zone\"\n  },\n  {\n    \"column\": \"date_time_nonull\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"timestamp without time zone\"\n  },\n  {\n    \"column\": \"date_time_uniq\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"timestamp without time zone\"\n  },\n  {\n    \"column\": \"json\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"json\"\n  },\n  {\n    \"column\": \"json_nonull\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"json\"\n  },\n  {\n    \"column\": \"jsonb\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"jsonb\"\n  },\n  {\n    \"column\": \"jsonb_nonull\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"jsonb\"\n  },\n  {\n    \"column\": \"jsonb_uniq\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"jsonb\"\n  },\n  {\n    \"column\": \"blob\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"bytea\"\n  },\n  {\n    \"column\": \"blob_nonull\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"bytea\"\n  },\n  {\n    \"column\": \"blob_uniq\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"bytea\"\n  },\n  {\n    \"column\": \"money\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"money\"\n  },\n  {\n    \"column\": \"money_nonull\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"money\"\n  },\n  {\n    \"column\": \"money_uniq\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"money\"\n  },\n  {\n    \"column\": \"unsigned_nonull\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"integer\"\n  },\n  {\n    \"column\": \"unsigned\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"integer\"\n  },\n  {\n    \"column\": \"unsigned_uniq\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"integer\"\n  },\n  {\n    \"column\": \"age\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"numeric\"\n  },\n  {\n    \"column\": \"age_nonull\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"numeric\"\n  },\n  {\n    \"column\": \"array_string\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"ARRAY\"\n  },\n  {\n    \"column\": \"array_float\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"ARRAY\"\n  },\n  {\n    \"column\": \"array_int\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"ARRAY\"\n  },\n  {\n    \"column\": \"array_double\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"ARRAY\"\n  },\n  {\n    \"column\": \"array_bool\",\n    \"table\": \"playlists_without_tzs\",\n    \"type\": \"ARRAY\"\n  },\n  {\n    \"column\": \"version\",\n    \"table\": \"seaql_migrations\",\n    \"type\": \"character varying\"\n  },\n  {\n    \"column\": \"applied_at\",\n    \"table\": \"seaql_migrations\",\n    \"type\": \"bigint\"\n  },\n  {\n    \"column\": \"minutes\",\n    \"table\": \"user_without_tz_movies\",\n    \"type\": \"integer\"\n  },\n  {\n    \"column\": \"user_without_tz_id\",\n    \"table\": \"user_without_tz_movies\",\n    \"type\": \"integer\"\n  },\n  {\n    \"column\": \"movie_id\",\n    \"table\": \"user_without_tz_movies\",\n    \"type\": \"integer\"\n  },\n  {\n    \"column\": \"id\",\n    \"table\": \"user_without_tzs\",\n    \"type\": \"integer\"\n  },\n  {\n    \"column\": \"name\",\n    \"table\": \"user_without_tzs\",\n    \"type\": \"character varying\"\n  },\n  {\n    \"column\": \"created_at\",\n    \"table\": \"users\",\n    \"type\": \"timestamp with time zone\"\n  },\n  {\n    \"column\": \"updated_at\",\n    \"table\": \"users\",\n    \"type\": \"timestamp with time zone\"\n  },\n  {\n    \"column\": \"id\",\n    \"table\": \"users\",\n    \"type\": \"integer\"\n  },\n  {\n    \"column\": \"pid\",\n    \"table\": \"users\",\n    \"type\": \"uuid\"\n  },\n  {\n    \"column\": \"email\",\n    \"table\": \"users\",\n    \"type\": \"character varying\"\n  },\n  {\n    \"column\": \"password\",\n    \"table\": \"users\",\n    \"type\": \"character varying\"\n  },\n  {\n    \"column\": \"api_key\",\n    \"table\": \"users\",\n    \"type\": \"character varying\"\n  },\n  {\n    \"column\": \"name\",\n    \"table\": \"users\",\n    \"type\": \"character varying\"\n  },\n  {\n    \"column\": \"reset_token\",\n    \"table\": \"users\",\n    \"type\": \"character varying\"\n  },\n  {\n    \"column\": \"reset_sent_at\",\n    \"table\": \"users\",\n    \"type\": \"timestamp with time zone\"\n  },\n  {\n    \"column\": \"email_verification_token\",\n    \"table\": \"users\",\n    \"type\": \"character varying\"\n  },\n  {\n    \"column\": \"email_verification_sent_at\",\n    \"table\": \"users\",\n    \"type\": \"timestamp with time zone\"\n  },\n  {\n    \"column\": \"email_verified_at\",\n    \"table\": \"users\",\n    \"type\": \"timestamp with time zone\"\n  },\n  {\n    \"column\": \"magic_link_token\",\n    \"table\": \"users\",\n    \"type\": \"character varying\"\n  },\n  {\n    \"column\": \"magic_link_expiration\",\n    \"table\": \"users\",\n    \"type\": \"timestamp with time zone\"\n  }\n]\n"
  },
  {
    "path": "loco-gen/tests/snapshots/db__migrations_flow_sqlite.snap",
    "content": "---\nsource: loco-gen/tests/db.rs\nexpression: \"read_to_string(tree_fs.root.join(\\\"myapp\\\").join(\\\"schema_dump.json\\\")).unwrap()\"\nsnapshot_kind: text\n---\n[\n  {\n    \"sql\": \"CREATE TABLE \\\"actor_movies\\\" ( \\\"minutes\\\" integer NULL, \\\"actor_id\\\" integer NOT NULL, \\\"movie_id\\\" integer NOT NULL, CONSTRAINT \\\"idx-actor_movies-refs-pk\\\" PRIMARY KEY (\\\"actor_id\\\", \\\"movie_id\\\"), FOREIGN KEY (\\\"actor_id\\\") REFERENCES \\\"actors\\\" (\\\"id\\\") ON DELETE CASCADE ON UPDATE CASCADE, FOREIGN KEY (\\\"movie_id\\\") REFERENCES \\\"movies\\\" (\\\"id\\\") ON DELETE CASCADE ON UPDATE CASCADE )\",\n    \"table\": \"actor_movies\"\n  },\n  {\n    \"sql\": \"CREATE TABLE \\\"actors\\\" ( \\\"created_at\\\" timestamp_with_timezone_text NOT NULL DEFAULT CURRENT_TIMESTAMP, \\\"updated_at\\\" timestamp_with_timezone_text NOT NULL DEFAULT CURRENT_TIMESTAMP, \\\"id\\\" integer NOT NULL PRIMARY KEY AUTOINCREMENT, \\\"foobar\\\" varchar NULL )\",\n    \"table\": \"actors\"\n  },\n  {\n    \"sql\": \"CREATE TABLE \\\"awards\\\" ( \\\"created_at\\\" timestamp_with_timezone_text NOT NULL DEFAULT CURRENT_TIMESTAMP, \\\"updated_at\\\" timestamp_with_timezone_text NOT NULL DEFAULT CURRENT_TIMESTAMP, \\\"id\\\" integer NOT NULL PRIMARY KEY AUTOINCREMENT, \\\"name\\\" varchar NULL, \\\"actor_id\\\" integer NOT NULL, FOREIGN KEY (\\\"actor_id\\\") REFERENCES \\\"actors\\\" (\\\"id\\\") ON DELETE CASCADE ON UPDATE CASCADE )\",\n    \"table\": \"awards\"\n  },\n  {\n    \"sql\": \"CREATE TABLE \\\"movies\\\" ( \\\"created_at\\\" timestamp_with_timezone_text NOT NULL DEFAULT CURRENT_TIMESTAMP, \\\"updated_at\\\" timestamp_with_timezone_text NOT NULL DEFAULT CURRENT_TIMESTAMP, \\\"id\\\" integer NOT NULL PRIMARY KEY AUTOINCREMENT, \\\"uuid_uniq\\\" uuid_text NOT NULL UNIQUE, \\\"uuid\\\" uuid_text NULL, \\\"uuid_nonull\\\" uuid_text NOT NULL, \\\"string\\\" varchar NULL, \\\"string_nonull\\\" varchar NOT NULL, \\\"string_uniq\\\" varchar NOT NULL UNIQUE, \\\"text\\\" text NULL, \\\"text_nonull\\\" text NOT NULL, \\\"text_uniq\\\" text NOT NULL UNIQUE, \\\"small_unsigned\\\" smallint NULL, \\\"small_unsigned_nonull\\\" smallint NOT NULL, \\\"small_unsigned_uniq\\\" smallint NOT NULL UNIQUE, \\\"big_unsigned\\\" bigint NULL, \\\"big_unsigned_nonull\\\" bigint NOT NULL, \\\"big_unsigned_uniq\\\" bigint NOT NULL UNIQUE, \\\"small_int\\\" smallint NULL, \\\"small_int_nonull\\\" smallint NOT NULL, \\\"small_int_uniq\\\" smallint NOT NULL UNIQUE, \\\"int\\\" integer NULL, \\\"int_nonull\\\" integer NOT NULL, \\\"int_uniq\\\" integer NOT NULL UNIQUE, \\\"big_int\\\" bigint NULL, \\\"big_int_nonull\\\" bigint NOT NULL, \\\"big_int_uniq\\\" bigint NOT NULL UNIQUE, \\\"float\\\" float NULL, \\\"float_nonull\\\" float NOT NULL, \\\"float_uniq\\\" float NOT NULL UNIQUE, \\\"double\\\" double NULL, \\\"double_nonull\\\" double NOT NULL, \\\"double_uniq\\\" double NOT NULL UNIQUE, \\\"decimal\\\" real NULL, \\\"decimal_nonull\\\" real NOT NULL, \\\"decimal_uniq\\\" real NOT NULL UNIQUE, \\\"bool\\\" boolean NULL, \\\"bool_nonull\\\" boolean NOT NULL, \\\"tstz\\\" timestamp_with_timezone_text NULL, \\\"tstz_nonull\\\" timestamp_with_timezone_text NOT NULL, \\\"date\\\" date_text NULL, \\\"date_nonull\\\" date_text NOT NULL, \\\"date_uniq\\\" date_text NOT NULL UNIQUE, \\\"date_time\\\" datetime_text NULL, \\\"date_time_nonull\\\" datetime_text NOT NULL, \\\"date_time_uniq\\\" datetime_text NOT NULL UNIQUE, \\\"json\\\" json_text NULL, \\\"json_nonull\\\" json_text NOT NULL, \\\"jsonb\\\" jsonb_text NULL, \\\"jsonb_nonull\\\" jsonb_text NOT NULL, \\\"jsonb_uniq\\\" jsonb_text NOT NULL UNIQUE, \\\"blob\\\" blob NULL, \\\"blob_nonull\\\" blob NOT NULL, \\\"blob_uniq\\\" blob NOT NULL UNIQUE, \\\"money\\\" real_money NULL, \\\"money_nonull\\\" real_money NOT NULL, \\\"money_uniq\\\" real_money NOT NULL UNIQUE, \\\"unsigned_nonull\\\" integer NOT NULL, \\\"unsigned\\\" integer NULL, \\\"unsigned_uniq\\\" integer NOT NULL UNIQUE, \\\"age\\\" real(8, 24) NULL, \\\"age_nonull\\\" real(8, 24) NOT NULL, \\\"playlist_id\\\" integer NOT NULL, \\\"user_id\\\" integer NULL, \\\"rating\\\" integer NULL, FOREIGN KEY (\\\"playlist_id\\\") REFERENCES \\\"playlists\\\" (\\\"id\\\") ON DELETE CASCADE ON UPDATE CASCADE, FOREIGN KEY (\\\"user_id\\\") REFERENCES \\\"users\\\" (\\\"id\\\") ON DELETE SET NULL ON UPDATE NO ACTION )\",\n    \"table\": \"movies\"\n  },\n  {\n    \"sql\": \"CREATE TABLE \\\"playlists\\\" ( \\\"created_at\\\" timestamp_with_timezone_text NOT NULL DEFAULT CURRENT_TIMESTAMP, \\\"updated_at\\\" timestamp_with_timezone_text NOT NULL DEFAULT CURRENT_TIMESTAMP, \\\"id\\\" integer NOT NULL PRIMARY KEY AUTOINCREMENT, \\\"uuid_uniq\\\" uuid_text NOT NULL UNIQUE, \\\"uuid\\\" uuid_text NULL, \\\"uuid_nonull\\\" uuid_text NOT NULL, \\\"string\\\" varchar NULL, \\\"string_nonull\\\" varchar NOT NULL, \\\"string_uniq\\\" varchar NOT NULL UNIQUE, \\\"text\\\" text NULL, \\\"text_nonull\\\" text NOT NULL, \\\"text_uniq\\\" text NOT NULL UNIQUE, \\\"small_unsigned\\\" smallint NULL, \\\"small_unsigned_nonull\\\" smallint NOT NULL, \\\"small_unsigned_uniq\\\" smallint NOT NULL UNIQUE, \\\"big_unsigned\\\" bigint NULL, \\\"big_unsigned_nonull\\\" bigint NOT NULL, \\\"big_unsigned_uniq\\\" bigint NOT NULL UNIQUE, \\\"small_int\\\" smallint NULL, \\\"small_int_nonull\\\" smallint NOT NULL, \\\"small_int_uniq\\\" smallint NOT NULL UNIQUE, \\\"int\\\" integer NULL, \\\"int_nonull\\\" integer NOT NULL, \\\"int_uniq\\\" integer NOT NULL UNIQUE, \\\"big_int\\\" bigint NULL, \\\"big_int_nonull\\\" bigint NOT NULL, \\\"big_int_uniq\\\" bigint NOT NULL UNIQUE, \\\"float\\\" float NULL, \\\"float_nonull\\\" float NOT NULL, \\\"float_uniq\\\" float NOT NULL UNIQUE, \\\"double\\\" double NULL, \\\"double_nonull\\\" double NOT NULL, \\\"double_uniq\\\" double NOT NULL UNIQUE, \\\"decimal\\\" real NULL, \\\"decimal_nonull\\\" real NOT NULL, \\\"decimal_uniq\\\" real NOT NULL UNIQUE, \\\"bool\\\" boolean NULL, \\\"bool_nonull\\\" boolean NOT NULL, \\\"tstz\\\" timestamp_with_timezone_text NULL, \\\"tstz_nonull\\\" timestamp_with_timezone_text NOT NULL, \\\"date\\\" date_text NULL, \\\"date_nonull\\\" date_text NOT NULL, \\\"date_uniq\\\" date_text NOT NULL UNIQUE, \\\"date_time\\\" datetime_text NULL, \\\"date_time_nonull\\\" datetime_text NOT NULL, \\\"date_time_uniq\\\" datetime_text NOT NULL UNIQUE, \\\"json\\\" json_text NULL, \\\"json_nonull\\\" json_text NOT NULL, \\\"jsonb\\\" jsonb_text NULL, \\\"jsonb_nonull\\\" jsonb_text NOT NULL, \\\"jsonb_uniq\\\" jsonb_text NOT NULL UNIQUE, \\\"blob\\\" blob NULL, \\\"blob_nonull\\\" blob NOT NULL, \\\"blob_uniq\\\" blob NOT NULL UNIQUE, \\\"money\\\" real_money NULL, \\\"money_nonull\\\" real_money NOT NULL, \\\"money_uniq\\\" real_money NOT NULL UNIQUE, \\\"unsigned_nonull\\\" integer NOT NULL, \\\"unsigned\\\" integer NULL, \\\"unsigned_uniq\\\" integer NOT NULL UNIQUE, \\\"age\\\" real(8, 24) NULL, \\\"age_nonull\\\" real(8, 24) NOT NULL )\",\n    \"table\": \"playlists\"\n  },\n  {\n    \"sql\": \"CREATE TABLE \\\"playlists_without_tzs\\\" ( \\\"id\\\" integer NOT NULL PRIMARY KEY AUTOINCREMENT, \\\"uuid_uniq\\\" uuid_text NOT NULL UNIQUE, \\\"uuid\\\" uuid_text NULL, \\\"uuid_nonull\\\" uuid_text NOT NULL, \\\"string\\\" varchar NULL, \\\"string_nonull\\\" varchar NOT NULL, \\\"string_uniq\\\" varchar NOT NULL UNIQUE, \\\"text\\\" text NULL, \\\"text_nonull\\\" text NOT NULL, \\\"text_uniq\\\" text NOT NULL UNIQUE, \\\"small_unsigned\\\" smallint NULL, \\\"small_unsigned_nonull\\\" smallint NOT NULL, \\\"small_unsigned_uniq\\\" smallint NOT NULL UNIQUE, \\\"big_unsigned\\\" bigint NULL, \\\"big_unsigned_nonull\\\" bigint NOT NULL, \\\"big_unsigned_uniq\\\" bigint NOT NULL UNIQUE, \\\"small_int\\\" smallint NULL, \\\"small_int_nonull\\\" smallint NOT NULL, \\\"small_int_uniq\\\" smallint NOT NULL UNIQUE, \\\"int\\\" integer NULL, \\\"int_nonull\\\" integer NOT NULL, \\\"int_uniq\\\" integer NOT NULL UNIQUE, \\\"big_int\\\" bigint NULL, \\\"big_int_nonull\\\" bigint NOT NULL, \\\"big_int_uniq\\\" bigint NOT NULL UNIQUE, \\\"float\\\" float NULL, \\\"float_nonull\\\" float NOT NULL, \\\"float_uniq\\\" float NOT NULL UNIQUE, \\\"double\\\" double NULL, \\\"double_nonull\\\" double NOT NULL, \\\"double_uniq\\\" double NOT NULL UNIQUE, \\\"decimal\\\" real NULL, \\\"decimal_nonull\\\" real NOT NULL, \\\"decimal_uniq\\\" real NOT NULL UNIQUE, \\\"bool\\\" boolean NULL, \\\"bool_nonull\\\" boolean NOT NULL, \\\"tstz\\\" timestamp_with_timezone_text NULL, \\\"tstz_nonull\\\" timestamp_with_timezone_text NOT NULL, \\\"date\\\" date_text NULL, \\\"date_nonull\\\" date_text NOT NULL, \\\"date_uniq\\\" date_text NOT NULL UNIQUE, \\\"date_time\\\" datetime_text NULL, \\\"date_time_nonull\\\" datetime_text NOT NULL, \\\"date_time_uniq\\\" datetime_text NOT NULL UNIQUE, \\\"json\\\" json_text NULL, \\\"json_nonull\\\" json_text NOT NULL, \\\"jsonb\\\" jsonb_text NULL, \\\"jsonb_nonull\\\" jsonb_text NOT NULL, \\\"jsonb_uniq\\\" jsonb_text NOT NULL UNIQUE, \\\"blob\\\" blob NULL, \\\"blob_nonull\\\" blob NOT NULL, \\\"blob_uniq\\\" blob NOT NULL UNIQUE, \\\"money\\\" real_money NULL, \\\"money_nonull\\\" real_money NOT NULL, \\\"money_uniq\\\" real_money NOT NULL UNIQUE, \\\"unsigned_nonull\\\" integer NOT NULL, \\\"unsigned\\\" integer NULL, \\\"unsigned_uniq\\\" integer NOT NULL UNIQUE, \\\"age\\\" real(8, 24) NULL, \\\"age_nonull\\\" real(8, 24) NOT NULL )\",\n    \"table\": \"playlists_without_tzs\"\n  },\n  {\n    \"sql\": \"CREATE TABLE \\\"seaql_migrations\\\" ( \\\"version\\\" varchar NOT NULL PRIMARY KEY, \\\"applied_at\\\" bigint NOT NULL )\",\n    \"table\": \"seaql_migrations\"\n  },\n  {\n    \"sql\": \"CREATE TABLE \\\"user_without_tz_movies\\\" ( \\\"minutes\\\" integer NULL, \\\"user_without_tz_id\\\" integer NOT NULL, \\\"movie_id\\\" integer NOT NULL, CONSTRAINT \\\"idx-user_without_tz_movies-refs-pk\\\" PRIMARY KEY (\\\"user_without_tz_id\\\", \\\"movie_id\\\"), FOREIGN KEY (\\\"user_without_tz_id\\\") REFERENCES \\\"user_without_tzs\\\" (\\\"id\\\") ON DELETE CASCADE ON UPDATE CASCADE, FOREIGN KEY (\\\"movie_id\\\") REFERENCES \\\"movies\\\" (\\\"id\\\") ON DELETE CASCADE ON UPDATE CASCADE )\",\n    \"table\": \"user_without_tz_movies\"\n  },\n  {\n    \"sql\": \"CREATE TABLE \\\"user_without_tzs\\\" ( \\\"id\\\" integer NOT NULL PRIMARY KEY AUTOINCREMENT, \\\"name\\\" varchar NULL )\",\n    \"table\": \"user_without_tzs\"\n  },\n  {\n    \"sql\": \"CREATE TABLE \\\"users\\\" ( \\\"created_at\\\" timestamp_with_timezone_text NOT NULL DEFAULT CURRENT_TIMESTAMP, \\\"updated_at\\\" timestamp_with_timezone_text NOT NULL DEFAULT CURRENT_TIMESTAMP, \\\"id\\\" integer NOT NULL PRIMARY KEY AUTOINCREMENT, \\\"pid\\\" uuid_text NOT NULL, \\\"email\\\" varchar NOT NULL UNIQUE, \\\"password\\\" varchar NOT NULL, \\\"api_key\\\" varchar NOT NULL UNIQUE, \\\"name\\\" varchar NOT NULL, \\\"reset_token\\\" varchar NULL, \\\"reset_sent_at\\\" timestamp_with_timezone_text NULL, \\\"email_verification_token\\\" varchar NULL, \\\"email_verification_sent_at\\\" timestamp_with_timezone_text NULL, \\\"email_verified_at\\\" timestamp_with_timezone_text NULL, \\\"magic_link_token\\\" varchar NULL, \\\"magic_link_expiration\\\" timestamp_with_timezone_text NULL )\",\n    \"table\": \"users\"\n  }\n]\n"
  },
  {
    "path": "loco-gen/tests/snapshots/r#mod__db__migrations_flow_postgres.snap",
    "content": "---\nsource: loco-gen/tests/db.rs\nexpression: \"read_to_string(tree_fs.root.join(\\\"myapp\\\").join(\\\"schema_dump.json\\\")).unwrap()\"\n---\n[\n  {\n    \"column\": \"created_at\",\n    \"table\": \"actor_movies\",\n    \"type\": \"timestamp with time zone\"\n  },\n  {\n    \"column\": \"updated_at\",\n    \"table\": \"actor_movies\",\n    \"type\": \"timestamp with time zone\"\n  },\n  {\n    \"column\": \"actor_id\",\n    \"table\": \"actor_movies\",\n    \"type\": \"integer\"\n  },\n  {\n    \"column\": \"movie_id\",\n    \"table\": \"actor_movies\",\n    \"type\": \"integer\"\n  },\n  {\n    \"column\": \"created_at\",\n    \"table\": \"actors\",\n    \"type\": \"timestamp with time zone\"\n  },\n  {\n    \"column\": \"updated_at\",\n    \"table\": \"actors\",\n    \"type\": \"timestamp with time zone\"\n  },\n  {\n    \"column\": \"id\",\n    \"table\": \"actors\",\n    \"type\": \"integer\"\n  },\n  {\n    \"column\": \"foobar\",\n    \"table\": \"actors\",\n    \"type\": \"character varying\"\n  },\n  {\n    \"column\": \"created_at\",\n    \"table\": \"awards\",\n    \"type\": \"timestamp with time zone\"\n  },\n  {\n    \"column\": \"updated_at\",\n    \"table\": \"awards\",\n    \"type\": \"timestamp with time zone\"\n  },\n  {\n    \"column\": \"id\",\n    \"table\": \"awards\",\n    \"type\": \"integer\"\n  },\n  {\n    \"column\": \"name\",\n    \"table\": \"awards\",\n    \"type\": \"character varying\"\n  },\n  {\n    \"column\": \"actor_id\",\n    \"table\": \"awards\",\n    \"type\": \"integer\"\n  },\n  {\n    \"column\": \"created_at\",\n    \"table\": \"movies\",\n    \"type\": \"timestamp with time zone\"\n  },\n  {\n    \"column\": \"updated_at\",\n    \"table\": \"movies\",\n    \"type\": \"timestamp with time zone\"\n  },\n  {\n    \"column\": \"id\",\n    \"table\": \"movies\",\n    \"type\": \"integer\"\n  },\n  {\n    \"column\": \"uuid_uniq\",\n    \"table\": \"movies\",\n    \"type\": \"uuid\"\n  },\n  {\n    \"column\": \"uuid\",\n    \"table\": \"movies\",\n    \"type\": \"uuid\"\n  },\n  {\n    \"column\": \"uuid_nonull\",\n    \"table\": \"movies\",\n    \"type\": \"uuid\"\n  },\n  {\n    \"column\": \"string\",\n    \"table\": \"movies\",\n    \"type\": \"character varying\"\n  },\n  {\n    \"column\": \"string_nonull\",\n    \"table\": \"movies\",\n    \"type\": \"character varying\"\n  },\n  {\n    \"column\": \"string_uniq\",\n    \"table\": \"movies\",\n    \"type\": \"character varying\"\n  },\n  {\n    \"column\": \"text\",\n    \"table\": \"movies\",\n    \"type\": \"text\"\n  },\n  {\n    \"column\": \"text_nonull\",\n    \"table\": \"movies\",\n    \"type\": \"text\"\n  },\n  {\n    \"column\": \"text_uniq\",\n    \"table\": \"movies\",\n    \"type\": \"text\"\n  },\n  {\n    \"column\": \"small_unsigned\",\n    \"table\": \"movies\",\n    \"type\": \"smallint\"\n  },\n  {\n    \"column\": \"small_unsigned_nonull\",\n    \"table\": \"movies\",\n    \"type\": \"smallint\"\n  },\n  {\n    \"column\": \"small_unsigned_uniq\",\n    \"table\": \"movies\",\n    \"type\": \"smallint\"\n  },\n  {\n    \"column\": \"big_unsigned\",\n    \"table\": \"movies\",\n    \"type\": \"bigint\"\n  },\n  {\n    \"column\": \"big_unsigned_nonull\",\n    \"table\": \"movies\",\n    \"type\": \"bigint\"\n  },\n  {\n    \"column\": \"big_unsigned_uniq\",\n    \"table\": \"movies\",\n    \"type\": \"bigint\"\n  },\n  {\n    \"column\": \"small_int\",\n    \"table\": \"movies\",\n    \"type\": \"smallint\"\n  },\n  {\n    \"column\": \"small_int_nonull\",\n    \"table\": \"movies\",\n    \"type\": \"smallint\"\n  },\n  {\n    \"column\": \"small_int_uniq\",\n    \"table\": \"movies\",\n    \"type\": \"smallint\"\n  },\n  {\n    \"column\": \"int\",\n    \"table\": \"movies\",\n    \"type\": \"integer\"\n  },\n  {\n    \"column\": \"int_nonull\",\n    \"table\": \"movies\",\n    \"type\": \"integer\"\n  },\n  {\n    \"column\": \"int_uniq\",\n    \"table\": \"movies\",\n    \"type\": \"integer\"\n  },\n  {\n    \"column\": \"big_int\",\n    \"table\": \"movies\",\n    \"type\": \"bigint\"\n  },\n  {\n    \"column\": \"big_int_nonull\",\n    \"table\": \"movies\",\n    \"type\": \"bigint\"\n  },\n  {\n    \"column\": \"big_int_uniq\",\n    \"table\": \"movies\",\n    \"type\": \"bigint\"\n  },\n  {\n    \"column\": \"float\",\n    \"table\": \"movies\",\n    \"type\": \"real\"\n  },\n  {\n    \"column\": \"float_nonull\",\n    \"table\": \"movies\",\n    \"type\": \"real\"\n  },\n  {\n    \"column\": \"float_uniq\",\n    \"table\": \"movies\",\n    \"type\": \"real\"\n  },\n  {\n    \"column\": \"double\",\n    \"table\": \"movies\",\n    \"type\": \"double precision\"\n  },\n  {\n    \"column\": \"double_nonull\",\n    \"table\": \"movies\",\n    \"type\": \"double precision\"\n  },\n  {\n    \"column\": \"double_uniq\",\n    \"table\": \"movies\",\n    \"type\": \"double precision\"\n  },\n  {\n    \"column\": \"decimal\",\n    \"table\": \"movies\",\n    \"type\": \"numeric\"\n  },\n  {\n    \"column\": \"decimal_nonull\",\n    \"table\": \"movies\",\n    \"type\": \"numeric\"\n  },\n  {\n    \"column\": \"decimal_uniq\",\n    \"table\": \"movies\",\n    \"type\": \"numeric\"\n  },\n  {\n    \"column\": \"bool\",\n    \"table\": \"movies\",\n    \"type\": \"boolean\"\n  },\n  {\n    \"column\": \"bool_nonull\",\n    \"table\": \"movies\",\n    \"type\": \"boolean\"\n  },\n  {\n    \"column\": \"tstz\",\n    \"table\": \"movies\",\n    \"type\": \"timestamp with time zone\"\n  },\n  {\n    \"column\": \"tstz_nonull\",\n    \"table\": \"movies\",\n    \"type\": \"timestamp with time zone\"\n  },\n  {\n    \"column\": \"date\",\n    \"table\": \"movies\",\n    \"type\": \"date\"\n  },\n  {\n    \"column\": \"date_nonull\",\n    \"table\": \"movies\",\n    \"type\": \"date\"\n  },\n  {\n    \"column\": \"date_uniq\",\n    \"table\": \"movies\",\n    \"type\": \"date\"\n  },\n  {\n    \"column\": \"date_time\",\n    \"table\": \"movies\",\n    \"type\": \"timestamp without time zone\"\n  },\n  {\n    \"column\": \"date_time_nonull\",\n    \"table\": \"movies\",\n    \"type\": \"timestamp without time zone\"\n  },\n  {\n    \"column\": \"date_time_uniq\",\n    \"table\": \"movies\",\n    \"type\": \"timestamp without time zone\"\n  },\n  {\n    \"column\": \"json\",\n    \"table\": \"movies\",\n    \"type\": \"json\"\n  },\n  {\n    \"column\": \"json_nonull\",\n    \"table\": \"movies\",\n    \"type\": \"json\"\n  },\n  {\n    \"column\": \"jsonb\",\n    \"table\": \"movies\",\n    \"type\": \"jsonb\"\n  },\n  {\n    \"column\": \"jsonb_nonull\",\n    \"table\": \"movies\",\n    \"type\": \"jsonb\"\n  },\n  {\n    \"column\": \"jsonb_uniq\",\n    \"table\": \"movies\",\n    \"type\": \"jsonb\"\n  },\n  {\n    \"column\": \"blob\",\n    \"table\": \"movies\",\n    \"type\": \"bytea\"\n  },\n  {\n    \"column\": \"blob_nonull\",\n    \"table\": \"movies\",\n    \"type\": \"bytea\"\n  },\n  {\n    \"column\": \"blob_uniq\",\n    \"table\": \"movies\",\n    \"type\": \"bytea\"\n  },\n  {\n    \"column\": \"money\",\n    \"table\": \"movies\",\n    \"type\": \"money\"\n  },\n  {\n    \"column\": \"money_nonull\",\n    \"table\": \"movies\",\n    \"type\": \"money\"\n  },\n  {\n    \"column\": \"money_uniq\",\n    \"table\": \"movies\",\n    \"type\": \"money\"\n  },\n  {\n    \"column\": \"unsigned_nonull\",\n    \"table\": \"movies\",\n    \"type\": \"integer\"\n  },\n  {\n    \"column\": \"unsigned\",\n    \"table\": \"movies\",\n    \"type\": \"integer\"\n  },\n  {\n    \"column\": \"unsigned_uniq\",\n    \"table\": \"movies\",\n    \"type\": \"integer\"\n  },\n  {\n    \"column\": \"age\",\n    \"table\": \"movies\",\n    \"type\": \"numeric\"\n  },\n  {\n    \"column\": \"age_nonull\",\n    \"table\": \"movies\",\n    \"type\": \"numeric\"\n  },\n  {\n    \"column\": \"array_string\",\n    \"table\": \"movies\",\n    \"type\": \"ARRAY\"\n  },\n  {\n    \"column\": \"array_float\",\n    \"table\": \"movies\",\n    \"type\": \"ARRAY\"\n  },\n  {\n    \"column\": \"array_int\",\n    \"table\": \"movies\",\n    \"type\": \"ARRAY\"\n  },\n  {\n    \"column\": \"array_double\",\n    \"table\": \"movies\",\n    \"type\": \"ARRAY\"\n  },\n  {\n    \"column\": \"array_bool\",\n    \"table\": \"movies\",\n    \"type\": \"ARRAY\"\n  },\n  {\n    \"column\": \"playlist_id\",\n    \"table\": \"movies\",\n    \"type\": \"integer\"\n  },\n  {\n    \"column\": \"rating\",\n    \"table\": \"movies\",\n    \"type\": \"integer\"\n  },\n  {\n    \"column\": \"created_at\",\n    \"table\": \"playlists\",\n    \"type\": \"timestamp with time zone\"\n  },\n  {\n    \"column\": \"updated_at\",\n    \"table\": \"playlists\",\n    \"type\": \"timestamp with time zone\"\n  },\n  {\n    \"column\": \"id\",\n    \"table\": \"playlists\",\n    \"type\": \"integer\"\n  },\n  {\n    \"column\": \"uuid_uniq\",\n    \"table\": \"playlists\",\n    \"type\": \"uuid\"\n  },\n  {\n    \"column\": \"uuid\",\n    \"table\": \"playlists\",\n    \"type\": \"uuid\"\n  },\n  {\n    \"column\": \"uuid_nonull\",\n    \"table\": \"playlists\",\n    \"type\": \"uuid\"\n  },\n  {\n    \"column\": \"string\",\n    \"table\": \"playlists\",\n    \"type\": \"character varying\"\n  },\n  {\n    \"column\": \"string_nonull\",\n    \"table\": \"playlists\",\n    \"type\": \"character varying\"\n  },\n  {\n    \"column\": \"string_uniq\",\n    \"table\": \"playlists\",\n    \"type\": \"character varying\"\n  },\n  {\n    \"column\": \"text\",\n    \"table\": \"playlists\",\n    \"type\": \"text\"\n  },\n  {\n    \"column\": \"text_nonull\",\n    \"table\": \"playlists\",\n    \"type\": \"text\"\n  },\n  {\n    \"column\": \"text_uniq\",\n    \"table\": \"playlists\",\n    \"type\": \"text\"\n  },\n  {\n    \"column\": \"small_unsigned\",\n    \"table\": \"playlists\",\n    \"type\": \"smallint\"\n  },\n  {\n    \"column\": \"small_unsigned_nonull\",\n    \"table\": \"playlists\",\n    \"type\": \"smallint\"\n  },\n  {\n    \"column\": \"small_unsigned_uniq\",\n    \"table\": \"playlists\",\n    \"type\": \"smallint\"\n  },\n  {\n    \"column\": \"big_unsigned\",\n    \"table\": \"playlists\",\n    \"type\": \"bigint\"\n  },\n  {\n    \"column\": \"big_unsigned_nonull\",\n    \"table\": \"playlists\",\n    \"type\": \"bigint\"\n  },\n  {\n    \"column\": \"big_unsigned_uniq\",\n    \"table\": \"playlists\",\n    \"type\": \"bigint\"\n  },\n  {\n    \"column\": \"small_int\",\n    \"table\": \"playlists\",\n    \"type\": \"smallint\"\n  },\n  {\n    \"column\": \"small_int_nonull\",\n    \"table\": \"playlists\",\n    \"type\": \"smallint\"\n  },\n  {\n    \"column\": \"small_int_uniq\",\n    \"table\": \"playlists\",\n    \"type\": \"smallint\"\n  },\n  {\n    \"column\": \"int\",\n    \"table\": \"playlists\",\n    \"type\": \"integer\"\n  },\n  {\n    \"column\": \"int_nonull\",\n    \"table\": \"playlists\",\n    \"type\": \"integer\"\n  },\n  {\n    \"column\": \"int_uniq\",\n    \"table\": \"playlists\",\n    \"type\": \"integer\"\n  },\n  {\n    \"column\": \"big_int\",\n    \"table\": \"playlists\",\n    \"type\": \"bigint\"\n  },\n  {\n    \"column\": \"big_int_nonull\",\n    \"table\": \"playlists\",\n    \"type\": \"bigint\"\n  },\n  {\n    \"column\": \"big_int_uniq\",\n    \"table\": \"playlists\",\n    \"type\": \"bigint\"\n  },\n  {\n    \"column\": \"float\",\n    \"table\": \"playlists\",\n    \"type\": \"real\"\n  },\n  {\n    \"column\": \"float_nonull\",\n    \"table\": \"playlists\",\n    \"type\": \"real\"\n  },\n  {\n    \"column\": \"float_uniq\",\n    \"table\": \"playlists\",\n    \"type\": \"real\"\n  },\n  {\n    \"column\": \"double\",\n    \"table\": \"playlists\",\n    \"type\": \"double precision\"\n  },\n  {\n    \"column\": \"double_nonull\",\n    \"table\": \"playlists\",\n    \"type\": \"double precision\"\n  },\n  {\n    \"column\": \"double_uniq\",\n    \"table\": \"playlists\",\n    \"type\": \"double precision\"\n  },\n  {\n    \"column\": \"decimal\",\n    \"table\": \"playlists\",\n    \"type\": \"numeric\"\n  },\n  {\n    \"column\": \"decimal_nonull\",\n    \"table\": \"playlists\",\n    \"type\": \"numeric\"\n  },\n  {\n    \"column\": \"decimal_uniq\",\n    \"table\": \"playlists\",\n    \"type\": \"numeric\"\n  },\n  {\n    \"column\": \"bool\",\n    \"table\": \"playlists\",\n    \"type\": \"boolean\"\n  },\n  {\n    \"column\": \"bool_nonull\",\n    \"table\": \"playlists\",\n    \"type\": \"boolean\"\n  },\n  {\n    \"column\": \"tstz\",\n    \"table\": \"playlists\",\n    \"type\": \"timestamp with time zone\"\n  },\n  {\n    \"column\": \"tstz_nonull\",\n    \"table\": \"playlists\",\n    \"type\": \"timestamp with time zone\"\n  },\n  {\n    \"column\": \"date\",\n    \"table\": \"playlists\",\n    \"type\": \"date\"\n  },\n  {\n    \"column\": \"date_nonull\",\n    \"table\": \"playlists\",\n    \"type\": \"date\"\n  },\n  {\n    \"column\": \"date_uniq\",\n    \"table\": \"playlists\",\n    \"type\": \"date\"\n  },\n  {\n    \"column\": \"date_time\",\n    \"table\": \"playlists\",\n    \"type\": \"timestamp without time zone\"\n  },\n  {\n    \"column\": \"date_time_nonull\",\n    \"table\": \"playlists\",\n    \"type\": \"timestamp without time zone\"\n  },\n  {\n    \"column\": \"date_time_uniq\",\n    \"table\": \"playlists\",\n    \"type\": \"timestamp without time zone\"\n  },\n  {\n    \"column\": \"json\",\n    \"table\": \"playlists\",\n    \"type\": \"json\"\n  },\n  {\n    \"column\": \"json_nonull\",\n    \"table\": \"playlists\",\n    \"type\": \"json\"\n  },\n  {\n    \"column\": \"jsonb\",\n    \"table\": \"playlists\",\n    \"type\": \"jsonb\"\n  },\n  {\n    \"column\": \"jsonb_nonull\",\n    \"table\": \"playlists\",\n    \"type\": \"jsonb\"\n  },\n  {\n    \"column\": \"jsonb_uniq\",\n    \"table\": \"playlists\",\n    \"type\": \"jsonb\"\n  },\n  {\n    \"column\": \"blob\",\n    \"table\": \"playlists\",\n    \"type\": \"bytea\"\n  },\n  {\n    \"column\": \"blob_nonull\",\n    \"table\": \"playlists\",\n    \"type\": \"bytea\"\n  },\n  {\n    \"column\": \"blob_uniq\",\n    \"table\": \"playlists\",\n    \"type\": \"bytea\"\n  },\n  {\n    \"column\": \"money\",\n    \"table\": \"playlists\",\n    \"type\": \"money\"\n  },\n  {\n    \"column\": \"money_nonull\",\n    \"table\": \"playlists\",\n    \"type\": \"money\"\n  },\n  {\n    \"column\": \"money_uniq\",\n    \"table\": \"playlists\",\n    \"type\": \"money\"\n  },\n  {\n    \"column\": \"unsigned_nonull\",\n    \"table\": \"playlists\",\n    \"type\": \"integer\"\n  },\n  {\n    \"column\": \"unsigned\",\n    \"table\": \"playlists\",\n    \"type\": \"integer\"\n  },\n  {\n    \"column\": \"unsigned_uniq\",\n    \"table\": \"playlists\",\n    \"type\": \"integer\"\n  },\n  {\n    \"column\": \"age\",\n    \"table\": \"playlists\",\n    \"type\": \"numeric\"\n  },\n  {\n    \"column\": \"age_nonull\",\n    \"table\": \"playlists\",\n    \"type\": \"numeric\"\n  },\n  {\n    \"column\": \"array_string\",\n    \"table\": \"playlists\",\n    \"type\": \"ARRAY\"\n  },\n  {\n    \"column\": \"array_float\",\n    \"table\": \"playlists\",\n    \"type\": \"ARRAY\"\n  },\n  {\n    \"column\": \"array_int\",\n    \"table\": \"playlists\",\n    \"type\": \"ARRAY\"\n  },\n  {\n    \"column\": \"array_double\",\n    \"table\": \"playlists\",\n    \"type\": \"ARRAY\"\n  },\n  {\n    \"column\": \"array_bool\",\n    \"table\": \"playlists\",\n    \"type\": \"ARRAY\"\n  },\n  {\n    \"column\": \"version\",\n    \"table\": \"seaql_migrations\",\n    \"type\": \"character varying\"\n  },\n  {\n    \"column\": \"applied_at\",\n    \"table\": \"seaql_migrations\",\n    \"type\": \"bigint\"\n  },\n  {\n    \"column\": \"created_at\",\n    \"table\": \"users\",\n    \"type\": \"timestamp with time zone\"\n  },\n  {\n    \"column\": \"updated_at\",\n    \"table\": \"users\",\n    \"type\": \"timestamp with time zone\"\n  },\n  {\n    \"column\": \"id\",\n    \"table\": \"users\",\n    \"type\": \"integer\"\n  },\n  {\n    \"column\": \"pid\",\n    \"table\": \"users\",\n    \"type\": \"uuid\"\n  },\n  {\n    \"column\": \"email\",\n    \"table\": \"users\",\n    \"type\": \"character varying\"\n  },\n  {\n    \"column\": \"password\",\n    \"table\": \"users\",\n    \"type\": \"character varying\"\n  },\n  {\n    \"column\": \"api_key\",\n    \"table\": \"users\",\n    \"type\": \"character varying\"\n  },\n  {\n    \"column\": \"name\",\n    \"table\": \"users\",\n    \"type\": \"character varying\"\n  },\n  {\n    \"column\": \"reset_token\",\n    \"table\": \"users\",\n    \"type\": \"character varying\"\n  },\n  {\n    \"column\": \"reset_sent_at\",\n    \"table\": \"users\",\n    \"type\": \"timestamp with time zone\"\n  },\n  {\n    \"column\": \"email_verification_token\",\n    \"table\": \"users\",\n    \"type\": \"character varying\"\n  },\n  {\n    \"column\": \"email_verification_sent_at\",\n    \"table\": \"users\",\n    \"type\": \"timestamp with time zone\"\n  },\n  {\n    \"column\": \"email_verified_at\",\n    \"table\": \"users\",\n    \"type\": \"timestamp with time zone\"\n  },\n  {\n    \"column\": \"magic_link_token\",\n    \"table\": \"users\",\n    \"type\": \"character varying\"\n  },\n  {\n    \"column\": \"magic_link_expiration\",\n    \"table\": \"users\",\n    \"type\": \"timestamp with time zone\"\n  }\n]\n"
  },
  {
    "path": "loco-gen/tests/snapshots/r#mod__db__migrations_flow_sqlite.snap",
    "content": "---\nsource: loco-gen/tests/db.rs\nexpression: \"read_to_string(tree_fs.root.join(\\\"myapp\\\").join(\\\"schema_dump.json\\\")).unwrap()\"\n---\n[\n  {\n    \"sql\": \"CREATE TABLE \\\"actor_movies\\\" ( \\\"created_at\\\" timestamp_with_timezone_text NOT NULL DEFAULT CURRENT_TIMESTAMP, \\\"updated_at\\\" timestamp_with_timezone_text NOT NULL DEFAULT CURRENT_TIMESTAMP, \\\"actor_id\\\" integer NOT NULL, \\\"movie_id\\\" integer NOT NULL, CONSTRAINT \\\"idx-actor_movies-refs-pk\\\" PRIMARY KEY (\\\"actor_id\\\", \\\"movie_id\\\"), FOREIGN KEY (\\\"actor_id\\\") REFERENCES \\\"actors\\\" (\\\"id\\\") ON DELETE CASCADE ON UPDATE CASCADE, FOREIGN KEY (\\\"movie_id\\\") REFERENCES \\\"movies\\\" (\\\"id\\\") ON DELETE CASCADE ON UPDATE CASCADE )\",\n    \"table\": \"actor_movies\"\n  },\n  {\n    \"sql\": \"CREATE TABLE \\\"actors\\\" ( \\\"created_at\\\" timestamp_with_timezone_text NOT NULL DEFAULT CURRENT_TIMESTAMP, \\\"updated_at\\\" timestamp_with_timezone_text NOT NULL DEFAULT CURRENT_TIMESTAMP, \\\"id\\\" integer NOT NULL PRIMARY KEY AUTOINCREMENT, \\\"foobar\\\" varchar NULL )\",\n    \"table\": \"actors\"\n  },\n  {\n    \"sql\": \"CREATE TABLE \\\"awards\\\" ( \\\"created_at\\\" timestamp_with_timezone_text NOT NULL DEFAULT CURRENT_TIMESTAMP, \\\"updated_at\\\" timestamp_with_timezone_text NOT NULL DEFAULT CURRENT_TIMESTAMP, \\\"id\\\" integer NOT NULL PRIMARY KEY AUTOINCREMENT, \\\"name\\\" varchar NULL, \\\"actor_id\\\" integer NOT NULL, FOREIGN KEY (\\\"actor_id\\\") REFERENCES \\\"actors\\\" (\\\"id\\\") ON DELETE CASCADE ON UPDATE CASCADE )\",\n    \"table\": \"awards\"\n  },\n  {\n    \"sql\": \"CREATE TABLE \\\"movies\\\" ( \\\"created_at\\\" timestamp_with_timezone_text NOT NULL DEFAULT CURRENT_TIMESTAMP, \\\"updated_at\\\" timestamp_with_timezone_text NOT NULL DEFAULT CURRENT_TIMESTAMP, \\\"id\\\" integer NOT NULL PRIMARY KEY AUTOINCREMENT, \\\"uuid_uniq\\\" uuid_text NOT NULL UNIQUE, \\\"uuid\\\" uuid_text NULL, \\\"uuid_nonull\\\" uuid_text NOT NULL, \\\"string\\\" varchar NULL, \\\"string_nonull\\\" varchar NOT NULL, \\\"string_uniq\\\" varchar NOT NULL UNIQUE, \\\"text\\\" text NULL, \\\"text_nonull\\\" text NOT NULL, \\\"text_uniq\\\" text NOT NULL UNIQUE, \\\"small_unsigned\\\" smallint NULL, \\\"small_unsigned_nonull\\\" smallint NOT NULL, \\\"small_unsigned_uniq\\\" smallint NOT NULL UNIQUE, \\\"big_unsigned\\\" bigint NULL, \\\"big_unsigned_nonull\\\" bigint NOT NULL, \\\"big_unsigned_uniq\\\" bigint NOT NULL UNIQUE, \\\"small_int\\\" smallint NULL, \\\"small_int_nonull\\\" smallint NOT NULL, \\\"small_int_uniq\\\" smallint NOT NULL UNIQUE, \\\"int\\\" integer NULL, \\\"int_nonull\\\" integer NOT NULL, \\\"int_uniq\\\" integer NOT NULL UNIQUE, \\\"big_int\\\" bigint NULL, \\\"big_int_nonull\\\" bigint NOT NULL, \\\"big_int_uniq\\\" bigint NOT NULL UNIQUE, \\\"float\\\" float NULL, \\\"float_nonull\\\" float NOT NULL, \\\"float_uniq\\\" float NOT NULL UNIQUE, \\\"double\\\" double NULL, \\\"double_nonull\\\" double NOT NULL, \\\"double_uniq\\\" double NOT NULL UNIQUE, \\\"decimal\\\" real NULL, \\\"decimal_nonull\\\" real NOT NULL, \\\"decimal_uniq\\\" real NOT NULL UNIQUE, \\\"bool\\\" boolean NULL, \\\"bool_nonull\\\" boolean NOT NULL, \\\"tstz\\\" timestamp_with_timezone_text, \\\"tstz_nonull\\\" timestamp_with_timezone_text NOT NULL, \\\"date\\\" date_text NULL, \\\"date_nonull\\\" date_text NOT NULL, \\\"date_uniq\\\" date_text NOT NULL UNIQUE, \\\"date_time\\\" datetime_text NULL, \\\"date_time_nonull\\\" datetime_text NOT NULL, \\\"date_time_uniq\\\" datetime_text NOT NULL UNIQUE, \\\"json\\\" json_text NULL, \\\"json_nonull\\\" json_text NOT NULL, \\\"jsonb\\\" jsonb_text NULL, \\\"jsonb_nonull\\\" jsonb_text NOT NULL, \\\"jsonb_uniq\\\" jsonb_text NOT NULL UNIQUE, \\\"blob\\\" blob NULL, \\\"blob_nonull\\\" blob NOT NULL, \\\"blob_uniq\\\" blob NOT NULL UNIQUE, \\\"money\\\" real_money NULL, \\\"money_nonull\\\" real_money NOT NULL, \\\"money_uniq\\\" real_money NOT NULL UNIQUE, \\\"unsigned_nonull\\\" integer NOT NULL, \\\"unsigned\\\" integer NULL, \\\"unsigned_uniq\\\" integer NOT NULL UNIQUE, \\\"age\\\" real(8, 24) NULL, \\\"age_nonull\\\" real(8, 24) NOT NULL, \\\"playlist_id\\\" integer NOT NULL, \\\"rating\\\" integer NULL, FOREIGN KEY (\\\"playlist_id\\\") REFERENCES \\\"playlists\\\" (\\\"id\\\") ON DELETE CASCADE ON UPDATE CASCADE )\",\n    \"table\": \"movies\"\n  },\n  {\n    \"sql\": \"CREATE TABLE \\\"playlists\\\" ( \\\"created_at\\\" timestamp_with_timezone_text NOT NULL DEFAULT CURRENT_TIMESTAMP, \\\"updated_at\\\" timestamp_with_timezone_text NOT NULL DEFAULT CURRENT_TIMESTAMP, \\\"id\\\" integer NOT NULL PRIMARY KEY AUTOINCREMENT, \\\"uuid_uniq\\\" uuid_text NOT NULL UNIQUE, \\\"uuid\\\" uuid_text NULL, \\\"uuid_nonull\\\" uuid_text NOT NULL, \\\"string\\\" varchar NULL, \\\"string_nonull\\\" varchar NOT NULL, \\\"string_uniq\\\" varchar NOT NULL UNIQUE, \\\"text\\\" text NULL, \\\"text_nonull\\\" text NOT NULL, \\\"text_uniq\\\" text NOT NULL UNIQUE, \\\"small_unsigned\\\" smallint NULL, \\\"small_unsigned_nonull\\\" smallint NOT NULL, \\\"small_unsigned_uniq\\\" smallint NOT NULL UNIQUE, \\\"big_unsigned\\\" bigint NULL, \\\"big_unsigned_nonull\\\" bigint NOT NULL, \\\"big_unsigned_uniq\\\" bigint NOT NULL UNIQUE, \\\"small_int\\\" smallint NULL, \\\"small_int_nonull\\\" smallint NOT NULL, \\\"small_int_uniq\\\" smallint NOT NULL UNIQUE, \\\"int\\\" integer NULL, \\\"int_nonull\\\" integer NOT NULL, \\\"int_uniq\\\" integer NOT NULL UNIQUE, \\\"big_int\\\" bigint NULL, \\\"big_int_nonull\\\" bigint NOT NULL, \\\"big_int_uniq\\\" bigint NOT NULL UNIQUE, \\\"float\\\" float NULL, \\\"float_nonull\\\" float NOT NULL, \\\"float_uniq\\\" float NOT NULL UNIQUE, \\\"double\\\" double NULL, \\\"double_nonull\\\" double NOT NULL, \\\"double_uniq\\\" double NOT NULL UNIQUE, \\\"decimal\\\" real NULL, \\\"decimal_nonull\\\" real NOT NULL, \\\"decimal_uniq\\\" real NOT NULL UNIQUE, \\\"bool\\\" boolean NULL, \\\"bool_nonull\\\" boolean NOT NULL, \\\"tstz\\\" timestamp_with_timezone_text, \\\"tstz_nonull\\\" timestamp_with_timezone_text NOT NULL, \\\"date\\\" date_text NULL, \\\"date_nonull\\\" date_text NOT NULL, \\\"date_uniq\\\" date_text NOT NULL UNIQUE, \\\"date_time\\\" datetime_text NULL, \\\"date_time_nonull\\\" datetime_text NOT NULL, \\\"date_time_uniq\\\" datetime_text NOT NULL UNIQUE, \\\"json\\\" json_text NULL, \\\"json_nonull\\\" json_text NOT NULL, \\\"jsonb\\\" jsonb_text NULL, \\\"jsonb_nonull\\\" jsonb_text NOT NULL, \\\"jsonb_uniq\\\" jsonb_text NOT NULL UNIQUE, \\\"blob\\\" blob NULL, \\\"blob_nonull\\\" blob NOT NULL, \\\"blob_uniq\\\" blob NOT NULL UNIQUE, \\\"money\\\" real_money NULL, \\\"money_nonull\\\" real_money NOT NULL, \\\"money_uniq\\\" real_money NOT NULL UNIQUE, \\\"unsigned_nonull\\\" integer NOT NULL, \\\"unsigned\\\" integer NULL, \\\"unsigned_uniq\\\" integer NOT NULL UNIQUE, \\\"age\\\" real(8, 24) NULL, \\\"age_nonull\\\" real(8, 24) NOT NULL )\",\n    \"table\": \"playlists\"\n  },\n  {\n    \"sql\": \"CREATE TABLE \\\"seaql_migrations\\\" ( \\\"version\\\" varchar NOT NULL PRIMARY KEY, \\\"applied_at\\\" bigint NOT NULL )\",\n    \"table\": \"seaql_migrations\"\n  },\n  {\n    \"sql\": \"CREATE TABLE \\\"users\\\" ( \\\"created_at\\\" timestamp_with_timezone_text NOT NULL DEFAULT CURRENT_TIMESTAMP, \\\"updated_at\\\" timestamp_with_timezone_text NOT NULL DEFAULT CURRENT_TIMESTAMP, \\\"id\\\" integer NOT NULL PRIMARY KEY AUTOINCREMENT, \\\"pid\\\" uuid_text NOT NULL, \\\"email\\\" varchar NOT NULL UNIQUE, \\\"password\\\" varchar NOT NULL, \\\"api_key\\\" varchar NOT NULL UNIQUE, \\\"name\\\" varchar NOT NULL, \\\"reset_token\\\" varchar NULL, \\\"reset_sent_at\\\" timestamp_with_timezone_text NULL, \\\"email_verification_token\\\" varchar NULL, \\\"email_verification_sent_at\\\" timestamp_with_timezone_text NULL, \\\"email_verified_at\\\" timestamp_with_timezone_text NULL, \\\"magic_link_token\\\" varchar NULL, \\\"magic_link_expiration\\\" timestamp_with_timezone_text NULL )\",\n    \"table\": \"users\"\n  }\n]\n"
  },
  {
    "path": "loco-gen/tests/templates/controller.rs",
    "content": "use super::utils::APP_ROUTS;\nuse insta::assert_snapshot;\nuse loco_gen::{collect_messages, generate, AppInfo, Component, ScaffoldKind};\nuse rrgen::RRgen;\nuse rstest::rstest;\nuse std::fs;\n\n#[rstest]\n#[case(ScaffoldKind::Api)]\n#[case(ScaffoldKind::Html)]\n#[case(ScaffoldKind::Htmx)]\n#[test]\nfn can_generate(#[case] kind: ScaffoldKind) {\n    let actions = vec![\"GET\".to_string(), \"POST\".to_string()];\n    let component = Component::Controller {\n        name: \"movie\".to_string(),\n        actions: actions.clone(),\n        kind: kind.clone(),\n    };\n\n    let mut settings = insta::Settings::clone_current();\n    settings.set_prepend_module_to_snapshot(false);\n    settings.set_snapshot_suffix(format!(\"{kind:?}_controller\"));\n    let _guard = settings.bind_to_scope();\n\n    let tree_fs = tree_fs::TreeBuilder::default()\n        .drop(true)\n        .add_empty(\"src/controllers/mod.rs\")\n        .add_empty(\"tests/requests/mod.rs\")\n        .add(\"src/app.rs\", APP_ROUTS)\n        .create()\n        .unwrap();\n\n    let rrgen = RRgen::with_working_dir(&tree_fs.root);\n\n    let gen_result = generate(\n        &rrgen,\n        component,\n        &AppInfo {\n            app_name: \"tester\".to_string(),\n        },\n    )\n    .expect(\"Generation failed\");\n\n    assert_snapshot!(\"generate_results\", collect_messages(&gen_result));\n\n    let controllers_path = tree_fs.root.join(\"src\").join(\"controllers\");\n    assert_snapshot!(\n        \"generate[controller_file]\",\n        fs::read_to_string(controllers_path.join(\"movie.rs\")).expect(\"controller file missing\")\n    );\n    assert_snapshot!(\n        \"inject[controller_mod_rs]\",\n        fs::read_to_string(controllers_path.join(\"mod.rs\")).expect(\"mod.rs injection failed\")\n    );\n    assert_snapshot!(\n        \"inject[app_rs]\",\n        fs::read_to_string(tree_fs.root.join(\"src\").join(\"app.rs\"))\n            .expect(\"app.rs injection failed\")\n    );\n\n    if matches!(kind, ScaffoldKind::Api) {\n        let test_controllers_path = tree_fs.root.join(\"tests\").join(\"requests\");\n        assert_snapshot!(\n            \"generate[tests_controller_mod_rs]\",\n            fs::read_to_string(test_controllers_path.join(\"movie.rs\")).expect(\"test file missing\")\n        );\n        assert_snapshot!(\n            \"inject[tests_controller_mod_rs]\",\n            fs::read_to_string(test_controllers_path.join(\"mod.rs\")).expect(\"test mod.rs missing\")\n        );\n    } else {\n        for action in actions {\n            assert_snapshot!(\n                format!(\"inject[views_[{action}]]\"),\n                fs::read_to_string(\n                    tree_fs\n                        .root\n                        .join(\"assets\")\n                        .join(\"views\")\n                        .join(\"movie\")\n                        .join(format!(\"{}.html\", action.to_uppercase()))\n                )\n                .expect(\"view file missing\")\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "loco-gen/tests/templates/deployment.rs",
    "content": "use insta::assert_snapshot;\nuse loco_gen::{collect_messages, generate, AppInfo, Component, DeploymentKind};\nuse rrgen::RRgen;\nuse std::{fs, path::PathBuf};\n\n#[rstest::rstest]\nfn can_generate_docker(\n    #[values(vec![], vec![std::path::PathBuf::from(\"404.html\"), PathBuf::from(\"asset\")])]\n    copy_paths: Vec<PathBuf>,\n    #[values(true, false)] is_client_side_rendering: bool,\n) {\n    let mut settings = insta::Settings::clone_current();\n    settings.set_prepend_module_to_snapshot(false);\n    settings.set_snapshot_suffix(\"deployment\");\n    let _guard = settings.bind_to_scope();\n\n    let component = Component::Deployment {\n        kind: DeploymentKind::Docker {\n            copy_paths: copy_paths.clone(),\n            is_client_side_rendering,\n        },\n    };\n\n    let tree_fs = tree_fs::TreeBuilder::default().drop(true).create().unwrap();\n    let rrgen = RRgen::with_working_dir(&tree_fs.root);\n\n    let gen_result = generate(\n        &rrgen,\n        component,\n        &AppInfo {\n            app_name: \"tester\".to_string(),\n        },\n    )\n    .expect(\"Generation failed\");\n\n    assert_eq!(\n        collect_messages(&gen_result),\n        r\"* Dockerfile generated successfully.\n* Dockerignore generated successfully.\n\"\n    );\n    insta::with_settings!({\n        filters => vec![\n            (r\"FROM rust:\\d+\\.\\d+\\.\\d+-slim\", \"FROM rust:[version]-slim\"),\n        ]\n    }, {\n        assert_snapshot!(\n            format!(\n                \"generate[docker_file_[{}]_[{}]]\",\n                copy_paths.len(),\n                is_client_side_rendering\n            ),\n            fs::read_to_string(tree_fs.root.join(\"Dockerfile\")).expect(\"Dockerfile missing\")\n        );\n    });\n\n    assert_eq!(\n        fs::read_to_string(tree_fs.root.join(\".dockerignore\")).expect(\".dockerignore missing\"),\n        r\"target\nDockerfile\n.dockerignore\n.git\n.gitignore\n\"\n    );\n}\n\n#[test]\nfn can_generate_nginx() {\n    let mut settings = insta::Settings::clone_current();\n    settings.set_prepend_module_to_snapshot(false);\n    settings.set_snapshot_suffix(\"deployment\");\n    let _guard = settings.bind_to_scope();\n\n    let component = Component::Deployment {\n        kind: DeploymentKind::Nginx {\n            host: \"localhost\".to_string(),\n            port: 8080,\n        },\n    };\n\n    let tree_fs = tree_fs::TreeBuilder::default().drop(true).create().unwrap();\n    let rrgen = RRgen::with_working_dir(&tree_fs.root);\n\n    let gen_result = generate(\n        &rrgen,\n        component,\n        &AppInfo {\n            app_name: \"tester\".to_string(),\n        },\n    )\n    .expect(\"Generation failed\");\n\n    assert_eq!(\n        collect_messages(&gen_result),\n        r\"* Nginx generated successfully.\n\"\n    );\n    assert_snapshot!(\n        \"generate[nginx]\",\n        fs::read_to_string(tree_fs.root.join(\"nginx\").join(\"default.conf\"))\n            .expect(\"nginx config missing\")\n    );\n}\n"
  },
  {
    "path": "loco-gen/tests/templates/mailer.rs",
    "content": "use insta::assert_snapshot;\nuse loco_gen::{collect_messages, generate, AppInfo, Component};\nuse rrgen::RRgen;\nuse std::fs;\n\n#[test]\nfn can_generate() {\n    let mut settings = insta::Settings::clone_current();\n    settings.set_prepend_module_to_snapshot(false);\n    settings.set_snapshot_suffix(\"mailer\");\n    let _guard = settings.bind_to_scope();\n\n    let component = Component::Mailer {\n        name: \"reset_password\".to_string(),\n    };\n\n    let tree_fs = tree_fs::TreeBuilder::default()\n        .drop(true)\n        .add_empty(\"src/mailers/mod.rs\")\n        .create()\n        .unwrap();\n\n    let rrgen = RRgen::with_working_dir(&tree_fs.root);\n\n    let gen_result = generate(\n        &rrgen,\n        component,\n        &AppInfo {\n            app_name: \"tester\".to_string(),\n        },\n    )\n    .expect(\"Generation failed\");\n\n    assert_eq!(\n        collect_messages(&gen_result),\n        r\"* A mailer `ResetPassword` was added successfully.\n\"\n    );\n\n    let mailer_path = tree_fs.root.join(\"src\").join(\"mailers\");\n\n    for (name, path) in [\n        (\n            \"generate[mailer_mod_rs]\",\n            mailer_path.join(\"reset_password.rs\"),\n        ),\n        (\"inject[mailer_mod_rs]\", mailer_path.join(\"mod.rs\")),\n        (\n            \"generate[subject_t_file]\",\n            mailer_path\n                .join(\"reset_password\")\n                .join(\"welcome\")\n                .join(\"subject.t\"),\n        ),\n        (\n            \"generate[text_t_file]\",\n            mailer_path\n                .join(\"reset_password\")\n                .join(\"welcome\")\n                .join(\"text.t\"),\n        ),\n        (\n            \"generate[html_t_file]\",\n            mailer_path\n                .join(\"reset_password\")\n                .join(\"welcome\")\n                .join(\"html.t\"),\n        ),\n    ] {\n        assert_snapshot!(\n            name,\n            fs::read_to_string(path).unwrap_or_else(|_| panic!(\"{name} missing\"))\n        );\n    }\n}\n"
  },
  {
    "path": "loco-gen/tests/templates/migration.rs",
    "content": "use super::utils::{guess_file_by_time, MIGRATION_SRC_LIB};\nuse insta::{assert_snapshot, with_settings};\nuse loco_gen::{collect_messages, generate, AppInfo, Component};\nuse rrgen::RRgen;\nuse rstest::rstest;\nuse std::fs;\n\n#[rstest]\n#[case(\"create_table\", Component::Migration {\n        name: \"CreateMovies\".to_string(),\n        with_tz: true,\n        fields: vec![\n            (\"title\".to_string(), \"string\".to_string()),\n            (\"user\".to_string(), \"references\".to_string()),\n        ],\n    }, \"movies.rs\")]\n#[case(\"create_table_without_tz\", Component::Migration {\n        name: \"CreateMovies\".to_string(),\n        with_tz: false,\n        fields: vec![\n            (\"title\".to_string(), \"string\".to_string()),\n            (\"user\".to_string(), \"references\".to_string()),\n        ],\n    }, \"movies.rs\")]\n#[case(\"add_column\", Component::Migration {\n        name: \"AddNameAndAgeToUsers\".to_string(),\n        with_tz: true,\n        fields: vec![\n            (\"name\".to_string(), \"string\".to_string()),\n            (\"age\".to_string(), \"int\".to_string()),\n        ],\n    }, \"add_name_and_age_to_users.rs\")]\n#[case(\"remove_columns\", Component::Migration {\n        name: \"RemoveNameAndAgeFromUsers\".to_string(),\n        with_tz: true,\n        fields: vec![\n            (\"name\".to_string(), \"string\".to_string()),\n            (\"age\".to_string(), \"int\".to_string()),\n        ],\n    }, \"remove_name_and_age_from_users.rs\")]\n#[case(\"add_reference\", Component::Migration {\n        name: \"AddUserRefToPosts\".to_string(),\n        with_tz: true,\n        fields: vec![\n            (\"user\".to_string(), \"references\".to_string()),\n        ],\n    }, \"add_user_ref_to_posts.rs\")]\n#[case(\"create_join_table_without_tz\", Component::Migration {\n        name: \"CreateJoinTableUsersAndGroups\".to_string(),\n        with_tz: false,\n        fields: vec![\n            (\"count\".to_string(), \"int\".to_string()),\n        ],\n    }, \"create_join_table_users_and_groups.rs\")]\n#[case(\"create_join_table\", Component::Migration {\n        name: \"CreateJoinTableUsersAndGroups\".to_string(),\n        with_tz: true,\n        fields: vec![\n            (\"count\".to_string(), \"int\".to_string()),\n        ],\n    }, \"create_join_table_users_and_groups.rs\")]\n#[case(\"empty\", Component::Migration {\n        name: \"FixUsersTable\".to_string(),\n        with_tz: true,\n        fields: vec![\n            (\"count\".to_string(), \"int\".to_string()),\n        ],\n    }, \"fix_users_table.rs\")]\n#[test]\nfn can_generate(\n    #[case] test_name: &str,\n    #[case] component: Component,\n    #[case] suffix_generate_file: &str,\n) {\n    let mut settings = insta::Settings::clone_current();\n    settings.set_prepend_module_to_snapshot(false);\n    settings.set_snapshot_suffix(format!(\"{test_name}_migration\"));\n    let _guard = settings.bind_to_scope();\n\n    let tree_fs = tree_fs::TreeBuilder::default()\n        .drop(true)\n        .add(\"migration/src/lib.rs\", MIGRATION_SRC_LIB)\n        .create()\n        .unwrap();\n\n    let rrgen = RRgen::with_working_dir(&tree_fs.root);\n\n    let gen_result = generate(\n        &rrgen,\n        component,\n        &AppInfo {\n            app_name: \"tester\".to_string(),\n        },\n    )\n    .expect(\"Generation failed\");\n\n    assert_snapshot!(\"generate_result\", collect_messages(&gen_result));\n\n    let migration_path = tree_fs.root.join(\"migration\").join(\"src\");\n    let migration_file = guess_file_by_time(\n        &migration_path,\n        &format!(\"m{{TIME}}_{suffix_generate_file}\"),\n        3,\n    )\n    .expect(\"Failed to find the generated migration file\");\n\n    assert_snapshot!(\n        \"generate[migration_file]\",\n        fs::read_to_string(&migration_file).expect(\"Failed to read the migration file\")\n    );\n\n    with_settings!({\n        filters => vec![(r\"\\d{8}_\\d{6}\", \"[TIME]\")]\n    }, {\n        assert_snapshot!(\n            \"inject[migration_lib]\",\n            fs::read_to_string(migration_path.join(\"lib.rs\")).expect(\"Failed to read lib.rs\")\n        );\n    });\n}\n\n#[rstest]\n#[case(Component::Migration {\n        name: \"CreateMovies\".to_string(),\n        with_tz: true,\n        fields: vec![\n            (\"title\".to_string(), \"string\".to_string()),\n            (\"user\".to_string(), \"references\".to_string()),\n        ],\n    })]\n#[case(Component::Migration {\n        name: \"AddNameAndAgeToUsers\".to_string(),\n        with_tz: true,\n        fields: vec![\n            (\"name\".to_string(), \"string\".to_string()),\n            (\"age\".to_string(), \"int\".to_string()),\n        ],\n    })]\n#[case(Component::Migration {\n        name: \"RemoveNameAndAgeFromUsers\".to_string(),\n        with_tz: true,\n        fields: vec![\n            (\"name\".to_string(), \"string\".to_string()),\n            (\"age\".to_string(), \"int\".to_string()),\n        ],\n    })]\n#[case(Component::Migration {\n        name: \"AddUserRefToPosts\".to_string(),\n        with_tz: true,\n        fields: vec![\n            (\"user\".to_string(), \"references\".to_string()),\n        ],\n    })]\n#[case(Component::Migration {\n        name: \"CreateJoinTableUsersAndGroups\".to_string(),\n        with_tz: true,\n        fields: vec![\n            (\"count\".to_string(), \"int\".to_string()),\n        ],\n    })]\n#[case(Component::Migration {\n        name: \"FixUsersTable\".to_string(),\n        with_tz: true,\n        fields: vec![\n            (\"count\".to_string(), \"int\".to_string()),\n        ],\n    })]\n#[test]\nfn fail_when_migration_lib_not_exists(#[case] component: Component) {\n    let tree_fs = tree_fs::TreeBuilder::default()\n        .drop(true)\n        .add_empty(\"tests/models/mod.rs\")\n        .create()\n        .unwrap();\n\n    let rrgen = RRgen::with_working_dir(&tree_fs.root);\n\n    let err = generate(\n        &rrgen,\n        component,\n        &AppInfo {\n            app_name: \"tester\".to_string(),\n        },\n    )\n    .expect_err(\"Expected error when migration lib doesn't exist\");\n\n    assert_eq!(\n        err.to_string(),\n        \"cannot inject into migration/src/lib.rs: file does not exist\"\n    );\n}\n"
  },
  {
    "path": "loco-gen/tests/templates/mod.rs",
    "content": "mod controller;\nmod deployment;\nmod mailer;\n#[cfg(feature = \"with-db\")]\nmod migration;\n#[cfg(feature = \"with-db\")]\nmod model;\n#[cfg(feature = \"with-db\")]\nmod scaffold;\nmod scheduler;\nmod task;\nmod utils;\nmod worker;\n"
  },
  {
    "path": "loco-gen/tests/templates/model.rs",
    "content": "use super::utils::{guess_file_by_time, MIGRATION_SRC_LIB};\nuse insta::{assert_snapshot, with_settings};\nuse loco_gen::{collect_messages, generate, AppInfo, Component};\nuse rrgen::RRgen;\nuse std::fs;\n\nmacro_rules! configure_insta {\n    () => {\n        let mut settings = insta::Settings::clone_current();\n        settings.set_prepend_module_to_snapshot(false);\n        settings.set_snapshot_suffix(\"model\");\n        let _guard = settings.bind_to_scope();\n    };\n}\n\n#[test]\nfn can_generate() {\n    std::env::set_var(\"SKIP_MIGRATION\", \"\");\n    configure_insta!();\n    let tree_fs = tree_fs::TreeBuilder::default()\n        .drop(true)\n        .add(\"migration/src/lib.rs\", MIGRATION_SRC_LIB)\n        .add_empty(\"tests/models/mod.rs\")\n        .create()\n        .unwrap();\n\n    let rrgen = RRgen::with_working_dir(&tree_fs.root);\n    let component = Component::Model {\n        name: \"movies\".to_string(),\n        with_tz: true,\n        fields: vec![(\"title\".to_string(), \"string\".to_string())],\n    };\n\n    let gen_result = generate(\n        &rrgen,\n        component,\n        &AppInfo {\n            app_name: \"tester\".to_string(),\n        },\n    )\n    .expect(\"Generation failed\");\n\n    assert_eq!(\n        collect_messages(&gen_result),\n        r\"* Migration for `movies` added! You can now apply it with `$ cargo loco db migrate && cargo loco db entities`.\n* A test for model `Movies` was added. Run with `cargo test`.\n\"\n    );\n\n    let migration_path = tree_fs.root.join(\"migration/src\");\n    let migration_file = guess_file_by_time(&migration_path, \"m{TIME}_movies.rs\", 3)\n        .expect(\"Failed to find the generated migration file\");\n\n    assert_snapshot!(\n        \"generate[migration_file]\",\n        fs::read_to_string(&migration_file).expect(\"Failed to read the migration file\")\n    );\n\n    with_settings!({\n        filters => vec![(r\"\\d{8}_\\d{6}\", \"[TIME]\")]\n    }, {\n        assert_snapshot!(\n            \"inject[migration_lib]\",\n            fs::read_to_string(migration_path.join(\"lib.rs\")).expect(\"Failed to read lib.rs\")\n        );\n    });\n\n    let tests_path = tree_fs.root.join(\"tests/models\");\n    assert_snapshot!(\n        \"generate[test_model]\",\n        fs::read_to_string(tests_path.join(\"movies.rs\")).expect(\"Failed to read movies.rs\")\n    );\n    assert_snapshot!(\n        \"inject[test_mod]\",\n        fs::read_to_string(tests_path.join(\"mod.rs\")).expect(\"Failed to read mod.rs\")\n    );\n}\n\n#[test]\nfn fail_when_migration_lib_not_exists() {\n    std::env::set_var(\"SKIP_MIGRATION\", \"\");\n    let tree_fs = tree_fs::TreeBuilder::default()\n        .drop(true)\n        .add_empty(\"tests/models/mod.rs\")\n        .create()\n        .unwrap();\n\n    let rrgen = RRgen::with_working_dir(&tree_fs.root);\n    let component = Component::Model {\n        name: \"movies\".to_string(),\n        with_tz: true,\n        fields: vec![(\"title\".to_string(), \"string\".to_string())],\n    };\n\n    let err = generate(\n        &rrgen,\n        component,\n        &AppInfo {\n            app_name: \"tester\".to_string(),\n        },\n    )\n    .expect_err(\"Expected error when model lib doesn't exist\");\n\n    assert_eq!(\n        err.to_string(),\n        \"cannot inject into migration/src/lib.rs: file does not exist\"\n    );\n}\n\n#[test]\nfn fail_when_test_models_mod_not_exists() {\n    std::env::set_var(\"SKIP_MIGRATION\", \"\");\n    let tree_fs = tree_fs::TreeBuilder::default()\n        .drop(true)\n        .add(\"migration/src/lib.rs\", MIGRATION_SRC_LIB)\n        .create()\n        .unwrap();\n\n    let rrgen = RRgen::with_working_dir(&tree_fs.root);\n    let component = Component::Model {\n        name: \"movies\".to_string(),\n        with_tz: true,\n        fields: vec![(\"title\".to_string(), \"string\".to_string())],\n    };\n\n    let err = generate(\n        &rrgen,\n        component,\n        &AppInfo {\n            app_name: \"tester\".to_string(),\n        },\n    )\n    .expect_err(\"Expected error when migration src doesn't exist\");\n\n    assert_eq!(\n        err.to_string(),\n        \"cannot inject into tests/models/mod.rs: file does not exist\"\n    );\n}\n"
  },
  {
    "path": "loco-gen/tests/templates/scaffold.rs",
    "content": "use super::utils::{guess_file_by_time, APP_ROUTS, MIGRATION_SRC_LIB};\nuse insta::{assert_snapshot, with_settings};\nuse loco_gen::{collect_messages, generate, tera_ext, AppInfo, Component, ScaffoldKind};\nuse rrgen::RRgen;\nuse rstest::rstest;\nuse std::fs;\n\n#[rstest]\n#[case(ScaffoldKind::Api)]\n#[case(ScaffoldKind::Html)]\n#[case(ScaffoldKind::Htmx)]\n#[test]\nfn can_generate(#[case] kind: ScaffoldKind) {\n    std::env::set_var(\"SKIP_MIGRATION\", \"\");\n    let mut settings = insta::Settings::clone_current();\n    settings.set_prepend_module_to_snapshot(false);\n    settings.set_snapshot_suffix(format!(\"{kind:?}_scaffold\"));\n    let _guard = settings.bind_to_scope();\n\n    let component = Component::Scaffold {\n        name: \"movie\".to_string(),\n        with_tz: true,\n        fields: vec![\n            (\"title\".to_string(), \"string\".to_string()),\n            (\"user\".to_string(), \"references\".to_string()),\n        ],\n        kind: kind.clone(),\n    };\n\n    let tree_fs = tree_fs::TreeBuilder::default()\n        .drop(true)\n        .add_empty(\"src/controllers/mod.rs\")\n        .add_empty(\"tests/models/mod.rs\")\n        .add_empty(\"src/views/mod.rs\")\n        .add_empty(\"tests/requests/mod.rs\")\n        .add(\"migration/src/lib.rs\", MIGRATION_SRC_LIB)\n        .add(\"src/app.rs\", APP_ROUTS)\n        .create()\n        .unwrap();\n\n    let rrgen = RRgen::with_working_dir(&tree_fs.root).add_template_engine(tera_ext::new());\n\n    let gen_result = generate(\n        &rrgen,\n        component,\n        &AppInfo {\n            app_name: \"tester\".to_string(),\n        },\n    )\n    .expect(\"Generation failed\");\n\n    assert_snapshot!(\"generate_results\", collect_messages(&gen_result));\n\n    // MODELS\n    let migration_path = tree_fs.root.join(\"migration/src\");\n    let migration_file = guess_file_by_time(&migration_path, \"m{TIME}_movies.rs\", 3)\n        .expect(\"Failed to find the generated migration file\");\n\n    assert_snapshot!(\n        \"generate[migration_file]\",\n        fs::read_to_string(&migration_file).expect(\"Failed to read the migration file\")\n    );\n\n    with_settings!({\n        filters => vec![(r\"\\d{8}_\\d{6}\", \"[TIME]\")]\n    }, {\n        assert_snapshot!(\n            \"inject[migration_lib]\",\n            fs::read_to_string(migration_path.join(\"lib.rs\")).expect(\"Failed to read lib.rs\")\n        );\n    });\n    with_settings!({\n        filters => vec![(r\"\\d{8}_\\d{6}\", \"[TIME]\")]\n    }, {\n        assert_snapshot!(\n            \"inject[migration_lib]\",\n            fs::read_to_string(migration_path.join(\"lib.rs\")).expect(\"Failed to read lib.rs\")\n        );\n    });\n\n    // CONTROLLER\n    let controllers_path = tree_fs.root.join(\"src\").join(\"controllers\");\n    assert_snapshot!(\n        \"generate[controller_file]\",\n        fs::read_to_string(controllers_path.join(\"movie.rs\")).expect(\"controller file missing\")\n    );\n\n    assert_snapshot!(\n        \"inject[controller_mod_rs]\",\n        fs::read_to_string(controllers_path.join(\"mod.rs\")).expect(\"mod.rs injection failed\")\n    );\n\n    assert_snapshot!(\n        \"inject[app_rs]\",\n        fs::read_to_string(tree_fs.root.join(\"src\").join(\"app.rs\"))\n            .expect(\"app.rs injection failed\")\n    );\n\n    // TESTS\n    let tests_path = tree_fs.root.join(\"tests/models\");\n    assert_snapshot!(\n        \"generate[test_model]\",\n        fs::read_to_string(tests_path.join(\"movies.rs\")).expect(\"Failed to read movies.rs\")\n    );\n    assert_snapshot!(\n        \"inject[test_mod]\",\n        fs::read_to_string(tests_path.join(\"mod.rs\")).expect(\"Failed to read mod.rs\")\n    );\n\n    // VIEWS\n    match kind {\n        ScaffoldKind::Api => (),\n        ScaffoldKind::Html | ScaffoldKind::Htmx => {\n            let base_views_path = tree_fs.root.join(\"src\").join(\"views\");\n            assert_snapshot!(\n                \"generate[views_rs]\",\n                fs::read_to_string(base_views_path.join(\"movie.rs\"))\n                    .expect(\"Failed to read mod.rs\")\n            );\n            assert_snapshot!(\n                \"inject[views_mod_rs]\",\n                fs::read_to_string(base_views_path.join(\"mod.rs\")).expect(\"Failed to read mod.rs\")\n            );\n\n            let views_path = tree_fs.root.join(\"assets\").join(\"views\").join(\"movie\");\n            let views = vec![\"create\", \"edit\", \"list\", \"show\"];\n            for view in views {\n                assert_snapshot!(\n                    format!(\"generate[views_[{view}]]\"),\n                    fs::read_to_string(views_path.join(format!(\"{view}.html\")))\n                        .expect(\"view file missing\")\n                );\n            }\n        }\n    }\n}\n\n// thread 'templates::scaffold::can_generate::case_1' panicked at loco-gen/tests/templates/scaffold.rs:48:6:\n"
  },
  {
    "path": "loco-gen/tests/templates/scheduler.rs",
    "content": "use insta::assert_snapshot;\nuse loco_gen::{collect_messages, generate, AppInfo, Component};\nuse rrgen::RRgen;\nuse std::fs;\n\nmacro_rules! configure_insta {\n    ($($expr:expr),*) => {\n        let mut settings = insta::Settings::clone_current();\n        settings.set_prepend_module_to_snapshot(false);\n        settings.set_snapshot_suffix(\"scheduler\");\n        let _guard = settings.bind_to_scope();\n    };\n}\n#[test]\nfn can_generate() {\n    configure_insta!();\n    let component = Component::Scheduler {};\n\n    let tree_fs: tree_fs::Tree = tree_fs::TreeBuilder::default().drop(true).create().unwrap();\n\n    let rrgen = RRgen::with_working_dir(&tree_fs.root);\n\n    let gen_result = generate(\n        &rrgen,\n        component,\n        &AppInfo {\n            app_name: \"tester\".to_string(),\n        },\n    )\n    .expect(\"Failed to  generated scheduler file\");\n\n    assert_eq!(\n        collect_messages(&gen_result),\n        r\"* A Scheduler job configuration was added successfully. Run with `cargo loco scheduler --list`.\n\"\n    );\n\n    assert_snapshot!(\n        \"generate[controller_file]\",\n        fs::read_to_string(tree_fs.root.join(\"config\").join(\"scheduler.yaml\"))\n            .expect(\"Failed to read the scheduler.yaml\")\n    );\n}\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/generate[controller_file]@Api_controller.snap",
    "content": "---\nsource: loco-gen/tests/templates/controller.rs\nexpression: \"fs::read_to_string(controllers_path.join(\\\"movie.rs\\\")).expect(\\\"controller file missing\\\")\"\n---\n#![allow(clippy::missing_errors_doc)]\n#![allow(clippy::unnecessary_struct_initialization)]\n#![allow(clippy::unused_async)]\nuse loco_rs::prelude::*;\n\n#[debug_handler]\npub async fn index(State(_ctx): State<AppContext>) -> Result<Response> {\n    format::empty()\n}\n\n#[debug_handler]\npub async fn GET(State(_ctx): State<AppContext>) -> Result<Response> {\n    format::empty()\n}\n\n#[debug_handler]\npub async fn POST(State(_ctx): State<AppContext>) -> Result<Response> {\n    format::empty()\n}\n\npub fn routes() -> Routes {\n    Routes::new()\n        .prefix(\"api/movies/\")\n        .add(\"/\", get(index))\n        .add(\"GET\", get(GET))\n        .add(\"POST\", get(POST))\n}\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/generate[controller_file]@Api_scaffold.snap",
    "content": "---\nsource: loco-gen/tests/templates/scaffold.rs\nexpression: \"fs::read_to_string(controllers_path.join(\\\"movie.rs\\\")).expect(\\\"controller file missing\\\")\"\n---\n#![allow(clippy::missing_errors_doc)]\n#![allow(clippy::unnecessary_struct_initialization)]\n#![allow(clippy::unused_async)]\nuse loco_rs::prelude::*;\nuse serde::{Deserialize, Serialize};\n\nuse crate::models::_entities::movies::{ActiveModel, Entity, Model};\n\n#[derive(Clone, Debug, Serialize, Deserialize)]\npub struct Params {\n    pub title: Option<String>,\n    pub user_id: i32,\n    }\n\nimpl Params {\n    fn update(&self, item: &mut ActiveModel) {\n      item.title = Set(self.title.clone());\n      item.user_id = Set(self.user_id);\n      }\n}\n\nasync fn load_item(ctx: &AppContext, id: i32) -> Result<Model> {\n    let item = Entity::find_by_id(id).one(&ctx.db).await?;\n    item.ok_or_else(|| Error::NotFound)\n}\n\n#[debug_handler]\npub async fn list(State(ctx): State<AppContext>) -> Result<Response> {\n    format::json(Entity::find().all(&ctx.db).await?)\n}\n\n#[debug_handler]\npub async fn add(State(ctx): State<AppContext>, Json(params): Json<Params>) -> Result<Response> {\n    let mut item = ActiveModel {\n        ..Default::default()\n    };\n    params.update(&mut item);\n    let item = item.insert(&ctx.db).await?;\n    format::json(item)\n}\n\n#[debug_handler]\npub async fn update(\n    Path(id): Path<i32>,\n    State(ctx): State<AppContext>,\n    Json(params): Json<Params>,\n) -> Result<Response> {\n    let item = load_item(&ctx, id).await?;\n    let mut item = item.into_active_model();\n    params.update(&mut item);\n    let item = item.update(&ctx.db).await?;\n    format::json(item)\n}\n\n#[debug_handler]\npub async fn remove(Path(id): Path<i32>, State(ctx): State<AppContext>) -> Result<Response> {\n    load_item(&ctx, id).await?.delete(&ctx.db).await?;\n    format::empty()\n}\n\n#[debug_handler]\npub async fn get_one(Path(id): Path<i32>, State(ctx): State<AppContext>) -> Result<Response> {\n    format::json(load_item(&ctx, id).await?)\n}\n\npub fn routes() -> Routes {\n    Routes::new()\n        .prefix(\"api/movies/\")\n        .add(\"/\", get(list))\n        .add(\"/\", post(add))\n        .add(\"{id}\", get(get_one))\n        .add(\"{id}\", delete(remove))\n        .add(\"{id}\", put(update))\n        .add(\"{id}\", patch(update))\n}\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/generate[controller_file]@Html_controller.snap",
    "content": "---\nsource: loco-gen/tests/templates/controller.rs\nexpression: \"fs::read_to_string(controllers_path.join(\\\"movie.rs\\\")).expect(\\\"controller file missing\\\")\"\n---\n#![allow(clippy::missing_errors_doc)]\n#![allow(clippy::unnecessary_struct_initialization)]\n#![allow(clippy::unused_async)]\nuse loco_rs::prelude::*;\n\n#[debug_handler]\npub async fn GET(\n    ViewEngine(v): ViewEngine<TeraView>,\n    State(_ctx): State<AppContext>\n) -> Result<Response> {\n    format::render().view(&v, \"movie/GET.html\", data!({}))\n}\n\n#[debug_handler]\npub async fn POST(\n    ViewEngine(v): ViewEngine<TeraView>,\n    State(_ctx): State<AppContext>\n) -> Result<Response> {\n    format::render().view(&v, \"movie/POST.html\", data!({}))\n}\n\npub fn routes() -> Routes {\n    Routes::new()\n        .prefix(\"movies/\")\n        .add(\"GET\", get(GET))\n        .add(\"POST\", get(POST))\n}\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/generate[controller_file]@Html_scaffold.snap",
    "content": "---\nsource: loco-gen/tests/templates/scaffold.rs\nexpression: \"fs::read_to_string(controllers_path.join(\\\"movie.rs\\\")).expect(\\\"controller file missing\\\")\"\n---\n#![allow(clippy::missing_errors_doc)]\n#![allow(clippy::unnecessary_struct_initialization)]\n#![allow(clippy::unused_async)]\nuse loco_rs::prelude::*;\nuse serde::{Deserialize, Serialize};\nuse axum::response::Redirect;\nuse axum_extra::extract::Form;\nuse sea_orm::{sea_query::Order, QueryOrder};\n\nuse crate::{\n    models::_entities::movies::{ActiveModel, Column, Entity, Model},\n    views,\n};\n\n#[derive(Clone, Debug, Serialize, Deserialize)]\npub struct Params {\n    pub title: Option<String>,\n    pub user_id: i32,\n    }\n\nimpl Params {\n    fn update(&self, item: &mut ActiveModel) {\n      item.title = Set(self.title.clone());\n      item.user_id = Set(self.user_id);\n      }\n}\n\nasync fn load_item(ctx: &AppContext, id: i32) -> Result<Model> {\n    let item = Entity::find_by_id(id).one(&ctx.db).await?;\n    item.ok_or_else(|| Error::NotFound)\n}\n\n#[debug_handler]\npub async fn list(\n    ViewEngine(v): ViewEngine<TeraView>,\n    State(ctx): State<AppContext>,\n) -> Result<Response> {\n    let item = Entity::find()\n        .order_by(Column::Id, Order::Desc)\n        .all(&ctx.db)\n        .await?;\n    views::movie::list(&v, &item)\n}\n\n#[debug_handler]\npub async fn new(\n    ViewEngine(v): ViewEngine<TeraView>,\n    State(_ctx): State<AppContext>,\n) -> Result<Response> {\n    views::movie::create(&v)\n}\n\n#[debug_handler]\npub async fn update(\n    Path(id): Path<i32>,\n    State(ctx): State<AppContext>,\n    Form(params): Form<Params>,\n) -> Result<Redirect> {\n    let item = load_item(&ctx, id).await?;\n    let mut item = item.into_active_model();\n    params.update(&mut item);\n    item.update(&ctx.db).await?;\n    Ok(Redirect::to(\"../movies\"))\n}\n\n#[debug_handler]\npub async fn edit(\n    Path(id): Path<i32>,\n    ViewEngine(v): ViewEngine<TeraView>,\n    State(ctx): State<AppContext>,\n) -> Result<Response> {\n    let item = load_item(&ctx, id).await?;\n    views::movie::edit(&v, &item)\n}\n\n#[debug_handler]\npub async fn show(\n    Path(id): Path<i32>,\n    ViewEngine(v): ViewEngine<TeraView>,\n    State(ctx): State<AppContext>,\n) -> Result<Response> {\n    let item = load_item(&ctx, id).await?;\n    views::movie::show(&v, &item)\n}\n\n#[debug_handler]\npub async fn add(\n    State(ctx): State<AppContext>,\n    Form(params): Form<Params>,\n) -> Result<Redirect> {\n    let mut item = ActiveModel {\n        ..Default::default()\n    };\n    params.update(&mut item);\n    item.insert(&ctx.db).await?;\n    Ok(Redirect::to(\"movies\"))\n}\n\n#[debug_handler]\npub async fn remove(Path(id): Path<i32>, State(ctx): State<AppContext>) -> Result<Response> {\n    load_item(&ctx, id).await?.delete(&ctx.db).await?;\n    format::empty()\n}\n\npub fn routes() -> Routes {\n    Routes::new()\n        .prefix(\"movies/\")\n        .add(\"/\", get(list))\n        .add(\"/\", post(add))\n        .add(\"new\", get(new))\n        .add(\"{id}\", get(show))\n        .add(\"{id}/edit\", get(edit))\n        .add(\"{id}\", delete(remove))\n        .add(\"{id}\", post(update))\n}\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/generate[controller_file]@Htmx_controller.snap",
    "content": "---\nsource: loco-gen/tests/templates/controller.rs\nexpression: \"fs::read_to_string(controllers_path.join(\\\"movie.rs\\\")).expect(\\\"controller file missing\\\")\"\n---\n#![allow(clippy::missing_errors_doc)]\n#![allow(clippy::unnecessary_struct_initialization)]\n#![allow(clippy::unused_async)]\nuse loco_rs::prelude::*;\n\n#[debug_handler]\npub async fn GET(\n    ViewEngine(v): ViewEngine<TeraView>,\n    State(_ctx): State<AppContext>\n) -> Result<Response> {\n    format::render().view(&v, \"movie/GET.html\", data!({}))\n}\n\n#[debug_handler]\npub async fn POST(\n    ViewEngine(v): ViewEngine<TeraView>,\n    State(_ctx): State<AppContext>\n) -> Result<Response> {\n    format::render().view(&v, \"movie/POST.html\", data!({}))\n}\n\npub fn routes() -> Routes {\n    Routes::new()\n        .prefix(\"movies\")\n        .add(\"GET\", get(GET))\n        .add(\"POST\", get(POST))\n}\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/generate[controller_file]@Htmx_scaffold.snap",
    "content": "---\nsource: loco-gen/tests/templates/scaffold.rs\nexpression: \"fs::read_to_string(controllers_path.join(\\\"movie.rs\\\")).expect(\\\"controller file missing\\\")\"\n---\n#![allow(clippy::missing_errors_doc)]\n#![allow(clippy::unnecessary_struct_initialization)]\n#![allow(clippy::unused_async)]\nuse loco_rs::prelude::*;\nuse serde::{Deserialize, Serialize};\nuse sea_orm::{sea_query::Order, QueryOrder};\n\nuse crate::{\n    models::_entities::movies::{ActiveModel, Column, Entity, Model},\n    views,\n};\n\n#[derive(Clone, Debug, Serialize, Deserialize)]\npub struct Params {\n    pub title: Option<String>,\n    pub user_id: i32,\n    }\n\nimpl Params {\n    fn update(&self, item: &mut ActiveModel) {\n      item.title = Set(self.title.clone());\n      item.user_id = Set(self.user_id);\n      }\n}\n\nasync fn load_item(ctx: &AppContext, id: i32) -> Result<Model> {\n    let item = Entity::find_by_id(id).one(&ctx.db).await?;\n    item.ok_or_else(|| Error::NotFound)\n}\n\n#[debug_handler]\npub async fn list(\n    ViewEngine(v): ViewEngine<TeraView>,\n    State(ctx): State<AppContext>,\n) -> Result<Response> {\n    let item = Entity::find()\n        .order_by(Column::Id, Order::Desc)\n        .all(&ctx.db)\n        .await?;\n    views::movie::list(&v, &item)\n}\n\n#[debug_handler]\npub async fn new(\n    ViewEngine(v): ViewEngine<TeraView>,\n    State(_ctx): State<AppContext>,\n) -> Result<Response> {\n    views::movie::create(&v)\n}\n\n#[debug_handler]\npub async fn update(\n    Path(id): Path<i32>,\n    State(ctx): State<AppContext>,\n    Json(params): Json<Params>,\n) -> Result<Response> {\n    let item = load_item(&ctx, id).await?;\n    let mut item = item.into_active_model();\n    params.update(&mut item);\n    let _ = item.update(&ctx.db).await?;\n    format::render().redirect_with_header_key(\"HX-Redirect\", \"/movies\")\n}\n\n#[debug_handler]\npub async fn edit(\n    Path(id): Path<i32>,\n    ViewEngine(v): ViewEngine<TeraView>,\n    State(ctx): State<AppContext>,\n) -> Result<Response> {\n    let item = load_item(&ctx, id).await?;\n    views::movie::edit(&v, &item)\n}\n\n#[debug_handler]\npub async fn show(\n    Path(id): Path<i32>,\n    ViewEngine(v): ViewEngine<TeraView>,\n    State(ctx): State<AppContext>,\n) -> Result<Response> {\n    let item = load_item(&ctx, id).await?;\n    views::movie::show(&v, &item)\n}\n\n#[debug_handler]\npub async fn add(\n    State(ctx): State<AppContext>,\n    Json(params): Json<Params>,\n) -> Result<Response> {\n    let mut item = ActiveModel {\n        ..Default::default()\n    };\n    params.update(&mut item);\n    let _ = item.insert(&ctx.db).await?;\n    format::render().redirect_with_header_key(\"HX-Redirect\", \"/movies\")\n}\n\n#[debug_handler]\npub async fn remove(Path(id): Path<i32>, State(ctx): State<AppContext>) -> Result<Response> {\n    load_item(&ctx, id).await?.delete(&ctx.db).await?;\n    format::empty()\n}\n\npub fn routes() -> Routes {\n    Routes::new()\n        .prefix(\"movies/\")\n        .add(\"/\", get(list))\n        .add(\"/\", post(add))\n        .add(\"new\", get(new))\n        .add(\"{id}\", get(show))\n        .add(\"{id}/edit\", get(edit))\n        .add(\"{id}\", delete(remove))\n        .add(\"{id}\", put(update))\n        .add(\"{id}\", patch(update))\n}\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/generate[controller_file]@scheduler.snap",
    "content": "---\nsource: loco-gen/tests/templates/scheduler.rs\nexpression: \"fs::read_to_string(tree_fs.root.join(\\\"config\\\").join(\\\"scheduler.yaml\\\")).expect(\\\"Failed to read the scheduler.yaml\\\")\"\n---\noutput: stdout\njobs:\n  write_content:\n      shell: true\n      run: \"echo loco >> ./scheduler.txt\"\n      schedule: run every 1 second\n      # schedule: \"* * * * * * *\"\n      output: silent\n      tags: ['base', 'infra']\n\n  # run_task:\n  #     run: \"foo\"\n  #     schedule: \"at 10:00 am\"\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/generate[controller_file]@task.snap",
    "content": "---\nsource: loco-gen/tests/templates/task.rs\nexpression: \"fs::read_to_string(task_path.join(\\\"cleanup.rs\\\")).expect(\\\"Failed to read generated task file: cleanup.rs\\\")\"\n---\nuse loco_rs::prelude::*;\n\npub struct Cleanup;\n#[async_trait]\nimpl Task for Cleanup {\n    fn task(&self) -> TaskInfo {\n        TaskInfo {\n            name: \"cleanup\".to_string(),\n            detail: \"Task generator\".to_string(),\n        }\n    }\n    async fn run(&self, _app_context: &AppContext, _vars: &task::Vars) -> Result<()> {\n        println!(\"Task Cleanup generated\");\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/generate[controller_file]@worker.snap",
    "content": "---\nsource: loco-gen/tests/templates/worker.rs\nexpression: \"fs::read_to_string(worker_path.join(\\\"register_email.rs\\\")).expect(\\\"Failed to read generated worker file: register_email.rs\\\")\"\nsnapshot_kind: text\n---\nuse serde::{Deserialize, Serialize};\nuse loco_rs::prelude::*;\n\npub struct Worker {\n    pub ctx: AppContext,\n}\n\n#[derive(Deserialize, Debug, Serialize)]\npub struct WorkerArgs {\n}\n\n#[async_trait]\nimpl BackgroundWorker<WorkerArgs> for Worker {\n    /// Creates a new instance of the Worker with the given application context.\n    /// \n    /// This function is called when registering the worker with the queue system.\n    /// \n    /// # Parameters\n    /// * `ctx` - The application context containing shared resources\n    fn build(ctx: &AppContext) -> Self {\n        Self { ctx: ctx.clone() }\n    }\n\n    /// Returns the class name of the worker.\n    /// \n    /// This name is used when enqueueing jobs and identifying the worker in logs.\n    /// The implementation returns the struct name as a string.\n    fn class_name() -> String {\n        \"RegisterEmail\".to_string()\n    }\n\n    /// Returns tags associated with this worker.\n    /// \n    /// Tags can be used to filter which workers run during startup.\n    /// The default implementation returns an empty vector (no tags).\n    fn tags() -> Vec<String> {\n        Vec::new()\n    }\n    \n    /// Performs the actual work when a job is processed.\n    /// \n    /// This is the main function that contains the worker's logic.\n    /// It gets executed when a job is dequeued from the job queue.\n    /// \n    /// # Returns\n    /// * `Result<()>` - Ok if the job completed successfully, Err otherwise\n    async fn perform(&self, _args: WorkerArgs) -> Result<()> {\n        println!(\"=================RegisterEmail=======================\");\n        // TODO: Some actual work goes here...\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/generate[docker_file_[0]_[false]]@deployment.snap",
    "content": "---\nsource: loco-gen/tests/templates/deployment.rs\nexpression: \"fs::read_to_string(tree_fs.root.join(\\\"Dockerfile\\\")).expect(\\\"Dockerfile missing\\\")\"\nsnapshot_kind: text\n---\nFROM rust:[version]-slim AS builder\n\nWORKDIR /usr/src/\n\nCOPY . .\n\nRUN cargo build --release\n\nFROM debian:bookworm-slim\n\nWORKDIR /usr/app\n\nCOPY --from=builder /usr/src/config config\nCOPY --from=builder /usr/src/target/release/tester-cli tester-cli\n\nENTRYPOINT [\"/usr/app/tester-cli\"]\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/generate[docker_file_[0]_[true]]@deployment.snap",
    "content": "---\nsource: loco-gen/tests/templates/deployment.rs\nexpression: \"fs::read_to_string(tree_fs.root.join(\\\"Dockerfile\\\")).expect(\\\"Dockerfile missing\\\")\"\nsnapshot_kind: text\n---\nFROM rust:[version]-slim AS builder\n\nWORKDIR /usr/src/\n\nCOPY . .\n\nRUN apt-get update && apt-get install -y curl ca-certificates\n\n# Install Node.js using the latest available version from NodeSource.\n# In production, replace \"setup_current.x\" with a specific version\n# to avoid unexpected breaking changes in future releases.\nRUN curl -fsSL https://deb.nodesource.com/setup_current.x | bash - && \\\n    apt-get install -y nodejs\nRUN cd frontend && npm install && npm run build\nRUN cargo build --release\n\nFROM debian:bookworm-slim\n\nWORKDIR /usr/app\n\nCOPY --from=builder /usr/src/config config\nCOPY --from=builder /usr/src/target/release/tester-cli tester-cli\n\nENTRYPOINT [\"/usr/app/tester-cli\"]\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/generate[docker_file_[2]_[false]]@deployment.snap",
    "content": "---\nsource: loco-gen/tests/templates/deployment.rs\nexpression: \"fs::read_to_string(tree_fs.root.join(\\\"Dockerfile\\\")).expect(\\\"Dockerfile missing\\\")\"\nsnapshot_kind: text\n---\nFROM rust:[version]-slim AS builder\n\nWORKDIR /usr/src/\n\nCOPY . .\n\nRUN cargo build --release\n\nFROM debian:bookworm-slim\n\nWORKDIR /usr/app\n\nCOPY --from=builder /usr/src/404.html 404.html\nCOPY --from=builder /usr/src/asset asset\nCOPY --from=builder /usr/src/config config\nCOPY --from=builder /usr/src/target/release/tester-cli tester-cli\n\nENTRYPOINT [\"/usr/app/tester-cli\"]\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/generate[docker_file_[2]_[true]]@deployment.snap",
    "content": "---\nsource: loco-gen/tests/templates/deployment.rs\nexpression: \"fs::read_to_string(tree_fs.root.join(\\\"Dockerfile\\\")).expect(\\\"Dockerfile missing\\\")\"\nsnapshot_kind: text\n---\nFROM rust:[version]-slim AS builder\n\nWORKDIR /usr/src/\n\nCOPY . .\n\nRUN apt-get update && apt-get install -y curl ca-certificates\n\n# Install Node.js using the latest available version from NodeSource.\n# In production, replace \"setup_current.x\" with a specific version\n# to avoid unexpected breaking changes in future releases.\nRUN curl -fsSL https://deb.nodesource.com/setup_current.x | bash - && \\\n    apt-get install -y nodejs\nRUN cd frontend && npm install && npm run build\nRUN cargo build --release\n\nFROM debian:bookworm-slim\n\nWORKDIR /usr/app\n\nCOPY --from=builder /usr/src/404.html 404.html\nCOPY --from=builder /usr/src/asset asset\nCOPY --from=builder /usr/src/config config\nCOPY --from=builder /usr/src/target/release/tester-cli tester-cli\n\nENTRYPOINT [\"/usr/app/tester-cli\"]\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/generate[html_t_file]@mailer.snap",
    "content": "---\nsource: loco-gen/tests/templates/mailer.rs\nexpression: \"fs::read_to_string(path).unwrap_or_else(|_| panic!(\\\"{name} missing\\\"))\"\n---\nwelcome to <em>acmeworld!</em>\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/generate[mailer_mod_rs]@mailer.snap",
    "content": "---\nsource: loco-gen/tests/templates/mailer.rs\nexpression: \"fs::read_to_string(path).unwrap_or_else(|_| panic!(\\\"{name} missing\\\"))\"\n---\n#![allow(non_upper_case_globals)]\n\nuse loco_rs::prelude::*;\nuse serde_json::json;\n\nstatic welcome: Dir<'_> = include_dir!(\"src/mailers/reset_password/welcome\");\n\n#[allow(clippy::module_name_repetitions)]\npub struct ResetPassword {}\nimpl Mailer for ResetPassword {}\nimpl ResetPassword {\n    /// Send an email\n    ///\n    /// # Errors\n    /// When email sending is failed\n    pub async fn send_welcome(ctx: &AppContext, to: &str, msg: &str) -> Result<()> {\n        Self::mail_template(\n            ctx,\n            &welcome,\n            mailer::Args {\n                to: to.to_string(),\n                locals: json!({\n                  \"message\": msg,\n                  \"domain\": ctx.config.server.full_url()\n                }),\n                ..Default::default()\n            },\n        )\n        .await?;\n\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/generate[migration_file]@Api_scaffold.snap",
    "content": "---\nsource: loco-gen/tests/templates/scaffold.rs\nexpression: \"fs::read_to_string(&migration_file).expect(\\\"Failed to read the migration file\\\")\"\n---\nuse loco_rs::schema::*;\nuse sea_orm_migration::prelude::*;\n\n#[derive(DeriveMigrationName)]\npub struct Migration;\n\n#[async_trait::async_trait]\nimpl MigrationTrait for Migration {\n    async fn up(&self, m: &SchemaManager) -> Result<(), DbErr> {\n        create_table(m, \"movies\",\n            &[\n            \n            (\"id\", ColType::PkAuto),\n            \n            (\"title\", ColType::StringNull),\n            ],\n            &[\n            (\"user\", \"\"),\n            ]\n        ).await\n    }\n\n    async fn down(&self, m: &SchemaManager) -> Result<(), DbErr> {\n        drop_table(m, \"movies\").await\n    }\n}\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/generate[migration_file]@Html_scaffold.snap",
    "content": "---\nsource: loco-gen/tests/templates/scaffold.rs\nexpression: \"fs::read_to_string(&migration_file).expect(\\\"Failed to read the migration file\\\")\"\n---\nuse loco_rs::schema::*;\nuse sea_orm_migration::prelude::*;\n\n#[derive(DeriveMigrationName)]\npub struct Migration;\n\n#[async_trait::async_trait]\nimpl MigrationTrait for Migration {\n    async fn up(&self, m: &SchemaManager) -> Result<(), DbErr> {\n        create_table(m, \"movies\",\n            &[\n            \n            (\"id\", ColType::PkAuto),\n            \n            (\"title\", ColType::StringNull),\n            ],\n            &[\n            (\"user\", \"\"),\n            ]\n        ).await\n    }\n\n    async fn down(&self, m: &SchemaManager) -> Result<(), DbErr> {\n        drop_table(m, \"movies\").await\n    }\n}\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/generate[migration_file]@Htmx_scaffold.snap",
    "content": "---\nsource: loco-gen/tests/templates/scaffold.rs\nexpression: \"fs::read_to_string(&migration_file).expect(\\\"Failed to read the migration file\\\")\"\n---\nuse loco_rs::schema::*;\nuse sea_orm_migration::prelude::*;\n\n#[derive(DeriveMigrationName)]\npub struct Migration;\n\n#[async_trait::async_trait]\nimpl MigrationTrait for Migration {\n    async fn up(&self, m: &SchemaManager) -> Result<(), DbErr> {\n        create_table(m, \"movies\",\n            &[\n            \n            (\"id\", ColType::PkAuto),\n            \n            (\"title\", ColType::StringNull),\n            ],\n            &[\n            (\"user\", \"\"),\n            ]\n        ).await\n    }\n\n    async fn down(&self, m: &SchemaManager) -> Result<(), DbErr> {\n        drop_table(m, \"movies\").await\n    }\n}\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/generate[migration_file]@add_column_migration.snap",
    "content": "---\nsource: loco-gen/tests/templates/migration.rs\nexpression: \"fs::read_to_string(&migration_file).expect(\\\"Failed to read the migration file\\\")\"\nsnapshot_kind: text\n---\nuse loco_rs::schema::*;\nuse sea_orm_migration::prelude::*;\n\n#[derive(DeriveMigrationName)]\npub struct Migration;\n\n#[async_trait::async_trait]\nimpl MigrationTrait for Migration {\n    async fn up(&self, m: &SchemaManager) -> Result<(), DbErr> {\n        add_column(m, \"users\", \"name\", ColType::StringNull).await?;\n        add_column(m, \"users\", \"age\", ColType::IntegerNull).await?;\n        Ok(())\n    }\n\n    async fn down(&self, m: &SchemaManager) -> Result<(), DbErr> {\n        remove_column(m, \"users\", \"name\").await?;\n        remove_column(m, \"users\", \"age\").await?;\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/generate[migration_file]@add_reference_migration.snap",
    "content": "---\nsource: loco-gen/tests/templates/migration.rs\nexpression: \"fs::read_to_string(&migration_file).expect(\\\"Failed to read the migration file\\\")\"\nsnapshot_kind: text\n---\nuse loco_rs::schema::*;\nuse sea_orm_migration::prelude::*;\n\n#[derive(DeriveMigrationName)]\npub struct Migration;\n\n#[async_trait::async_trait]\nimpl MigrationTrait for Migration {\n    async fn up(&self, m: &SchemaManager) -> Result<(), DbErr> {\n        add_reference(m, \"posts\", \"user\", \"\").await?;\n        Ok(())\n    }\n\n    async fn down(&self, m: &SchemaManager) -> Result<(), DbErr> {\n        remove_reference(m, \"posts\", \"user\", \"\").await?;\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/generate[migration_file]@create_join_table_migration.snap",
    "content": "---\nsource: loco-gen/tests/templates/migration.rs\nexpression: \"fs::read_to_string(&migration_file).expect(\\\"Failed to read the migration file\\\")\"\nsnapshot_kind: text\n---\nuse loco_rs::schema::*;\nuse sea_orm_migration::prelude::*;\n\n#[derive(DeriveMigrationName)]\npub struct Migration;\n\n#[async_trait::async_trait]\nimpl MigrationTrait for Migration {\n    async fn up(&self, m: &SchemaManager) -> Result<(), DbErr> {\n        create_join_table_without_timestamps(m, \"user_groups\",\n            &[\n            (\"count\", ColType::IntegerNull),\n            ],\n            &[\n            (\"user\", \"\"),\n            (\"group\", \"\"),\n            ]\n        ).await\n    }\n\n    async fn down(&self, m: &SchemaManager) -> Result<(), DbErr> {\n        drop_table(m, \"user_groups\").await\n    }\n}\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/generate[migration_file]@create_join_table_without_tz_migration.snap",
    "content": "---\nsource: loco-gen/tests/templates/migration.rs\nexpression: \"fs::read_to_string(&migration_file).expect(\\\"Failed to read the migration file\\\")\"\nsnapshot_kind: text\n---\nuse loco_rs::schema::*;\nuse sea_orm_migration::prelude::*;\n\n#[derive(DeriveMigrationName)]\npub struct Migration;\n\n#[async_trait::async_trait]\nimpl MigrationTrait for Migration {\n    async fn up(&self, m: &SchemaManager) -> Result<(), DbErr> {\n        create_join_table_without_timestamps(m, \"user_groups\",\n            &[\n            (\"count\", ColType::IntegerNull),\n            ],\n            &[\n            (\"user\", \"\"),\n            (\"group\", \"\"),\n            ]\n        ).await\n    }\n\n    async fn down(&self, m: &SchemaManager) -> Result<(), DbErr> {\n        drop_table(m, \"user_groups\").await\n    }\n}\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/generate[migration_file]@create_table_migration.snap",
    "content": "---\nsource: loco-gen/tests/templates/migration.rs\nexpression: \"fs::read_to_string(&migration_file).expect(\\\"Failed to read the migration file\\\")\"\n---\nuse loco_rs::schema::*;\nuse sea_orm_migration::prelude::*;\n\n#[derive(DeriveMigrationName)]\npub struct Migration;\n\n#[async_trait::async_trait]\nimpl MigrationTrait for Migration {\n    async fn up(&self, m: &SchemaManager) -> Result<(), DbErr> {\n        create_table(m, \"movies\",\n            &[\n            \n            (\"id\", ColType::PkAuto),\n            \n            (\"title\", ColType::StringNull),\n            ],\n            &[\n            (\"user\", \"\"),\n            ]\n        ).await\n    }\n\n    async fn down(&self, m: &SchemaManager) -> Result<(), DbErr> {\n        drop_table(m, \"movies\").await\n    }\n}\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/generate[migration_file]@create_table_without_tz_migration.snap",
    "content": "---\nsource: loco-gen/tests/templates/migration.rs\nexpression: \"fs::read_to_string(&migration_file).expect(\\\"Failed to read the migration file\\\")\"\nsnapshot_kind: text\n---\nuse loco_rs::schema::*;\nuse sea_orm_migration::prelude::*;\n\n#[derive(DeriveMigrationName)]\npub struct Migration;\n\n#[async_trait::async_trait]\nimpl MigrationTrait for Migration {\n    async fn up(&self, m: &SchemaManager) -> Result<(), DbErr> {\n        create_table_without_timestamps(m, \"movies\",\n            &[\n            \n            (\"id\", ColType::PkAuto),\n            \n            (\"title\", ColType::StringNull),\n            ],\n            &[\n            (\"user\", \"\"),\n            ]\n        ).await\n    }\n\n    async fn down(&self, m: &SchemaManager) -> Result<(), DbErr> {\n        drop_table(m, \"movies\").await\n    }\n}\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/generate[migration_file]@empty_migration.snap",
    "content": "---\nsource: loco-gen/tests/templates/migration.rs\nexpression: \"fs::read_to_string(&migration_file).expect(\\\"Failed to read the migration file\\\")\"\nsnapshot_kind: text\n---\nuse sea_orm_migration::{prelude::*, schema::*};\n\n#[derive(DeriveMigrationName)]\npub struct Migration;\n\n#[async_trait::async_trait]\nimpl MigrationTrait for Migration {\n    async fn up(&self, m: &SchemaManager) -> Result<(), DbErr> {\n        todo!()\n    }\n\n    async fn down(&self, m: &SchemaManager) -> Result<(), DbErr> {\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/generate[migration_file]@model.snap",
    "content": "---\nsource: loco-gen/tests/templates/model.rs\nexpression: \"fs::read_to_string(&migration_file).expect(\\\"Failed to read the migration file\\\")\"\n---\nuse loco_rs::schema::*;\nuse sea_orm_migration::prelude::*;\n\n#[derive(DeriveMigrationName)]\npub struct Migration;\n\n#[async_trait::async_trait]\nimpl MigrationTrait for Migration {\n    async fn up(&self, m: &SchemaManager) -> Result<(), DbErr> {\n        create_table(m, \"movies\",\n            &[\n            \n            (\"id\", ColType::PkAuto),\n            \n            (\"title\", ColType::StringNull),\n            ],\n            &[\n            ]\n        ).await\n    }\n\n    async fn down(&self, m: &SchemaManager) -> Result<(), DbErr> {\n        drop_table(m, \"movies\").await\n    }\n}\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/generate[migration_file]@remove_columns_migration.snap",
    "content": "---\nsource: loco-gen/tests/templates/migration.rs\nexpression: \"fs::read_to_string(&migration_file).expect(\\\"Failed to read the migration file\\\")\"\nsnapshot_kind: text\n---\nuse loco_rs::schema::*;\nuse sea_orm_migration::prelude::*;\n\n#[derive(DeriveMigrationName)]\npub struct Migration;\n\n#[async_trait::async_trait]\nimpl MigrationTrait for Migration {\n    async fn up(&self, m: &SchemaManager) -> Result<(), DbErr> {\n        remove_column(m, \"users\", \"name\").await?;\n        remove_column(m, \"users\", \"age\").await?;\n        Ok(())\n    }\n\n    async fn down(&self, m: &SchemaManager) -> Result<(), DbErr> {\n        add_column(m, \"users\", \"name\", ColType::StringNull).await?;\n        add_column(m, \"users\", \"age\", ColType::IntegerNull).await?;\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/generate[nginx]@deployment.snap",
    "content": "---\nsource: loco-gen/tests/templates/deployment.rs\nexpression: \"fs::read_to_string(tree_fs.root.join(\\\"nginx\\\").join(\\\"default.conf\\\")).expect(\\\"nginx config missing\\\")\"\n---\nserver {\n  listen 80;\n  server_name ~^(?<subdomain>\\w*)\\.localhost$;\n\n  location / {\n      if ($http_x_subdomain = \"\") {\n          set $http_x_subdomain $subdomain;\n      }\n      proxy_set_header X-Subdomain $http_x_subdomain;\n      proxy_pass http://localhost:8080/;\n  }\n}\n\nserver {\n  listen 80;\n  server_name localhost;\n\n  location / {\n      proxy_pass http://localhost:8080/;\n  }\n}\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/generate[subject_t_file]@mailer.snap",
    "content": "---\nsource: loco-gen/tests/templates/mailer.rs\nexpression: \"fs::read_to_string(path).unwrap_or_else(|_| panic!(\\\"{name} missing\\\"))\"\n---\nguess what? welcome!\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/generate[test_model]@Api_scaffold.snap",
    "content": "---\nsource: loco-gen/tests/templates/scaffold.rs\nexpression: \"fs::read_to_string(tests_path.join(\\\"movies.rs\\\")).expect(\\\"Failed to read movies.rs\\\")\"\n---\nuse tester::app::App;\nuse loco_rs::testing::prelude::*;\nuse serial_test::serial;\n\nmacro_rules! configure_insta {\n    ($($expr:expr),*) => {\n        let mut settings = insta::Settings::clone_current();\n        settings.set_prepend_module_to_snapshot(false);\n        let _guard = settings.bind_to_scope();\n    };\n}\n\n#[tokio::test]\n#[serial]\nasync fn test_model() {\n    configure_insta!();\n\n    let boot = boot_test::<App>().await.unwrap();\n    seed::<App>(&boot.app_context).await.unwrap();\n\n    // query your model, e.g.:\n    //\n    // let item = models::posts::Model::find_by_pid(\n    //     &boot.app_context.db,\n    //     \"11111111-1111-1111-1111-111111111111\",\n    // )\n    // .await;\n\n    // snapshot the result:\n    // assert_debug_snapshot!(item);\n}\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/generate[test_model]@Html_scaffold.snap",
    "content": "---\nsource: loco-gen/tests/templates/scaffold.rs\nexpression: \"fs::read_to_string(tests_path.join(\\\"movies.rs\\\")).expect(\\\"Failed to read movies.rs\\\")\"\n---\nuse tester::app::App;\nuse loco_rs::testing::prelude::*;\nuse serial_test::serial;\n\nmacro_rules! configure_insta {\n    ($($expr:expr),*) => {\n        let mut settings = insta::Settings::clone_current();\n        settings.set_prepend_module_to_snapshot(false);\n        let _guard = settings.bind_to_scope();\n    };\n}\n\n#[tokio::test]\n#[serial]\nasync fn test_model() {\n    configure_insta!();\n\n    let boot = boot_test::<App>().await.unwrap();\n    seed::<App>(&boot.app_context).await.unwrap();\n\n    // query your model, e.g.:\n    //\n    // let item = models::posts::Model::find_by_pid(\n    //     &boot.app_context.db,\n    //     \"11111111-1111-1111-1111-111111111111\",\n    // )\n    // .await;\n\n    // snapshot the result:\n    // assert_debug_snapshot!(item);\n}\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/generate[test_model]@Htmx_scaffold.snap",
    "content": "---\nsource: loco-gen/tests/templates/scaffold.rs\nexpression: \"fs::read_to_string(tests_path.join(\\\"movies.rs\\\")).expect(\\\"Failed to read movies.rs\\\")\"\n---\nuse tester::app::App;\nuse loco_rs::testing::prelude::*;\nuse serial_test::serial;\n\nmacro_rules! configure_insta {\n    ($($expr:expr),*) => {\n        let mut settings = insta::Settings::clone_current();\n        settings.set_prepend_module_to_snapshot(false);\n        let _guard = settings.bind_to_scope();\n    };\n}\n\n#[tokio::test]\n#[serial]\nasync fn test_model() {\n    configure_insta!();\n\n    let boot = boot_test::<App>().await.unwrap();\n    seed::<App>(&boot.app_context).await.unwrap();\n\n    // query your model, e.g.:\n    //\n    // let item = models::posts::Model::find_by_pid(\n    //     &boot.app_context.db,\n    //     \"11111111-1111-1111-1111-111111111111\",\n    // )\n    // .await;\n\n    // snapshot the result:\n    // assert_debug_snapshot!(item);\n}\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/generate[test_model]@model.snap",
    "content": "---\nsource: loco-gen/tests/templates/model.rs\nexpression: \"fs::read_to_string(tests_path.join(\\\"movies.rs\\\")).expect(\\\"Failed to read movies.rs\\\")\"\n---\nuse tester::app::App;\nuse loco_rs::testing::prelude::*;\nuse serial_test::serial;\n\nmacro_rules! configure_insta {\n    ($($expr:expr),*) => {\n        let mut settings = insta::Settings::clone_current();\n        settings.set_prepend_module_to_snapshot(false);\n        let _guard = settings.bind_to_scope();\n    };\n}\n\n#[tokio::test]\n#[serial]\nasync fn test_model() {\n    configure_insta!();\n\n    let boot = boot_test::<App>().await.unwrap();\n    seed::<App>(&boot.app_context).await.unwrap();\n\n    // query your model, e.g.:\n    //\n    // let item = models::posts::Model::find_by_pid(\n    //     &boot.app_context.db,\n    //     \"11111111-1111-1111-1111-111111111111\",\n    // )\n    // .await;\n\n    // snapshot the result:\n    // assert_debug_snapshot!(item);\n}\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/generate[tests_controller_mod_rs]@Api_controller.snap",
    "content": "---\nsource: loco-gen/tests/templates/controller.rs\nexpression: \"fs::read_to_string(test_controllers_path.join(\\\"movie.rs\\\")).expect(\\\"test file missing\\\")\"\n---\nuse tester::app::App;\nuse loco_rs::testing::prelude::*;\nuse serial_test::serial;\n\n#[tokio::test]\n#[serial]\nasync fn can_get_movies() {\n    request::<App, _, _>(|request, _ctx| async move {\n        let res = request.get(\"/api/movies/\").await;\n        assert_eq!(res.status_code(), 200);\n\n        // you can assert content like this:\n        // assert_eq!(res.text(), \"content\");\n    })\n    .await;\n}\n\n#[tokio::test]\n#[serial]\nasync fn can_get_GET() {\n    request::<App, _, _>(|request, _ctx| async move {\n        let res = request.get(\"/movies/GET\").await;\n        assert_eq!(res.status_code(), 200);\n    })\n    .await;\n}\n\n#[tokio::test]\n#[serial]\nasync fn can_get_POST() {\n    request::<App, _, _>(|request, _ctx| async move {\n        let res = request.get(\"/movies/POST\").await;\n        assert_eq!(res.status_code(), 200);\n    })\n    .await;\n}\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/generate[tests_task_file]@task.snap",
    "content": "---\nsource: loco-gen/tests/templates/task.rs\nexpression: \"fs::read_to_string(tests_task_path.join(\\\"cleanup.rs\\\")).expect(\\\"Failed to read generated tests task file: cleanup.rs\\\")\"\n---\nuse tester::app::App;\nuse loco_rs::{task, testing::prelude::*};\n\nuse loco_rs::boot::run_task;\nuse serial_test::serial;\n\n#[tokio::test]\n#[serial]\nasync fn test_can_run_cleanup() {\n    let boot = boot_test::<App>().await.unwrap();\n\n    assert!(\n        run_task::<App>(&boot.app_context, Some(&\"cleanup\".to_string()), &task::Vars::default())\n            .await\n            .is_ok()\n    );\n}\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/generate[tests_worker_file]@worker.snap",
    "content": "---\nsource: loco-gen/tests/templates/worker.rs\nexpression: \"fs::read_to_string(tests_worker_path.join(\\\"register_email.rs\\\")).expect(\\\"Failed to read generated tests worker file: register_email.rs\\\")\"\n---\nuse loco_rs::{bgworker::BackgroundWorker, testing::prelude::*};\nuse tester::{\n    app::App,\n    workers::register_email::{Worker, WorkerArgs},\n};\nuse serial_test::serial;\n\n#[tokio::test]\n#[serial]\nasync fn test_run_register_email_worker() {\n    let boot = boot_test::<App>().await.unwrap();\n\n    // Execute the worker ensuring that it operates in 'ForegroundBlocking' mode, which prevents the addition of your worker to the background\n    assert!(\n        Worker::perform_later(&boot.app_context,WorkerArgs {})\n            .await\n            .is_ok()\n    );\n    // Include additional assert validations after the execution of the worker\n}\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/generate[text_t_file]@mailer.snap",
    "content": "---\nsource: loco-gen/tests/templates/mailer.rs\nexpression: \"fs::read_to_string(path).unwrap_or_else(|_| panic!(\\\"{name} missing\\\"))\"\n---\nwelcome to acmeworld!\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/generate[views_[create]]@Html_scaffold.snap",
    "content": "---\nsource: loco-gen/tests/templates/scaffold.rs\nexpression: \"fs::read_to_string(views_path.join(format!(\\\"{view}.html\\\"))).expect(\\\"view file missing\\\")\"\nsnapshot_kind: text\n---\n{% extends \"base.html\" %}\n\n{% block title %}\nCreate movie\n{% endblock title %}\n\n{% block page_title %}\nCreate new movie\n{% endblock page_title %}\n\n{% block content %}\n<div class=\"mb-10\">\n    <form action=\"/movies\" method=\"post\" class=\"flex-1 lg:max-w-2xl\">\n    <div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">title</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"title\" name=\"title\" type=\"text\" value=\"\"  />\n</div>\n        <div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">user_id</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-2147483648\" max=\"2147483647\" id=\"user_id\" name=\"user_id\" type=\"number\" value=\"\"  step=\"1\" />\n</div>\n        <div class=\"mt-5\">\n            <button class=\" text-xs py-3 px-6 rounded-lg bg-gray-900 text-white\" type=\"submit\">Submit</button>\n        </div>\n    </form>\n<br />\n<a href=\"/movies\">Back to movies</a>\n</div>\n{% endblock content %}\n\n{% block js %}\n\n{% endblock js %}\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/generate[views_[create]]@Htmx_scaffold.snap",
    "content": "---\nsource: loco-gen/tests/templates/scaffold.rs\nexpression: \"fs::read_to_string(views_path.join(format!(\\\"{view}.html\\\"))).expect(\\\"view file missing\\\")\"\nsnapshot_kind: text\n---\n{% extends \"base.html\" %}\n\n{% block title %}\nCreate movie\n{% endblock title %}\n\n{% block page_title %}\nCreate new movie\n{% endblock page_title %}\n\n{% block content %}\n<div class=\"mb-10\">\n    <div id=\"error-message\" class=\"mt-4 text-sm text-red-600\"></div>\n    <form hx-post=\"/movies\" hx-ext=\"submitjson\" class=\"flex-1 lg:max-w-2xl\">\n        <div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">title</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"title\" name=\"title\" type=\"text\" value=\"\"  />\n</div>\n        <div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">user_id</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-2147483648\" max=\"2147483647\" id=\"user_id\" name=\"user_id\" type=\"number\" value=\"\"  step=\"1\" />\n</div>\n        <div class=\"mt-5\">\n            <button class=\" text-xs py-3 px-6 rounded-lg bg-gray-900 text-white\" type=\"submit\">Submit</button>\n        </div>\n\n    </form>\n</div>\n{% endblock content %}\n\n{% block js %}\n\n{% endblock js %}\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/generate[views_[edit]]@Html_scaffold.snap",
    "content": "---\nsource: loco-gen/tests/templates/scaffold.rs\nexpression: \"fs::read_to_string(views_path.join(format!(\\\"{view}.html\\\"))).expect(\\\"view file missing\\\")\"\nsnapshot_kind: text\n---\n{% extends \"base.html\" %}\n\n{% block title %}\nEdit movie: {{ item.id }}\n{% endblock title %}\n\n{% block page_title %}\nEdit movie: {{ item.id }}\n{% endblock page_title %}\n\n{% block content %}\n<div class=\"mb-10\">\n    <form action=\"/movies/{{ item.id }}\" method=\"post\" class=\"flex-1 lg:max-w-2xl\">\n    <div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">title</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"title\" name=\"title\" type=\"text\" value=\"{{item.title}}\"  />\n</div>\n        <div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">user_id</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-2147483648\" max=\"2147483647\" id=\"user_id\" name=\"user_id\" type=\"number\" value=\"{{item.user_id}}\"  step=\"1\" />\n</div>\n        <div>\n            <div class=\"mt-5\">\n                <button class=\" text-xs py-3 px-6 rounded-lg bg-gray-900 text-white\" type=\"submit\">Submit</button>\n                <button class=\"text-xs py-3 px-6 rounded-lg bg-red-600 text-white\"\n                            onclick=\"confirmDelete(event, '/movies/{{ item.id }}', '/movies' )\">Delete</button>\n            </div>\n        </div> \n    </form>\n    <div id=\"success-message\" class=\"mt-4\"></div>\n    <br />\n    <a href=\"/movies\">Back to movie</a>\n</div>\n{% endblock content %}\n\n{% block js %}\n\n{% endblock js %}\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/generate[views_[edit]]@Htmx_scaffold.snap",
    "content": "---\nsource: loco-gen/tests/templates/scaffold.rs\nexpression: \"fs::read_to_string(views_path.join(format!(\\\"{view}.html\\\"))).expect(\\\"view file missing\\\")\"\nsnapshot_kind: text\n---\n{% extends \"base.html\" %}\n\n{% block title %}\nEdit movie: {{ item.id }}\n{% endblock title %}\n\n{% block page_title %}\nEdit movie: {{ item.id }}\n{% endblock page_title %}\n\n{% block content %}\n<div class=\"mb-10\">\n    <div id=\"error-message\" class=\"mt-4 text-sm text-red-600\"></div>\n    <form hx-put=\"/movies/{{ item.id }}\" hx-ext=\"submitjson\" hx-target=\"#success-message\" class=\"flex-1 lg:max-w-2xl\">\n        <div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">title</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" id=\"title\" name=\"title\" type=\"text\" value=\"{{item.title}}\"  />\n</div>\n        <div class=\"space-y-2\">\n    <label class=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\" for=\":r2l:-form-item\">user_id</label>\n    <input class=\"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm md:text-sm\" min=\"-2147483648\" max=\"2147483647\" id=\"user_id\" name=\"user_id\" type=\"number\" value=\"{{item.user_id}}\"  step=\"1\" />\n</div>\n        <div>\n            <div class=\"mt-5\">\n                <button class=\" text-xs py-3 px-6 rounded-lg bg-gray-900 text-white\" type=\"submit\">Submit</button>\n                <button class=\"text-xs py-3 px-6 rounded-lg bg-red-600 text-white\"\n                            onclick=\"confirmDelete(event, '/movies/{{ item.id }}', '/movies' )\">Delete</button>\n            </div>\n        </div>\n    </form>\n    <div id=\"success-message\" class=\"mt-4\"></div>\n    <br />\n    <a href=\"/movies\">Back to movie</a>\n</div>\n{% endblock content %}\n\n{% block js %}\n\n{% endblock js %}\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/generate[views_[list]]@Html_scaffold.snap",
    "content": "---\nsource: loco-gen/tests/templates/scaffold.rs\nexpression: \"fs::read_to_string(views_path.join(format!(\\\"{view}.html\\\"))).expect(\\\"view file missing\\\")\"\nsnapshot_kind: text\n---\n{% extends \"base.html\" %}\n\n{% block title %}\nList of movie\n{% endblock title %}\n\n{% block page_title %}\nmovie\n{% endblock page_title %}\n\n{% block content %}\n<div class=\"mb-10\">\n\n    {% if items %}\n\n    <div class=\"mb-5\">\n        <div class=\"relative w-full overflow-auto\">\n            <table class=\"w-full caption-bottom text-sm\">\n                <thead class=\"[&amp;_tr]:border-b\">\n                    <tr class=\"border-b transition-colors hover:bg-muted/50\">\n                        <th class=\"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&amp;:has([role=checkbox])]:pr-0 [&amp;>[role=checkbox]]:translate-y-[2px] w-[100px]\">\n                            {{\"title\" | capitalize }}\n                        </th>\n                        <th class=\"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&amp;:has([role=checkbox])]:pr-0 [&amp;>[role=checkbox]]:translate-y-[2px] w-[100px]\">\n                            {{\"user_id\" | capitalize }}\n                        </th>\n                        <th class=\"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&amp;:has([role=checkbox])]:pr-0 [&amp;>[role=checkbox]]:translate-y-[2px] w-[100px]\">\n                           Actions\n                        </th>\n                    </tr>\n                </thead>\n                <tbody class=\"[&amp;_tr:last-child]:border-0\">\n                   {% for item in items %}\n                    <tr class=\"border-b transition-colors hover:bg-muted/50\">\n                        <td\n                            class=\"p-2 align-middle  font-medium\">\n                            {{item.title | escape }}\n                        </td>\n                        <td\n                            class=\"p-2 align-middle  font-medium\">\n                            {{item.user_id}}\n                        </td>\n                        <td>\n                            <a href=\"/movies/{{ item.id }}/edit\">Edit</a>\n                        </td>\n                    </tr>\n                    {% endfor %}\n                </tbody>\n            </table>\n        </div>\n    \n        <div class=\"flex\">\n            <div class=\"ml-auto  p-4\">\n                <a href=\"/movies/new\"\n                    class=\"mt-5 bg-blue-500 text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800\">\n                    Create\n                </a>\n            </div>\n        </div>\n    </div>\n\n    {% else %}\n\n    <div class=\"mt-10 flex items-center justify-center\">\n        <div class=\"bg-white rounded-lg shadow-lg p-8 max-w-4xl w-full flex flex-col items-center\">\n            <h3 class=\"font-bold text-lg\">Nothing Here Yet</h3>\n            There are no records to display. Add a new record to get started!\n            <a href=\"/movies/new\"\n            class=\"mt-5 bg-blue-500 text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800\">\n            Create\n        </a>\n        </div>\n    </div>\n   \n    {% endif %}\n\n    \n</div>\n{% endblock content %}\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/generate[views_[list]]@Htmx_scaffold.snap",
    "content": "---\nsource: loco-gen/tests/templates/scaffold.rs\nexpression: \"fs::read_to_string(views_path.join(format!(\\\"{view}.html\\\"))).expect(\\\"view file missing\\\")\"\nsnapshot_kind: text\n---\n{% extends \"base.html\" %}\n\n{% block title %}\nList of movie\n{% endblock title %}\n\n{% block page_title %}\nmovie\n{% endblock page_title %}\n\n{% block content %}\n<div class=\"mb-10\">\n\n    {% if items %}\n\n    <div class=\"mb-5\">\n        <div class=\"relative w-full overflow-auto\">\n            <table class=\"w-full caption-bottom text-sm\">\n                <thead class=\"[&amp;_tr]:border-b\">\n                    <tr class=\"border-b transition-colors hover:bg-muted/50\">\n                        <th class=\"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&amp;:has([role=checkbox])]:pr-0 [&amp;>[role=checkbox]]:translate-y-[2px] w-[100px]\">\n                            {{\"title\" | capitalize }}\n                        </th>\n                        <th class=\"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&amp;:has([role=checkbox])]:pr-0 [&amp;>[role=checkbox]]:translate-y-[2px] w-[100px]\">\n                            {{\"user_id\" | capitalize }}\n                        </th>\n                        <th class=\"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&amp;:has([role=checkbox])]:pr-0 [&amp;>[role=checkbox]]:translate-y-[2px] w-[100px]\">\n                           Actions\n                        </th>\n                    </tr>\n                </thead>\n                <tbody class=\"[&amp;_tr:last-child]:border-0\">\n                   {% for item in items %}\n                    <tr class=\"border-b transition-colors hover:bg-muted/50\">\n                        <td\n                            class=\"p-2 align-middle  font-medium\">\n                            {{item.title | escape }}\n                        </td>\n                        <td\n                            class=\"p-2 align-middle  font-medium\">\n                            {{item.user_id}}\n                        </td>\n                        <td>\n                            <a href=\"/movies/{{ item.id }}/edit\">Edit</a>\n                        </td>\n                    </tr>\n                    {% endfor %}\n                </tbody>\n            </table>\n        </div>\n    \n        <div class=\"flex\">\n            <div class=\"ml-auto  p-4\">\n                <a href=\"/movies/new\"\n                    class=\"mt-5 bg-blue-500 text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800\">\n                    Create\n                </a>\n            </div>\n        </div>\n    </div>\n\n    {% else %}\n\n    <div class=\"mt-10 flex items-center justify-center\">\n        <div class=\"bg-white rounded-lg shadow-lg p-8 max-w-4xl w-full flex flex-col items-center\">\n            <h3 class=\"font-bold text-lg\">Nothing Here Yet</h3>\n            There are no records to display. Add a new record to get started!\n            <a href=\"/movies/new\"\n            class=\"mt-5 bg-blue-500 text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800\">\n            Create\n        </a>\n        </div>\n    </div>\n   \n    {% endif %}\n\n    \n</div>\n{% endblock content %}\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/generate[views_[show]]@Html_scaffold.snap",
    "content": "---\nsource: loco-gen/tests/templates/scaffold.rs\nexpression: \"fs::read_to_string(views_path.join(format!(\\\"{view}.html\\\"))).expect(\\\"view file missing\\\")\"\nsnapshot_kind: text\n---\n{% extends \"base.html\" %}\n\n{% block title %}\nView movie: {{ item.id }}\n{% endblock title %}\n\n{% block content %}\n<h1>View movie: {{ item.id }}</h1>\n<div class=\"mb-10\">\n<div>\n        <label>title: {{item.title}}</label>\n    </div>\n<div>\n        <label>user_id: {{item.user_id}}</label>\n    </div>\n<br />\n<a href=\"/movies\">Back to movies</a>\n</div>\n{% endblock content %}\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/generate[views_[show]]@Htmx_scaffold.snap",
    "content": "---\nsource: loco-gen/tests/templates/scaffold.rs\nexpression: \"fs::read_to_string(views_path.join(format!(\\\"{view}.html\\\"))).expect(\\\"view file missing\\\")\"\nsnapshot_kind: text\n---\n{% extends \"base.html\" %}\n\n{% block title %}\nView movie: {{ item.id }}\n{% endblock title %}\n\n{% block page_title %}\nView movie: {{ item.id }}\n{% endblock page_title %}\n\n\n{% block content %}\n<div class=\"mb-10\">\n    <div>\n    <label><b>{{\"title\" | capitalize }}:</b> {{item.title}}</label>\n    </div>\n<div>\n    <label><b>{{\"user_id\" | capitalize }}:</b> {{item.user_id}}</label>\n    </div>\n<br />\n<a href=\"/movies\">Back to movies</a>\n</div>\n{% endblock content %}\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/generate[views_rs]@Html_scaffold.snap",
    "content": "---\nsource: loco-gen/tests/templates/scaffold.rs\nexpression: \"fs::read_to_string(base_views_path.join(\\\"movie.rs\\\")).expect(\\\"Failed to read mod.rs\\\")\"\n---\nuse loco_rs::prelude::*;\n\nuse crate::models::_entities::movies;\n\n/// Render a list view of `movies`.\n///\n/// # Errors\n///\n/// When there is an issue with rendering the view.\npub fn list(v: &impl ViewRenderer, items: &Vec<movies::Model>) -> Result<Response> {\n    format::render().view(v, \"movie/list.html\", data!({\"items\": items}))\n}\n\n/// Render a single `movie` view.\n///\n/// # Errors\n///\n/// When there is an issue with rendering the view.\npub fn show(v: &impl ViewRenderer, item: &movies::Model) -> Result<Response> {\n    format::render().view(v, \"movie/show.html\", data!({\"item\": item}))\n}\n\n/// Render a `movie` create form.\n///\n/// # Errors\n///\n/// When there is an issue with rendering the view.\npub fn create(v: &impl ViewRenderer) -> Result<Response> {\n    format::render().view(v, \"movie/create.html\", data!({}))\n}\n\n/// Render a `movie` edit form.\n///\n/// # Errors\n///\n/// When there is an issue with rendering the view.\npub fn edit(v: &impl ViewRenderer, item: &movies::Model) -> Result<Response> {\n    format::render().view(v, \"movie/edit.html\", data!({\"item\": item}))\n}\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/generate[views_rs]@Htmx_scaffold.snap",
    "content": "---\nsource: loco-gen/tests/templates/scaffold.rs\nexpression: \"fs::read_to_string(base_views_path.join(\\\"movie.rs\\\")).expect(\\\"Failed to read mod.rs\\\")\"\n---\nuse loco_rs::prelude::*;\n\nuse crate::models::_entities::movies;\n\n/// Render a list view of `movies`.\n///\n/// # Errors\n///\n/// When there is an issue with rendering the view.\npub fn list(v: &impl ViewRenderer, items: &Vec<movies::Model>) -> Result<Response> {\n    format::render().view(v, \"movie/list.html\", data!({\"items\": items}))\n}\n\n/// Render a single `movie` view.\n///\n/// # Errors\n///\n/// When there is an issue with rendering the view.\npub fn show(v: &impl ViewRenderer, item: &movies::Model) -> Result<Response> {\n    format::render().view(v, \"movie/show.html\", data!({\"item\": item}))\n}\n\n/// Render a `movie` create form.\n///\n/// # Errors\n///\n/// When there is an issue with rendering the view.\npub fn create(v: &impl ViewRenderer) -> Result<Response> {\n    format::render().view(v, \"movie/create.html\", data!({}))\n}\n\n/// Render a `movie` edit form.\n///\n/// # Errors\n///\n/// When there is an issue with rendering the view.\npub fn edit(v: &impl ViewRenderer, item: &movies::Model) -> Result<Response> {\n    format::render().view(v, \"movie/edit.html\", data!({\"item\": item}))\n}\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/generate_result@add_column_migration.snap",
    "content": "---\nsource: loco-gen/tests/templates/migration.rs\nexpression: collect_messages(&gen_result)\nsnapshot_kind: text\n---\n* Migration `add_name_and_age_to_users` added! You can now apply it with `$ cargo loco db migrate && cargo loco db entities`.\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/generate_result@add_reference_migration.snap",
    "content": "---\nsource: loco-gen/tests/templates/migration.rs\nexpression: collect_messages(&gen_result)\nsnapshot_kind: text\n---\n* Migration `add_user_ref_to_posts` added! You can now apply it with `$ cargo loco db migrate && cargo loco db entities`.\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/generate_result@create_join_table_migration.snap",
    "content": "---\nsource: loco-gen/tests/templates/migration.rs\nexpression: collect_messages(&gen_result)\nsnapshot_kind: text\n---\n* Migration for `CreateJoinTableUsersAndGroups` added! You can now apply it with `$ cargo loco db migrate && cargo loco db entities`.\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/generate_result@create_join_table_without_tz_migration.snap",
    "content": "---\nsource: loco-gen/tests/templates/migration.rs\nexpression: collect_messages(&gen_result)\nsnapshot_kind: text\n---\n* Migration for `CreateJoinTableUsersAndGroups` added! You can now apply it with `$ cargo loco db migrate && cargo loco db entities`.\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/generate_result@create_table_migration.snap",
    "content": "---\nsource: loco-gen/tests/templates/migration.rs\nexpression: collect_messages(&gen_result)\nsnapshot_kind: text\n---\n* Migration for `movies` added! You can now apply it with `$ cargo loco db migrate && cargo loco db entities`.\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/generate_result@create_table_without_tz_migration.snap",
    "content": "---\nsource: loco-gen/tests/templates/migration.rs\nexpression: collect_messages(&gen_result)\nsnapshot_kind: text\n---\n* Migration for `movies` added! You can now apply it with `$ cargo loco db migrate && cargo loco db entities`.\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/generate_result@empty_migration.snap",
    "content": "---\nsource: loco-gen/tests/templates/migration.rs\nexpression: collect_messages(&gen_result)\nsnapshot_kind: text\n---\n* Migration for `FixUsersTable` added! You can now apply it with `$ cargo loco db migrate && cargo loco db entities`.\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/generate_result@remove_columns_migration.snap",
    "content": "---\nsource: loco-gen/tests/templates/migration.rs\nexpression: collect_messages(&gen_result)\nsnapshot_kind: text\n---\n* Migration `remove_name_and_age_from_users` added! You can now apply it with `$ cargo loco db migrate && cargo loco db entities`.\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/generate_results@Api_controller.snap",
    "content": "---\nsource: loco-gen/tests/templates/controller.rs\nexpression: collect_messages(&gen_result)\n---\n* Controller `Movie` was added successfully.\n* Tests for controller `Movie` was added successfully. Run `cargo test`.\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/generate_results@Api_scaffold.snap",
    "content": "---\nsource: loco-gen/tests/templates/scaffold.rs\nexpression: collect_messages(&gen_result)\nsnapshot_kind: text\n---\n* Migration for `movie` added! You can now apply it with `$ cargo loco db migrate && cargo loco db entities`.\n* A test for model `Movies` was added. Run with `cargo test`.\n* Controller `Movie` was added successfully.\n* Tests for controller `Movie` was added successfully. Run `cargo test`.\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/generate_results@Html_controller.snap",
    "content": "---\nsource: loco-gen/tests/templates/controller.rs\nexpression: collect_messages(&gen_result)\n---\n* Controller `Movie` was added successfully.\n* movie/GET view was added successfully.\n* movie/POST view was added successfully.\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/generate_results@Html_scaffold.snap",
    "content": "---\nsource: loco-gen/tests/templates/scaffold.rs\nexpression: collect_messages(&gen_result)\nsnapshot_kind: text\n---\n* Migration for `movie` added! You can now apply it with `$ cargo loco db migrate && cargo loco db entities`.\n* A test for model `Movies` was added. Run with `cargo test`.\n* Base template was added successfully.\n* Controller `Movie` was added successfully.\n* movie view was added successfully.\n* movie create view was added successfully.\n* movie edit view was added successfully.\n* movie list view was added successfully.\n* movie view was added successfully.\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/generate_results@Htmx_controller.snap",
    "content": "---\nsource: loco-gen/tests/templates/controller.rs\nexpression: collect_messages(&gen_result)\n---\n* Controller `Movie` was added successfully.\n* movie/GET view was added successfully.\n* movie/POST view was added successfully.\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/generate_results@Htmx_scaffold.snap",
    "content": "---\nsource: loco-gen/tests/templates/scaffold.rs\nexpression: collect_messages(&gen_result)\nsnapshot_kind: text\n---\n* Migration for `movie` added! You can now apply it with `$ cargo loco db migrate && cargo loco db entities`.\n* A test for model `Movies` was added. Run with `cargo test`.\n* Base template was added successfully.\n* Controller `Movie` was added successfully.\n* movie view was added successfully.\n* movie create view was added successfully.\n* movie edit view was added successfully.\n* movie list view was added successfully.\n* movie view was added successfully.\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/inject[.config_toml]@deployment.snap",
    "content": "---\nsource: loco-gen/tests/templates/deployment.rs\nexpression: \"fs::read_to_string(tree_fs.root.join(\\\".cargo\\\").join(\\\"config.toml\\\")).expect(\\\".cargo/config.toml not exists\\\")\"\n---\n[alias]\nloco = \"run --\"\nloco-tool = \"run --\"\n\nplayground = \"run --example playground\"\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/inject[app_rs]@Api_controller.snap",
    "content": "---\nsource: loco-gen/tests/templates/controller.rs\nexpression: \"fs::read_to_string(tree_fs.root.join(\\\"src\\\").join(\\\"app.rs\\\")).expect(\\\"app.rs injection failed\\\")\"\n---\nimpl Hooks for App {\n    fn routes(_ctx: &AppContext) -> AppRoutes {\n        AppRoutes::with_default_routes() // controller routes below\n            .add_route(controllers::movie::routes())\n            .add_route(controllers::auth::routes())\n        }\n    }\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/inject[app_rs]@Api_scaffold.snap",
    "content": "---\nsource: loco-gen/tests/templates/scaffold.rs\nexpression: \"fs::read_to_string(tree_fs.root.join(\\\"src\\\").join(\\\"app.rs\\\")).expect(\\\"app.rs injection failed\\\")\"\n---\nimpl Hooks for App {\n    fn routes(_ctx: &AppContext) -> AppRoutes {\n        AppRoutes::with_default_routes() // controller routes below\n            .add_route(controllers::movie::routes())\n            .add_route(controllers::auth::routes())\n        }\n    }\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/inject[app_rs]@Html_controller.snap",
    "content": "---\nsource: loco-gen/tests/templates/controller.rs\nexpression: \"fs::read_to_string(tree_fs.root.join(\\\"src\\\").join(\\\"app.rs\\\")).expect(\\\"app.rs injection failed\\\")\"\n---\nimpl Hooks for App {\n    fn routes(_ctx: &AppContext) -> AppRoutes {\n        AppRoutes::with_default_routes() // controller routes below\n            .add_route(controllers::movie::routes())\n            .add_route(controllers::auth::routes())\n        }\n    }\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/inject[app_rs]@Html_scaffold.snap",
    "content": "---\nsource: loco-gen/tests/templates/scaffold.rs\nexpression: \"fs::read_to_string(tree_fs.root.join(\\\"src\\\").join(\\\"app.rs\\\")).expect(\\\"app.rs injection failed\\\")\"\n---\nimpl Hooks for App {\n    fn routes(_ctx: &AppContext) -> AppRoutes {\n        AppRoutes::with_default_routes() // controller routes below\n            .add_route(controllers::movie::routes())\n            .add_route(controllers::auth::routes())\n        }\n    }\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/inject[app_rs]@Htmx_controller.snap",
    "content": "---\nsource: loco-gen/tests/templates/controller.rs\nexpression: \"fs::read_to_string(tree_fs.root.join(\\\"src\\\").join(\\\"app.rs\\\")).expect(\\\"app.rs injection failed\\\")\"\n---\nimpl Hooks for App {\n    fn routes(_ctx: &AppContext) -> AppRoutes {\n        AppRoutes::with_default_routes() // controller routes below\n            .add_route(controllers::movie::routes())\n            .add_route(controllers::auth::routes())\n        }\n    }\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/inject[app_rs]@Htmx_scaffold.snap",
    "content": "---\nsource: loco-gen/tests/templates/scaffold.rs\nexpression: \"fs::read_to_string(tree_fs.root.join(\\\"src\\\").join(\\\"app.rs\\\")).expect(\\\"app.rs injection failed\\\")\"\n---\nimpl Hooks for App {\n    fn routes(_ctx: &AppContext) -> AppRoutes {\n        AppRoutes::with_default_routes() // controller routes below\n            .add_route(controllers::movie::routes())\n            .add_route(controllers::auth::routes())\n        }\n    }\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/inject[app_rs]@task.snap",
    "content": "---\nsource: loco-gen/tests/templates/task.rs\nexpression: \"fs::read_to_string(tree_fs.root.join(\\\"src\\\").join(\\\"app.rs\\\")).expect(\\\"Failed to read updated app file: app.rs\\\")\"\n---\nimpl Hooks for App {\n    #[allow(unused_variables)]\n    fn register_tasks(tasks: &mut Tasks) {\n        tasks.register(tasks::cleanup::Cleanup);\n        // tasks-inject (do not remove)\n    }\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/inject[app_rs]@worker.snap",
    "content": "---\nsource: loco-gen/tests/templates/worker.rs\nexpression: \"fs::read_to_string(tree_fs.root.join(\\\"src\\\").join(\\\"app.rs\\\")).expect(\\\"Failed to read updated app file: app.rs\\\")\"\n---\nasync fn connect_workers(ctx: &AppContext, queue: &Queue) -> Result<()> {\n        queue.register(crate::workers::register_email::Worker::build(ctx)).await?;\n    queue.register(DownloadWorker::build(ctx)).await?;\n        Ok(())\n    }\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/inject[controller_mod_rs]@Api_controller.snap",
    "content": "---\nsource: loco-gen/tests/templates/controller.rs\nexpression: \"fs::read_to_string(controllers_path.join(\\\"mod.rs\\\")).expect(\\\"mod.rs injection failed\\\")\"\n---\npub mod movie;\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/inject[controller_mod_rs]@Api_scaffold.snap",
    "content": "---\nsource: loco-gen/tests/templates/scaffold.rs\nexpression: \"fs::read_to_string(controllers_path.join(\\\"mod.rs\\\")).expect(\\\"mod.rs injection failed\\\")\"\n---\npub mod movie;\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/inject[controller_mod_rs]@Html_controller.snap",
    "content": "---\nsource: loco-gen/tests/templates/controller.rs\nexpression: \"fs::read_to_string(controllers_path.join(\\\"mod.rs\\\")).expect(\\\"mod.rs injection failed\\\")\"\n---\npub mod movie;\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/inject[controller_mod_rs]@Html_scaffold.snap",
    "content": "---\nsource: loco-gen/tests/templates/scaffold.rs\nexpression: \"fs::read_to_string(controllers_path.join(\\\"mod.rs\\\")).expect(\\\"mod.rs injection failed\\\")\"\n---\npub mod movie;\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/inject[controller_mod_rs]@Htmx_controller.snap",
    "content": "---\nsource: loco-gen/tests/templates/controller.rs\nexpression: \"fs::read_to_string(controllers_path.join(\\\"mod.rs\\\")).expect(\\\"mod.rs injection failed\\\")\"\n---\npub mod movie;\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/inject[controller_mod_rs]@Htmx_scaffold.snap",
    "content": "---\nsource: loco-gen/tests/templates/scaffold.rs\nexpression: \"fs::read_to_string(controllers_path.join(\\\"mod.rs\\\")).expect(\\\"mod.rs injection failed\\\")\"\n---\npub mod movie;\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/inject[mailer_mod_rs]@mailer.snap",
    "content": "---\nsource: loco-gen/tests/templates/mailer.rs\nexpression: \"fs::read_to_string(path).unwrap_or_else(|_| panic!(\\\"{name} missing\\\"))\"\n---\npub mod reset_password;\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/inject[migration_lib]@Api_scaffold.snap",
    "content": "---\nsource: loco-gen/tests/templates/scaffold.rs\nexpression: \"fs::read_to_string(migration_path.join(\\\"lib.rs\\\")).expect(\\\"Failed to read lib.rs\\\")\"\n---\n#![allow(elided_lifetimes_in_paths)]\n#![allow(clippy::wildcard_imports)]\npub use sea_orm_migration::prelude::*;\nmod m[TIME]_users;\n\nmod m[TIME]_movies;\npub struct Migrator;\n\n#[async_trait::async_trait]\nimpl MigratorTrait for Migrator {\n    fn migrations() -> Vec<Box<dyn MigrationTrait>> {\n        vec![\n            Box::new(m[TIME]_users::Migration),\n            Box::new(m[TIME]_movies::Migration),\n            // inject-above (do not remove this comment)\n        ]\n    }\n}\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/inject[migration_lib]@Html_scaffold.snap",
    "content": "---\nsource: loco-gen/tests/templates/scaffold.rs\nexpression: \"fs::read_to_string(migration_path.join(\\\"lib.rs\\\")).expect(\\\"Failed to read lib.rs\\\")\"\n---\n#![allow(elided_lifetimes_in_paths)]\n#![allow(clippy::wildcard_imports)]\npub use sea_orm_migration::prelude::*;\nmod m[TIME]_users;\n\nmod m[TIME]_movies;\npub struct Migrator;\n\n#[async_trait::async_trait]\nimpl MigratorTrait for Migrator {\n    fn migrations() -> Vec<Box<dyn MigrationTrait>> {\n        vec![\n            Box::new(m[TIME]_users::Migration),\n            Box::new(m[TIME]_movies::Migration),\n            // inject-above (do not remove this comment)\n        ]\n    }\n}\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/inject[migration_lib]@Htmx_scaffold.snap",
    "content": "---\nsource: loco-gen/tests/templates/scaffold.rs\nexpression: \"fs::read_to_string(migration_path.join(\\\"lib.rs\\\")).expect(\\\"Failed to read lib.rs\\\")\"\n---\n#![allow(elided_lifetimes_in_paths)]\n#![allow(clippy::wildcard_imports)]\npub use sea_orm_migration::prelude::*;\nmod m[TIME]_users;\n\nmod m[TIME]_movies;\npub struct Migrator;\n\n#[async_trait::async_trait]\nimpl MigratorTrait for Migrator {\n    fn migrations() -> Vec<Box<dyn MigrationTrait>> {\n        vec![\n            Box::new(m[TIME]_users::Migration),\n            Box::new(m[TIME]_movies::Migration),\n            // inject-above (do not remove this comment)\n        ]\n    }\n}\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/inject[migration_lib]@add_column_migration.snap",
    "content": "---\nsource: loco-gen/tests/templates/migration.rs\nexpression: \"fs::read_to_string(migration_path.join(\\\"lib.rs\\\")).expect(\\\"Failed to read lib.rs\\\")\"\n---\n#![allow(elided_lifetimes_in_paths)]\n#![allow(clippy::wildcard_imports)]\npub use sea_orm_migration::prelude::*;\nmod m[TIME]_users;\n\nmod m[TIME]_add_name_and_age_to_users;\npub struct Migrator;\n\n#[async_trait::async_trait]\nimpl MigratorTrait for Migrator {\n    fn migrations() -> Vec<Box<dyn MigrationTrait>> {\n        vec![\n            Box::new(m[TIME]_users::Migration),\n            Box::new(m[TIME]_add_name_and_age_to_users::Migration),\n            // inject-above (do not remove this comment)\n        ]\n    }\n}\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/inject[migration_lib]@add_reference_migration.snap",
    "content": "---\nsource: loco-gen/tests/templates/migration.rs\nexpression: \"fs::read_to_string(migration_path.join(\\\"lib.rs\\\")).expect(\\\"Failed to read lib.rs\\\")\"\n---\n#![allow(elided_lifetimes_in_paths)]\n#![allow(clippy::wildcard_imports)]\npub use sea_orm_migration::prelude::*;\nmod m[TIME]_users;\n\nmod m[TIME]_add_user_ref_to_posts;\npub struct Migrator;\n\n#[async_trait::async_trait]\nimpl MigratorTrait for Migrator {\n    fn migrations() -> Vec<Box<dyn MigrationTrait>> {\n        vec![\n            Box::new(m[TIME]_users::Migration),\n            Box::new(m[TIME]_add_user_ref_to_posts::Migration),\n            // inject-above (do not remove this comment)\n        ]\n    }\n}\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/inject[migration_lib]@create_join_table_migration.snap",
    "content": "---\nsource: loco-gen/tests/templates/migration.rs\nexpression: \"fs::read_to_string(migration_path.join(\\\"lib.rs\\\")).expect(\\\"Failed to read lib.rs\\\")\"\n---\n#![allow(elided_lifetimes_in_paths)]\n#![allow(clippy::wildcard_imports)]\npub use sea_orm_migration::prelude::*;\nmod m[TIME]_users;\n\nmod m[TIME]_create_join_table_users_and_groups;\npub struct Migrator;\n\n#[async_trait::async_trait]\nimpl MigratorTrait for Migrator {\n    fn migrations() -> Vec<Box<dyn MigrationTrait>> {\n        vec![\n            Box::new(m[TIME]_users::Migration),\n            Box::new(m[TIME]_create_join_table_users_and_groups::Migration),\n            // inject-above (do not remove this comment)\n        ]\n    }\n}\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/inject[migration_lib]@create_join_table_without_tz_migration.snap",
    "content": "---\nsource: loco-gen/tests/templates/migration.rs\nexpression: \"fs::read_to_string(migration_path.join(\\\"lib.rs\\\")).expect(\\\"Failed to read lib.rs\\\")\"\nsnapshot_kind: text\n---\n#![allow(elided_lifetimes_in_paths)]\n#![allow(clippy::wildcard_imports)]\npub use sea_orm_migration::prelude::*;\nmod m[TIME]_users;\n\nmod m[TIME]_create_join_table_users_and_groups;\npub struct Migrator;\n\n#[async_trait::async_trait]\nimpl MigratorTrait for Migrator {\n    fn migrations() -> Vec<Box<dyn MigrationTrait>> {\n        vec![\n            Box::new(m[TIME]_users::Migration),\n            Box::new(m[TIME]_create_join_table_users_and_groups::Migration),\n            // inject-above (do not remove this comment)\n        ]\n    }\n}\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/inject[migration_lib]@create_table_migration.snap",
    "content": "---\nsource: loco-gen/tests/templates/migration.rs\nexpression: \"fs::read_to_string(migration_path.join(\\\"lib.rs\\\")).expect(\\\"Failed to read lib.rs\\\")\"\n---\n#![allow(elided_lifetimes_in_paths)]\n#![allow(clippy::wildcard_imports)]\npub use sea_orm_migration::prelude::*;\nmod m[TIME]_users;\n\nmod m[TIME]_movies;\npub struct Migrator;\n\n#[async_trait::async_trait]\nimpl MigratorTrait for Migrator {\n    fn migrations() -> Vec<Box<dyn MigrationTrait>> {\n        vec![\n            Box::new(m[TIME]_users::Migration),\n            Box::new(m[TIME]_movies::Migration),\n            // inject-above (do not remove this comment)\n        ]\n    }\n}\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/inject[migration_lib]@create_table_without_tz_migration.snap",
    "content": "---\nsource: loco-gen/tests/templates/migration.rs\nexpression: \"fs::read_to_string(migration_path.join(\\\"lib.rs\\\")).expect(\\\"Failed to read lib.rs\\\")\"\nsnapshot_kind: text\n---\n#![allow(elided_lifetimes_in_paths)]\n#![allow(clippy::wildcard_imports)]\npub use sea_orm_migration::prelude::*;\nmod m[TIME]_users;\n\nmod m[TIME]_movies;\npub struct Migrator;\n\n#[async_trait::async_trait]\nimpl MigratorTrait for Migrator {\n    fn migrations() -> Vec<Box<dyn MigrationTrait>> {\n        vec![\n            Box::new(m[TIME]_users::Migration),\n            Box::new(m[TIME]_movies::Migration),\n            // inject-above (do not remove this comment)\n        ]\n    }\n}\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/inject[migration_lib]@empty_migration.snap",
    "content": "---\nsource: loco-gen/tests/templates/migration.rs\nexpression: \"fs::read_to_string(migration_path.join(\\\"lib.rs\\\")).expect(\\\"Failed to read lib.rs\\\")\"\n---\n#![allow(elided_lifetimes_in_paths)]\n#![allow(clippy::wildcard_imports)]\npub use sea_orm_migration::prelude::*;\nmod m[TIME]_users;\n\nmod m[TIME]_fix_users_table;\npub struct Migrator;\n\n#[async_trait::async_trait]\nimpl MigratorTrait for Migrator {\n    fn migrations() -> Vec<Box<dyn MigrationTrait>> {\n        vec![\n            Box::new(m[TIME]_users::Migration),\n            Box::new(m[TIME]_fix_users_table::Migration),\n            // inject-above (do not remove this comment)\n        ]\n    }\n}\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/inject[migration_lib]@model.snap",
    "content": "---\nsource: loco-gen/tests/templates/model.rs\nexpression: \"fs::read_to_string(migration_path.join(\\\"lib.rs\\\")).expect(\\\"Failed to read lib.rs\\\")\"\n---\n#![allow(elided_lifetimes_in_paths)]\n#![allow(clippy::wildcard_imports)]\npub use sea_orm_migration::prelude::*;\nmod m[TIME]_users;\n\nmod m[TIME]_movies;\npub struct Migrator;\n\n#[async_trait::async_trait]\nimpl MigratorTrait for Migrator {\n    fn migrations() -> Vec<Box<dyn MigrationTrait>> {\n        vec![\n            Box::new(m[TIME]_users::Migration),\n            Box::new(m[TIME]_movies::Migration),\n            // inject-above (do not remove this comment)\n        ]\n    }\n}\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/inject[migration_lib]@remove_columns_migration.snap",
    "content": "---\nsource: loco-gen/tests/templates/migration.rs\nexpression: \"fs::read_to_string(migration_path.join(\\\"lib.rs\\\")).expect(\\\"Failed to read lib.rs\\\")\"\n---\n#![allow(elided_lifetimes_in_paths)]\n#![allow(clippy::wildcard_imports)]\npub use sea_orm_migration::prelude::*;\nmod m[TIME]_users;\n\nmod m[TIME]_remove_name_and_age_from_users;\npub struct Migrator;\n\n#[async_trait::async_trait]\nimpl MigratorTrait for Migrator {\n    fn migrations() -> Vec<Box<dyn MigrationTrait>> {\n        vec![\n            Box::new(m[TIME]_users::Migration),\n            Box::new(m[TIME]_remove_name_and_age_from_users::Migration),\n            // inject-above (do not remove this comment)\n        ]\n    }\n}\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/inject[task_mod_rs]@task.snap",
    "content": "---\nsource: loco-gen/tests/templates/task.rs\nexpression: \"fs::read_to_string(task_path.join(\\\"mod.rs\\\")).expect(\\\"Failed to read updated task mod file: mod.rs\\\")\"\n---\npub mod cleanup;\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/inject[test_mod]@Api_scaffold.snap",
    "content": "---\nsource: loco-gen/tests/templates/scaffold.rs\nexpression: \"fs::read_to_string(tests_path.join(\\\"mod.rs\\\")).expect(\\\"Failed to read mod.rs\\\")\"\n---\nmod movies;\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/inject[test_mod]@Html_scaffold.snap",
    "content": "---\nsource: loco-gen/tests/templates/scaffold.rs\nexpression: \"fs::read_to_string(tests_path.join(\\\"mod.rs\\\")).expect(\\\"Failed to read mod.rs\\\")\"\n---\nmod movies;\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/inject[test_mod]@Htmx_scaffold.snap",
    "content": "---\nsource: loco-gen/tests/templates/scaffold.rs\nexpression: \"fs::read_to_string(tests_path.join(\\\"mod.rs\\\")).expect(\\\"Failed to read mod.rs\\\")\"\n---\nmod movies;\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/inject[test_mod]@model.snap",
    "content": "---\nsource: loco-gen/tests/templates/model.rs\nexpression: \"fs::read_to_string(tests_path.join(\\\"mod.rs\\\")).expect(\\\"Failed to read mod.rs\\\")\"\n---\nmod movies;\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/inject[tests_controller_mod_rs]@Api_controller.snap",
    "content": "---\nsource: loco-gen/tests/templates/controller.rs\nexpression: \"fs::read_to_string(test_controllers_path.join(\\\"mod.rs\\\")).expect(\\\"test mod.rs missing\\\")\"\n---\npub mod movie;\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/inject[tests_task_mod]@task.snap",
    "content": "---\nsource: loco-gen/tests/templates/task.rs\nexpression: \"fs::read_to_string(tests_task_path.join(\\\"mod.rs\\\")).expect(\\\"Failed to read updated tests task mod file: mod.rs\\\")\"\n---\npub mod cleanup;\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/inject[tests_worker_mod]@worker.snap",
    "content": "---\nsource: loco-gen/tests/templates/worker.rs\nexpression: \"fs::read_to_string(tests_worker_path.join(\\\"mod.rs\\\")).expect(\\\"Failed to read updated tests worker mod file: mod.rs\\\")\"\n---\npub mod register_email;\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/inject[views_[GET]]@Html_controller.snap",
    "content": "---\nsource: loco-gen/tests/templates/controller.rs\nexpression: \"fs::read_to_string(tree_fs.root.join(\\\"assets\\\").join(\\\"views\\\").join(\\\"movie\\\").join(format!(\\\"{}.html\\\",\\naction.to_uppercase()))).expect(\\\"view file missing\\\")\"\n---\n<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n    <script src=\"https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio,line-clamp\"></script>\n</head>\n\n<body class=\"prose p-10\">\n    <h1>View GET</h1>\n    Find me in <code>movie/GET</code>\n</body>\n\n</html>\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/inject[views_[GET]]@Htmx_controller.snap",
    "content": "---\nsource: loco-gen/tests/templates/controller.rs\nexpression: \"fs::read_to_string(tree_fs.root.join(\\\"assets\\\").join(\\\"views\\\").join(\\\"movie\\\").join(format!(\\\"{}.html\\\",\\naction.to_uppercase()))).expect(\\\"view file missing\\\")\"\n---\n<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n    <script src=\"https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio,line-clamp\"></script>\n</head>\n\n<body class=\"prose p-10\">\n    <h1>View GET</h1>\n    Find me in <code>movie/GET</code>\n</body>\n\n</html>\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/inject[views_[POST]]@Html_controller.snap",
    "content": "---\nsource: loco-gen/tests/templates/controller.rs\nexpression: \"fs::read_to_string(tree_fs.root.join(\\\"assets\\\").join(\\\"views\\\").join(\\\"movie\\\").join(format!(\\\"{}.html\\\",\\naction.to_uppercase()))).expect(\\\"view file missing\\\")\"\n---\n<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n    <script src=\"https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio,line-clamp\"></script>\n</head>\n\n<body class=\"prose p-10\">\n    <h1>View POST</h1>\n    Find me in <code>movie/POST</code>\n</body>\n\n</html>\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/inject[views_[POST]]@Htmx_controller.snap",
    "content": "---\nsource: loco-gen/tests/templates/controller.rs\nexpression: \"fs::read_to_string(tree_fs.root.join(\\\"assets\\\").join(\\\"views\\\").join(\\\"movie\\\").join(format!(\\\"{}.html\\\",\\naction.to_uppercase()))).expect(\\\"view file missing\\\")\"\n---\n<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n    <script src=\"https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio,line-clamp\"></script>\n</head>\n\n<body class=\"prose p-10\">\n    <h1>View POST</h1>\n    Find me in <code>movie/POST</code>\n</body>\n\n</html>\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/inject[views_mod_rs]@Html_scaffold.snap",
    "content": "---\nsource: loco-gen/tests/templates/scaffold.rs\nexpression: \"fs::read_to_string(base_views_path.join(\\\"mod.rs\\\")).expect(\\\"Failed to read mod.rs\\\")\"\n---\npub mod movie;\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/inject[views_mod_rs]@Htmx_scaffold.snap",
    "content": "---\nsource: loco-gen/tests/templates/scaffold.rs\nexpression: \"fs::read_to_string(base_views_path.join(\\\"mod.rs\\\")).expect(\\\"Failed to read mod.rs\\\")\"\n---\npub mod movie;\n"
  },
  {
    "path": "loco-gen/tests/templates/snapshots/inject[worker_mod_rs]@worker.snap",
    "content": "---\nsource: loco-gen/tests/templates/worker.rs\nexpression: \"fs::read_to_string(worker_path.join(\\\"mod.rs\\\")).expect(\\\"Failed to read updated worker mod file: mod.rs\\\")\"\n---\npub mod register_email;\n"
  },
  {
    "path": "loco-gen/tests/templates/task.rs",
    "content": "use super::utils::APP_TASK;\nuse insta::assert_snapshot;\nuse loco_gen::{collect_messages, generate, AppInfo, Component};\nuse rrgen::RRgen;\nuse std::fs;\n\nmacro_rules! configure_insta {\n    () => {\n        let mut settings = insta::Settings::clone_current();\n        settings.set_prepend_module_to_snapshot(false);\n        settings.set_snapshot_suffix(\"task\");\n        let _guard = settings.bind_to_scope();\n    };\n}\n\n#[test]\nfn can_generate() {\n    configure_insta!();\n\n    let component = Component::Task {\n        name: \"cleanup\".to_string(),\n    };\n\n    let tree_fs = tree_fs::TreeBuilder::default()\n        .drop(true)\n        .add_empty(\"src/tasks/mod.rs\")\n        .add_empty(\"tests/requests/mod.rs\")\n        .add_empty(\"tests/tasks/mod.rs\")\n        .add(\"src/app.rs\", APP_TASK)\n        .create()\n        .expect(\"Failed to create tree_fs structure\");\n\n    let rrgen = RRgen::with_working_dir(&tree_fs.root);\n\n    let gen_result = generate(\n        &rrgen,\n        component,\n        &AppInfo {\n            app_name: \"tester\".to_string(),\n        },\n    )\n    .expect(\"Failed to generate components\");\n\n    assert_eq!(\n        collect_messages(&gen_result),\n        r\"* A Task `Cleanup` was added successfully. Run with `cargo run task cleanup`.\n* Tests for task `Cleanup` was added successfully. Run `cargo test`.\n\"\n    );\n\n    let task_path = tree_fs.root.join(\"src\").join(\"tasks\");\n    assert_snapshot!(\n        \"generate[controller_file]\",\n        fs::read_to_string(task_path.join(\"cleanup.rs\"))\n            .expect(\"Failed to read generated task file: cleanup.rs\")\n    );\n    assert_snapshot!(\n        \"inject[task_mod_rs]\",\n        fs::read_to_string(task_path.join(\"mod.rs\"))\n            .expect(\"Failed to read updated task mod file: mod.rs\")\n    );\n    assert_snapshot!(\n        \"inject[app_rs]\",\n        fs::read_to_string(tree_fs.root.join(\"src\").join(\"app.rs\"))\n            .expect(\"Failed to read updated app file: app.rs\")\n    );\n\n    // Assertions for test files\n    let tests_task_path = tree_fs.root.join(\"tests\").join(\"tasks\");\n    assert_snapshot!(\n        \"generate[tests_task_file]\",\n        fs::read_to_string(tests_task_path.join(\"cleanup.rs\"))\n            .expect(\"Failed to read generated tests task file: cleanup.rs\")\n    );\n    assert_snapshot!(\n        \"inject[tests_task_mod]\",\n        fs::read_to_string(tests_task_path.join(\"mod.rs\"))\n            .expect(\"Failed to read updated tests task mod file: mod.rs\")\n    );\n}\n"
  },
  {
    "path": "loco-gen/tests/templates/utils.rs",
    "content": "use chrono::{Duration, Utc};\nuse std::path::{Path, PathBuf};\n\npub const MIGRATION_SRC_LIB: &str = r\"\n#![allow(elided_lifetimes_in_paths)]\n#![allow(clippy::wildcard_imports)]\npub use sea_orm_migration::prelude::*;\nmod m20220101_000001_users;\n\npub struct Migrator;\n\n#[async_trait::async_trait]\nimpl MigratorTrait for Migrator {\n    fn migrations() -> Vec<Box<dyn MigrationTrait>> {\n        vec![\n            Box::new(m20220101_000001_users::Migration),\n            // inject-above (do not remove this comment)\n        ]\n    }\n}\n        \";\n\npub const APP_ROUTS: &str = r\"\nimpl Hooks for App {\n    fn routes(_ctx: &AppContext) -> AppRoutes {\n        AppRoutes::with_default_routes() // controller routes below\n            .add_route(controllers::auth::routes())\n        }\n    }\n\";\n\npub const APP_TASK: &str = r\"\nimpl Hooks for App {\n    #[allow(unused_variables)]\n    fn register_tasks(tasks: &mut Tasks) {\n        // tasks-inject (do not remove)\n    }\n\";\n\npub const APP_WORKER: &str = r\"\nasync fn connect_workers(ctx: &AppContext, queue: &Queue) -> Result<()> {\n    queue.register(DownloadWorker::build(ctx)).await?;\n        Ok(())\n    }\n\";\n\npub fn guess_file_by_time(path: &Path, file_format: &str, max_attempts: u32) -> Option<PathBuf> {\n    let now = Utc::now();\n\n    for seconds_to_subtract in 0..=max_attempts {\n        let guessed_time = now - Duration::seconds(i64::from(seconds_to_subtract));\n        let formatted_time = guessed_time.format(\"%Y%m%d_%H%M%S\").to_string();\n        let file_name = file_format.replace(\"{TIME}\", &formatted_time);\n\n        let file_path = path.join(file_name);\n        if file_path.exists() {\n            return Some(file_path);\n        }\n    }\n\n    None\n}\n"
  },
  {
    "path": "loco-gen/tests/templates/worker.rs",
    "content": "use super::utils::APP_WORKER;\nuse insta::assert_snapshot;\nuse loco_gen::{collect_messages, generate, AppInfo, Component};\nuse rrgen::RRgen;\nuse std::fs;\n\nmacro_rules! configure_insta {\n    () => {\n        let mut settings = insta::Settings::clone_current();\n        settings.set_prepend_module_to_snapshot(false);\n        settings.set_snapshot_suffix(\"worker\");\n        let _guard = settings.bind_to_scope();\n    };\n}\n\n#[test]\nfn can_generate() {\n    configure_insta!();\n\n    let component = Component::Worker {\n        name: \"register_email\".to_string(),\n    };\n\n    let tree_fs = tree_fs::TreeBuilder::default()\n        .drop(true)\n        .add_empty(\"src/workers/mod.rs\")\n        .add_empty(\"tests/workers/mod.rs\")\n        .add(\"src/app.rs\", APP_WORKER)\n        .create()\n        .expect(\"Failed to create tree_fs structure\");\n\n    let rrgen = RRgen::with_working_dir(&tree_fs.root);\n\n    let gen_result = generate(\n        &rrgen,\n        component,\n        &AppInfo {\n            app_name: \"tester\".to_string(),\n        },\n    )\n    .expect(\"Failed to generate components\");\n\n    assert_eq!(\n        collect_messages(&gen_result),\n        r\"* Test for worker `RegisterEmail` was added successfully. Run `cargo test`.\n* A worker `RegisterEmail` was added successfully. Run with `cargo run start --worker`.\n\"\n    );\n\n    // Assertions for generated files\n    let worker_path = tree_fs.root.join(\"src\").join(\"workers\");\n    assert_snapshot!(\n        \"generate[controller_file]\",\n        fs::read_to_string(worker_path.join(\"register_email.rs\"))\n            .expect(\"Failed to read generated worker file: register_email.rs\")\n    );\n    assert_snapshot!(\n        \"inject[worker_mod_rs]\",\n        fs::read_to_string(worker_path.join(\"mod.rs\"))\n            .expect(\"Failed to read updated worker mod file: mod.rs\")\n    );\n    assert_snapshot!(\n        \"inject[app_rs]\",\n        fs::read_to_string(tree_fs.root.join(\"src\").join(\"app.rs\"))\n            .expect(\"Failed to read updated app file: app.rs\")\n    );\n\n    // Assertions for test files\n    let tests_worker_path = tree_fs.root.join(\"tests\").join(\"workers\");\n    assert_snapshot!(\n        \"generate[tests_worker_file]\",\n        fs::read_to_string(tests_worker_path.join(\"register_email.rs\"))\n            .expect(\"Failed to read generated tests worker file: register_email.rs\")\n    );\n    assert_snapshot!(\n        \"inject[tests_worker_mod]\",\n        fs::read_to_string(tests_worker_path.join(\"mod.rs\"))\n            .expect(\"Failed to read updated tests worker mod file: mod.rs\")\n    );\n}\n"
  },
  {
    "path": "loco-new/.gitignore",
    "content": "!Cargo.lock\n"
  },
  {
    "path": "loco-new/Cargo.lock",
    "content": "# This file is automatically @generated by Cargo.\n# It is not intended for manual editing.\nversion = 4\n\n[[package]]\nname = \"ahash\"\nversion = \"0.8.12\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75\"\ndependencies = [\n \"cfg-if\",\n \"const-random\",\n \"getrandom 0.3.4\",\n \"once_cell\",\n \"version_check\",\n \"zerocopy\",\n]\n\n[[package]]\nname = \"aho-corasick\"\nversion = \"1.1.4\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301\"\ndependencies = [\n \"memchr\",\n]\n\n[[package]]\nname = \"android_system_properties\"\nversion = \"0.1.5\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311\"\ndependencies = [\n \"libc\",\n]\n\n[[package]]\nname = \"anstream\"\nversion = \"0.6.21\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a\"\ndependencies = [\n \"anstyle\",\n \"anstyle-parse\",\n \"anstyle-query\",\n \"anstyle-wincon\",\n \"colorchoice\",\n \"is_terminal_polyfill\",\n \"utf8parse\",\n]\n\n[[package]]\nname = \"anstyle\"\nversion = \"1.0.13\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78\"\n\n[[package]]\nname = \"anstyle-parse\"\nversion = \"0.2.7\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2\"\ndependencies = [\n \"utf8parse\",\n]\n\n[[package]]\nname = \"anstyle-query\"\nversion = \"1.1.5\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc\"\ndependencies = [\n \"windows-sys 0.61.2\",\n]\n\n[[package]]\nname = \"anstyle-wincon\"\nversion = \"3.0.11\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d\"\ndependencies = [\n \"anstyle\",\n \"once_cell_polyfill\",\n \"windows-sys 0.61.2\",\n]\n\n[[package]]\nname = \"autocfg\"\nversion = \"1.5.0\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8\"\n\n[[package]]\nname = \"bitflags\"\nversion = \"2.10.0\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3\"\n\n[[package]]\nname = \"block-buffer\"\nversion = \"0.10.4\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71\"\ndependencies = [\n \"generic-array\",\n]\n\n[[package]]\nname = \"bstr\"\nversion = \"1.12.1\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab\"\ndependencies = [\n \"memchr\",\n \"serde\",\n]\n\n[[package]]\nname = \"bumpalo\"\nversion = \"3.19.1\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510\"\n\n[[package]]\nname = \"cc\"\nversion = \"1.2.49\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215\"\ndependencies = [\n \"find-msvc-tools\",\n \"shlex\",\n]\n\n[[package]]\nname = \"cfg-if\"\nversion = \"1.0.4\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801\"\n\n[[package]]\nname = \"chrono\"\nversion = \"0.4.42\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2\"\ndependencies = [\n \"iana-time-zone\",\n \"num-traits\",\n \"windows-link\",\n]\n\n[[package]]\nname = \"chrono-tz\"\nversion = \"0.9.0\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"93698b29de5e97ad0ae26447b344c482a7284c737d9ddc5f9e52b74a336671bb\"\ndependencies = [\n \"chrono\",\n \"chrono-tz-build\",\n \"phf\",\n]\n\n[[package]]\nname = \"chrono-tz-build\"\nversion = \"0.3.0\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"0c088aee841df9c3041febbb73934cfc39708749bf96dc827e3359cd39ef11b1\"\ndependencies = [\n \"parse-zoneinfo\",\n \"phf\",\n \"phf_codegen\",\n]\n\n[[package]]\nname = \"clap\"\nversion = \"4.5.53\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8\"\ndependencies = [\n \"clap_builder\",\n \"clap_derive\",\n]\n\n[[package]]\nname = \"clap_builder\"\nversion = \"4.5.53\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00\"\ndependencies = [\n \"anstream\",\n \"anstyle\",\n \"clap_lex\",\n \"strsim\",\n]\n\n[[package]]\nname = \"clap_derive\"\nversion = \"4.5.49\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671\"\ndependencies = [\n \"heck\",\n \"proc-macro2\",\n \"quote\",\n \"syn\",\n]\n\n[[package]]\nname = \"clap_lex\"\nversion = \"0.7.6\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d\"\n\n[[package]]\nname = \"colorchoice\"\nversion = \"1.0.4\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75\"\n\n[[package]]\nname = \"colored\"\nversion = \"2.2.0\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c\"\ndependencies = [\n \"lazy_static\",\n \"windows-sys 0.59.0\",\n]\n\n[[package]]\nname = \"console\"\nversion = \"0.15.11\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8\"\ndependencies = [\n \"encode_unicode\",\n \"libc\",\n \"once_cell\",\n \"unicode-width\",\n \"windows-sys 0.59.0\",\n]\n\n[[package]]\nname = \"const-random\"\nversion = \"0.1.18\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359\"\ndependencies = [\n \"const-random-macro\",\n]\n\n[[package]]\nname = \"const-random-macro\"\nversion = \"0.1.16\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e\"\ndependencies = [\n \"getrandom 0.2.16\",\n \"once_cell\",\n \"tiny-keccak\",\n]\n\n[[package]]\nname = \"core-foundation-sys\"\nversion = \"0.8.7\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b\"\n\n[[package]]\nname = \"cpufeatures\"\nversion = \"0.2.17\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280\"\ndependencies = [\n \"libc\",\n]\n\n[[package]]\nname = \"crossbeam-deque\"\nversion = \"0.8.6\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51\"\ndependencies = [\n \"crossbeam-epoch\",\n \"crossbeam-utils\",\n]\n\n[[package]]\nname = \"crossbeam-epoch\"\nversion = \"0.9.18\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e\"\ndependencies = [\n \"crossbeam-utils\",\n]\n\n[[package]]\nname = \"crossbeam-utils\"\nversion = \"0.8.21\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28\"\n\n[[package]]\nname = \"crunchy\"\nversion = \"0.2.4\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5\"\n\n[[package]]\nname = \"crypto-common\"\nversion = \"0.1.7\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a\"\ndependencies = [\n \"generic-array\",\n \"typenum\",\n]\n\n[[package]]\nname = \"deunicode\"\nversion = \"1.6.2\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04\"\n\n[[package]]\nname = \"dialoguer\"\nversion = \"0.11.0\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de\"\ndependencies = [\n \"console\",\n \"shell-words\",\n \"tempfile\",\n \"thiserror\",\n \"zeroize\",\n]\n\n[[package]]\nname = \"digest\"\nversion = \"0.10.7\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292\"\ndependencies = [\n \"block-buffer\",\n \"crypto-common\",\n]\n\n[[package]]\nname = \"downcast\"\nversion = \"0.11.0\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1\"\n\n[[package]]\nname = \"duct\"\nversion = \"0.13.7\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"e4ab5718d1224b63252cd0c6f74f6480f9ffeb117438a2e0f5cf6d9a4798929c\"\ndependencies = [\n \"libc\",\n \"once_cell\",\n \"os_pipe\",\n \"shared_child\",\n]\n\n[[package]]\nname = \"encode_unicode\"\nversion = \"1.0.0\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0\"\n\n[[package]]\nname = \"equivalent\"\nversion = \"1.0.2\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f\"\n\n[[package]]\nname = \"errno\"\nversion = \"0.3.14\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb\"\ndependencies = [\n \"libc\",\n \"windows-sys 0.61.2\",\n]\n\n[[package]]\nname = \"fastrand\"\nversion = \"2.3.0\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be\"\n\n[[package]]\nname = \"find-msvc-tools\"\nversion = \"0.1.5\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844\"\n\n[[package]]\nname = \"fragile\"\nversion = \"2.0.1\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"28dd6caf6059519a65843af8fe2a3ae298b14b80179855aeb4adc2c1934ee619\"\n\n[[package]]\nname = \"fs_extra\"\nversion = \"1.3.0\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c\"\n\n[[package]]\nname = \"futures\"\nversion = \"0.3.31\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876\"\ndependencies = [\n \"futures-channel\",\n \"futures-core\",\n \"futures-executor\",\n \"futures-io\",\n \"futures-sink\",\n \"futures-task\",\n \"futures-util\",\n]\n\n[[package]]\nname = \"futures-channel\"\nversion = \"0.3.31\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10\"\ndependencies = [\n \"futures-core\",\n \"futures-sink\",\n]\n\n[[package]]\nname = \"futures-core\"\nversion = \"0.3.31\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e\"\n\n[[package]]\nname = \"futures-executor\"\nversion = \"0.3.31\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f\"\ndependencies = [\n \"futures-core\",\n \"futures-task\",\n \"futures-util\",\n]\n\n[[package]]\nname = \"futures-io\"\nversion = \"0.3.31\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6\"\n\n[[package]]\nname = \"futures-macro\"\nversion = \"0.3.31\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650\"\ndependencies = [\n \"proc-macro2\",\n \"quote\",\n \"syn\",\n]\n\n[[package]]\nname = \"futures-sink\"\nversion = \"0.3.31\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7\"\n\n[[package]]\nname = \"futures-task\"\nversion = \"0.3.31\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988\"\n\n[[package]]\nname = \"futures-timer\"\nversion = \"3.0.3\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24\"\n\n[[package]]\nname = \"futures-util\"\nversion = \"0.3.31\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81\"\ndependencies = [\n \"futures-channel\",\n \"futures-core\",\n \"futures-io\",\n \"futures-macro\",\n \"futures-sink\",\n \"futures-task\",\n \"memchr\",\n \"pin-project-lite\",\n \"pin-utils\",\n \"slab\",\n]\n\n[[package]]\nname = \"generic-array\"\nversion = \"0.14.7\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a\"\ndependencies = [\n \"typenum\",\n \"version_check\",\n]\n\n[[package]]\nname = \"getrandom\"\nversion = \"0.2.16\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592\"\ndependencies = [\n \"cfg-if\",\n \"libc\",\n \"wasi\",\n]\n\n[[package]]\nname = \"getrandom\"\nversion = \"0.3.4\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd\"\ndependencies = [\n \"cfg-if\",\n \"libc\",\n \"r-efi\",\n \"wasip2\",\n]\n\n[[package]]\nname = \"glob\"\nversion = \"0.3.3\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280\"\n\n[[package]]\nname = \"globset\"\nversion = \"0.4.18\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3\"\ndependencies = [\n \"aho-corasick\",\n \"bstr\",\n \"log\",\n \"regex-automata\",\n \"regex-syntax\",\n]\n\n[[package]]\nname = \"globwalk\"\nversion = \"0.9.1\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757\"\ndependencies = [\n \"bitflags\",\n \"ignore\",\n \"walkdir\",\n]\n\n[[package]]\nname = \"hashbrown\"\nversion = \"0.16.1\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100\"\n\n[[package]]\nname = \"heck\"\nversion = \"0.5.0\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea\"\n\n[[package]]\nname = \"humansize\"\nversion = \"2.1.3\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7\"\ndependencies = [\n \"libm\",\n]\n\n[[package]]\nname = \"iana-time-zone\"\nversion = \"0.1.64\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb\"\ndependencies = [\n \"android_system_properties\",\n \"core-foundation-sys\",\n \"iana-time-zone-haiku\",\n \"js-sys\",\n \"log\",\n \"wasm-bindgen\",\n \"windows-core\",\n]\n\n[[package]]\nname = \"iana-time-zone-haiku\"\nversion = \"0.1.2\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f\"\ndependencies = [\n \"cc\",\n]\n\n[[package]]\nname = \"ignore\"\nversion = \"0.4.25\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a\"\ndependencies = [\n \"crossbeam-deque\",\n \"globset\",\n \"log\",\n \"memchr\",\n \"regex-automata\",\n \"same-file\",\n \"walkdir\",\n \"winapi-util\",\n]\n\n[[package]]\nname = \"include_dir\"\nversion = \"0.7.4\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd\"\ndependencies = [\n \"include_dir_macros\",\n]\n\n[[package]]\nname = \"include_dir_macros\"\nversion = \"0.7.4\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75\"\ndependencies = [\n \"proc-macro2\",\n \"quote\",\n]\n\n[[package]]\nname = \"indexmap\"\nversion = \"2.12.1\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2\"\ndependencies = [\n \"equivalent\",\n \"hashbrown\",\n]\n\n[[package]]\nname = \"insta\"\nversion = \"1.44.3\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"b5c943d4415edd8153251b6f197de5eb1640e56d84e8d9159bea190421c73698\"\ndependencies = [\n \"console\",\n \"once_cell\",\n \"pest\",\n \"pest_derive\",\n \"regex\",\n \"serde\",\n \"similar\",\n]\n\n[[package]]\nname = \"instant\"\nversion = \"0.1.13\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222\"\ndependencies = [\n \"cfg-if\",\n]\n\n[[package]]\nname = \"is_terminal_polyfill\"\nversion = \"1.70.2\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695\"\n\n[[package]]\nname = \"itoa\"\nversion = \"1.0.15\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c\"\n\n[[package]]\nname = \"js-sys\"\nversion = \"0.3.83\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8\"\ndependencies = [\n \"once_cell\",\n \"wasm-bindgen\",\n]\n\n[[package]]\nname = \"lazy_static\"\nversion = \"1.5.0\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe\"\n\n[[package]]\nname = \"libc\"\nversion = \"0.2.178\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091\"\n\n[[package]]\nname = \"libm\"\nversion = \"0.2.15\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de\"\n\n[[package]]\nname = \"linux-raw-sys\"\nversion = \"0.11.0\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039\"\n\n[[package]]\nname = \"loco\"\nversion = \"0.16.3\"\ndependencies = [\n \"clap\",\n \"colored\",\n \"dialoguer\",\n \"duct\",\n \"fs_extra\",\n \"heck\",\n \"include_dir\",\n \"insta\",\n \"mockall\",\n \"rand 0.8.5\",\n \"regex\",\n \"rhai\",\n \"rstest\",\n \"serde\",\n \"serde_yaml\",\n \"strum\",\n \"tera\",\n \"thiserror\",\n \"toml\",\n \"tracing\",\n \"tracing-subscriber\",\n \"tree-fs\",\n \"unicode-xid\",\n \"uuid\",\n \"walkdir\",\n]\n\n[[package]]\nname = \"log\"\nversion = \"0.4.29\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897\"\n\n[[package]]\nname = \"matchers\"\nversion = \"0.2.0\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9\"\ndependencies = [\n \"regex-automata\",\n]\n\n[[package]]\nname = \"memchr\"\nversion = \"2.7.6\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273\"\n\n[[package]]\nname = \"mockall\"\nversion = \"0.13.1\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"39a6bfcc6c8c7eed5ee98b9c3e33adc726054389233e201c95dab2d41a3839d2\"\ndependencies = [\n \"cfg-if\",\n \"downcast\",\n \"fragile\",\n \"mockall_derive\",\n \"predicates\",\n \"predicates-tree\",\n]\n\n[[package]]\nname = \"mockall_derive\"\nversion = \"0.13.1\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"25ca3004c2efe9011bd4e461bd8256445052b9615405b4f7ea43fc8ca5c20898\"\ndependencies = [\n \"cfg-if\",\n \"proc-macro2\",\n \"quote\",\n \"syn\",\n]\n\n[[package]]\nname = \"nu-ansi-term\"\nversion = \"0.50.3\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5\"\ndependencies = [\n \"windows-sys 0.61.2\",\n]\n\n[[package]]\nname = \"num-traits\"\nversion = \"0.2.19\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841\"\ndependencies = [\n \"autocfg\",\n]\n\n[[package]]\nname = \"once_cell\"\nversion = \"1.21.3\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d\"\ndependencies = [\n \"portable-atomic\",\n]\n\n[[package]]\nname = \"once_cell_polyfill\"\nversion = \"1.70.2\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe\"\n\n[[package]]\nname = \"os_pipe\"\nversion = \"1.2.3\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967\"\ndependencies = [\n \"libc\",\n \"windows-sys 0.61.2\",\n]\n\n[[package]]\nname = \"parse-zoneinfo\"\nversion = \"0.3.1\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24\"\ndependencies = [\n \"regex\",\n]\n\n[[package]]\nname = \"percent-encoding\"\nversion = \"2.3.2\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220\"\n\n[[package]]\nname = \"pest\"\nversion = \"2.8.4\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"cbcfd20a6d4eeba40179f05735784ad32bdaef05ce8e8af05f180d45bb3e7e22\"\ndependencies = [\n \"memchr\",\n \"ucd-trie\",\n]\n\n[[package]]\nname = \"pest_derive\"\nversion = \"2.8.4\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"51f72981ade67b1ca6adc26ec221be9f463f2b5839c7508998daa17c23d94d7f\"\ndependencies = [\n \"pest\",\n \"pest_generator\",\n]\n\n[[package]]\nname = \"pest_generator\"\nversion = \"2.8.4\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"dee9efd8cdb50d719a80088b76f81aec7c41ed6d522ee750178f83883d271625\"\ndependencies = [\n \"pest\",\n \"pest_meta\",\n \"proc-macro2\",\n \"quote\",\n \"syn\",\n]\n\n[[package]]\nname = \"pest_meta\"\nversion = \"2.8.4\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"bf1d70880e76bdc13ba52eafa6239ce793d85c8e43896507e43dd8984ff05b82\"\ndependencies = [\n \"pest\",\n \"sha2\",\n]\n\n[[package]]\nname = \"phf\"\nversion = \"0.11.3\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078\"\ndependencies = [\n \"phf_shared\",\n]\n\n[[package]]\nname = \"phf_codegen\"\nversion = \"0.11.3\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a\"\ndependencies = [\n \"phf_generator\",\n \"phf_shared\",\n]\n\n[[package]]\nname = \"phf_generator\"\nversion = \"0.11.3\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d\"\ndependencies = [\n \"phf_shared\",\n \"rand 0.8.5\",\n]\n\n[[package]]\nname = \"phf_shared\"\nversion = \"0.11.3\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5\"\ndependencies = [\n \"siphasher\",\n]\n\n[[package]]\nname = \"pin-project-lite\"\nversion = \"0.2.16\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b\"\n\n[[package]]\nname = \"pin-utils\"\nversion = \"0.1.0\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184\"\n\n[[package]]\nname = \"portable-atomic\"\nversion = \"1.11.1\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483\"\n\n[[package]]\nname = \"ppv-lite86\"\nversion = \"0.2.21\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9\"\ndependencies = [\n \"zerocopy\",\n]\n\n[[package]]\nname = \"predicates\"\nversion = \"3.1.3\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573\"\ndependencies = [\n \"anstyle\",\n \"predicates-core\",\n]\n\n[[package]]\nname = \"predicates-core\"\nversion = \"1.0.9\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa\"\n\n[[package]]\nname = \"predicates-tree\"\nversion = \"1.0.12\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c\"\ndependencies = [\n \"predicates-core\",\n \"termtree\",\n]\n\n[[package]]\nname = \"proc-macro-crate\"\nversion = \"3.4.0\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983\"\ndependencies = [\n \"toml_edit 0.23.10+spec-1.0.0\",\n]\n\n[[package]]\nname = \"proc-macro2\"\nversion = \"1.0.103\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8\"\ndependencies = [\n \"unicode-ident\",\n]\n\n[[package]]\nname = \"quote\"\nversion = \"1.0.42\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f\"\ndependencies = [\n \"proc-macro2\",\n]\n\n[[package]]\nname = \"r-efi\"\nversion = \"5.3.0\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f\"\n\n[[package]]\nname = \"rand\"\nversion = \"0.8.5\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404\"\ndependencies = [\n \"libc\",\n \"rand_chacha 0.3.1\",\n \"rand_core 0.6.4\",\n]\n\n[[package]]\nname = \"rand\"\nversion = \"0.9.2\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1\"\ndependencies = [\n \"rand_chacha 0.9.0\",\n \"rand_core 0.9.3\",\n]\n\n[[package]]\nname = \"rand_chacha\"\nversion = \"0.3.1\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88\"\ndependencies = [\n \"ppv-lite86\",\n \"rand_core 0.6.4\",\n]\n\n[[package]]\nname = \"rand_chacha\"\nversion = \"0.9.0\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb\"\ndependencies = [\n \"ppv-lite86\",\n \"rand_core 0.9.3\",\n]\n\n[[package]]\nname = \"rand_core\"\nversion = \"0.6.4\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c\"\ndependencies = [\n \"getrandom 0.2.16\",\n]\n\n[[package]]\nname = \"rand_core\"\nversion = \"0.9.3\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38\"\ndependencies = [\n \"getrandom 0.3.4\",\n]\n\n[[package]]\nname = \"regex\"\nversion = \"1.12.2\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4\"\ndependencies = [\n \"aho-corasick\",\n \"memchr\",\n \"regex-automata\",\n \"regex-syntax\",\n]\n\n[[package]]\nname = \"regex-automata\"\nversion = \"0.4.13\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c\"\ndependencies = [\n \"aho-corasick\",\n \"memchr\",\n \"regex-syntax\",\n]\n\n[[package]]\nname = \"regex-syntax\"\nversion = \"0.8.8\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58\"\n\n[[package]]\nname = \"relative-path\"\nversion = \"1.9.3\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2\"\n\n[[package]]\nname = \"rhai\"\nversion = \"1.23.6\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"f4e35aaaa439a5bda2f8d15251bc375e4edfac75f9865734644782c9701b5709\"\ndependencies = [\n \"ahash\",\n \"bitflags\",\n \"instant\",\n \"num-traits\",\n \"once_cell\",\n \"rhai_codegen\",\n \"smallvec\",\n \"smartstring\",\n \"thin-vec\",\n]\n\n[[package]]\nname = \"rhai_codegen\"\nversion = \"3.1.0\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"d4322a2a4e8cf30771dd9f27f7f37ca9ac8fe812dddd811096a98483080dabe6\"\ndependencies = [\n \"proc-macro2\",\n \"quote\",\n \"syn\",\n]\n\n[[package]]\nname = \"rstest\"\nversion = \"0.23.0\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"0a2c585be59b6b5dd66a9d2084aa1d8bd52fbdb806eafdeffb52791147862035\"\ndependencies = [\n \"futures\",\n \"futures-timer\",\n \"rstest_macros\",\n \"rustc_version\",\n]\n\n[[package]]\nname = \"rstest_macros\"\nversion = \"0.23.0\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"825ea780781b15345a146be27eaefb05085e337e869bff01b4306a4fd4a9ad5a\"\ndependencies = [\n \"cfg-if\",\n \"glob\",\n \"proc-macro-crate\",\n \"proc-macro2\",\n \"quote\",\n \"regex\",\n \"relative-path\",\n \"rustc_version\",\n \"syn\",\n \"unicode-ident\",\n]\n\n[[package]]\nname = \"rustc_version\"\nversion = \"0.4.1\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92\"\ndependencies = [\n \"semver\",\n]\n\n[[package]]\nname = \"rustix\"\nversion = \"1.1.2\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e\"\ndependencies = [\n \"bitflags\",\n \"errno\",\n \"libc\",\n \"linux-raw-sys\",\n \"windows-sys 0.61.2\",\n]\n\n[[package]]\nname = \"rustversion\"\nversion = \"1.0.22\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d\"\n\n[[package]]\nname = \"ryu\"\nversion = \"1.0.20\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f\"\n\n[[package]]\nname = \"same-file\"\nversion = \"1.0.6\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502\"\ndependencies = [\n \"winapi-util\",\n]\n\n[[package]]\nname = \"semver\"\nversion = \"1.0.27\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2\"\n\n[[package]]\nname = \"serde\"\nversion = \"1.0.228\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e\"\ndependencies = [\n \"serde_core\",\n \"serde_derive\",\n]\n\n[[package]]\nname = \"serde_core\"\nversion = \"1.0.228\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad\"\ndependencies = [\n \"serde_derive\",\n]\n\n[[package]]\nname = \"serde_derive\"\nversion = \"1.0.228\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79\"\ndependencies = [\n \"proc-macro2\",\n \"quote\",\n \"syn\",\n]\n\n[[package]]\nname = \"serde_json\"\nversion = \"1.0.145\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c\"\ndependencies = [\n \"itoa\",\n \"memchr\",\n \"ryu\",\n \"serde\",\n \"serde_core\",\n]\n\n[[package]]\nname = \"serde_spanned\"\nversion = \"0.6.9\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3\"\ndependencies = [\n \"serde\",\n]\n\n[[package]]\nname = \"serde_yaml\"\nversion = \"0.9.34+deprecated\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47\"\ndependencies = [\n \"indexmap\",\n \"itoa\",\n \"ryu\",\n \"serde\",\n \"unsafe-libyaml\",\n]\n\n[[package]]\nname = \"sha2\"\nversion = \"0.10.9\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283\"\ndependencies = [\n \"cfg-if\",\n \"cpufeatures\",\n \"digest\",\n]\n\n[[package]]\nname = \"sharded-slab\"\nversion = \"0.1.7\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6\"\ndependencies = [\n \"lazy_static\",\n]\n\n[[package]]\nname = \"shared_child\"\nversion = \"1.1.1\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"1e362d9935bc50f019969e2f9ecd66786612daae13e8f277be7bfb66e8bed3f7\"\ndependencies = [\n \"libc\",\n \"sigchld\",\n \"windows-sys 0.60.2\",\n]\n\n[[package]]\nname = \"shell-words\"\nversion = \"1.1.1\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77\"\n\n[[package]]\nname = \"shlex\"\nversion = \"1.3.0\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64\"\n\n[[package]]\nname = \"sigchld\"\nversion = \"0.2.4\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"47106eded3c154e70176fc83df9737335c94ce22f821c32d17ed1db1f83badb1\"\ndependencies = [\n \"libc\",\n \"os_pipe\",\n \"signal-hook\",\n]\n\n[[package]]\nname = \"signal-hook\"\nversion = \"0.3.18\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2\"\ndependencies = [\n \"libc\",\n \"signal-hook-registry\",\n]\n\n[[package]]\nname = \"signal-hook-registry\"\nversion = \"1.4.7\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad\"\ndependencies = [\n \"libc\",\n]\n\n[[package]]\nname = \"similar\"\nversion = \"2.7.0\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa\"\n\n[[package]]\nname = \"siphasher\"\nversion = \"1.0.1\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d\"\n\n[[package]]\nname = \"slab\"\nversion = \"0.4.11\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589\"\n\n[[package]]\nname = \"slug\"\nversion = \"0.1.6\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"882a80f72ee45de3cc9a5afeb2da0331d58df69e4e7d8eeb5d3c7784ae67e724\"\ndependencies = [\n \"deunicode\",\n \"wasm-bindgen\",\n]\n\n[[package]]\nname = \"smallvec\"\nversion = \"1.15.1\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03\"\n\n[[package]]\nname = \"smartstring\"\nversion = \"1.0.1\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29\"\ndependencies = [\n \"autocfg\",\n \"static_assertions\",\n \"version_check\",\n]\n\n[[package]]\nname = \"static_assertions\"\nversion = \"1.1.0\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f\"\n\n[[package]]\nname = \"strsim\"\nversion = \"0.11.1\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f\"\n\n[[package]]\nname = \"strum\"\nversion = \"0.26.3\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06\"\ndependencies = [\n \"strum_macros\",\n]\n\n[[package]]\nname = \"strum_macros\"\nversion = \"0.26.4\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be\"\ndependencies = [\n \"heck\",\n \"proc-macro2\",\n \"quote\",\n \"rustversion\",\n \"syn\",\n]\n\n[[package]]\nname = \"syn\"\nversion = \"2.0.111\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87\"\ndependencies = [\n \"proc-macro2\",\n \"quote\",\n \"unicode-ident\",\n]\n\n[[package]]\nname = \"tempfile\"\nversion = \"3.23.0\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16\"\ndependencies = [\n \"fastrand\",\n \"getrandom 0.3.4\",\n \"once_cell\",\n \"rustix\",\n \"windows-sys 0.61.2\",\n]\n\n[[package]]\nname = \"tera\"\nversion = \"1.20.1\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"e8004bca281f2d32df3bacd59bc67b312cb4c70cea46cbd79dbe8ac5ed206722\"\ndependencies = [\n \"chrono\",\n \"chrono-tz\",\n \"globwalk\",\n \"humansize\",\n \"lazy_static\",\n \"percent-encoding\",\n \"pest\",\n \"pest_derive\",\n \"rand 0.8.5\",\n \"regex\",\n \"serde\",\n \"serde_json\",\n \"slug\",\n \"unicode-segmentation\",\n]\n\n[[package]]\nname = \"termtree\"\nversion = \"0.5.1\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683\"\n\n[[package]]\nname = \"thin-vec\"\nversion = \"0.2.14\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"144f754d318415ac792f9d69fc87abbbfc043ce2ef041c60f16ad828f638717d\"\n\n[[package]]\nname = \"thiserror\"\nversion = \"1.0.69\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52\"\ndependencies = [\n \"thiserror-impl\",\n]\n\n[[package]]\nname = \"thiserror-impl\"\nversion = \"1.0.69\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1\"\ndependencies = [\n \"proc-macro2\",\n \"quote\",\n \"syn\",\n]\n\n[[package]]\nname = \"thread_local\"\nversion = \"1.1.9\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185\"\ndependencies = [\n \"cfg-if\",\n]\n\n[[package]]\nname = \"tiny-keccak\"\nversion = \"2.0.2\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237\"\ndependencies = [\n \"crunchy\",\n]\n\n[[package]]\nname = \"toml\"\nversion = \"0.8.23\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362\"\ndependencies = [\n \"serde\",\n \"serde_spanned\",\n \"toml_datetime 0.6.11\",\n \"toml_edit 0.22.27\",\n]\n\n[[package]]\nname = \"toml_datetime\"\nversion = \"0.6.11\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c\"\ndependencies = [\n \"serde\",\n]\n\n[[package]]\nname = \"toml_datetime\"\nversion = \"0.7.5+spec-1.1.0\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347\"\ndependencies = [\n \"serde_core\",\n]\n\n[[package]]\nname = \"toml_edit\"\nversion = \"0.22.27\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a\"\ndependencies = [\n \"indexmap\",\n \"serde\",\n \"serde_spanned\",\n \"toml_datetime 0.6.11\",\n \"toml_write\",\n \"winnow\",\n]\n\n[[package]]\nname = \"toml_edit\"\nversion = \"0.23.10+spec-1.0.0\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269\"\ndependencies = [\n \"indexmap\",\n \"toml_datetime 0.7.5+spec-1.1.0\",\n \"toml_parser\",\n \"winnow\",\n]\n\n[[package]]\nname = \"toml_parser\"\nversion = \"1.0.6+spec-1.1.0\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44\"\ndependencies = [\n \"winnow\",\n]\n\n[[package]]\nname = \"toml_write\"\nversion = \"0.1.2\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801\"\n\n[[package]]\nname = \"tracing\"\nversion = \"0.1.44\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100\"\ndependencies = [\n \"pin-project-lite\",\n \"tracing-attributes\",\n \"tracing-core\",\n]\n\n[[package]]\nname = \"tracing-attributes\"\nversion = \"0.1.31\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da\"\ndependencies = [\n \"proc-macro2\",\n \"quote\",\n \"syn\",\n]\n\n[[package]]\nname = \"tracing-core\"\nversion = \"0.1.36\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a\"\ndependencies = [\n \"once_cell\",\n \"valuable\",\n]\n\n[[package]]\nname = \"tracing-log\"\nversion = \"0.2.0\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3\"\ndependencies = [\n \"log\",\n \"once_cell\",\n \"tracing-core\",\n]\n\n[[package]]\nname = \"tracing-subscriber\"\nversion = \"0.3.22\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e\"\ndependencies = [\n \"matchers\",\n \"nu-ansi-term\",\n \"once_cell\",\n \"regex-automata\",\n \"sharded-slab\",\n \"smallvec\",\n \"thread_local\",\n \"tracing\",\n \"tracing-core\",\n \"tracing-log\",\n]\n\n[[package]]\nname = \"tree-fs\"\nversion = \"0.3.1\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"5c6115680fa5fdb99b4ff19c9c3217e75116d2bb0eae82458c4e1818be6a10c7\"\ndependencies = [\n \"rand 0.9.2\",\n \"serde\",\n]\n\n[[package]]\nname = \"typenum\"\nversion = \"1.19.0\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb\"\n\n[[package]]\nname = \"ucd-trie\"\nversion = \"0.1.7\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971\"\n\n[[package]]\nname = \"unicode-ident\"\nversion = \"1.0.22\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5\"\n\n[[package]]\nname = \"unicode-segmentation\"\nversion = \"1.12.0\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493\"\n\n[[package]]\nname = \"unicode-width\"\nversion = \"0.2.2\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254\"\n\n[[package]]\nname = \"unicode-xid\"\nversion = \"0.2.6\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853\"\n\n[[package]]\nname = \"unsafe-libyaml\"\nversion = \"0.2.11\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861\"\n\n[[package]]\nname = \"utf8parse\"\nversion = \"0.2.2\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821\"\n\n[[package]]\nname = \"uuid\"\nversion = \"1.19.0\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a\"\ndependencies = [\n \"getrandom 0.3.4\",\n \"js-sys\",\n \"rand 0.9.2\",\n \"wasm-bindgen\",\n]\n\n[[package]]\nname = \"valuable\"\nversion = \"0.1.1\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65\"\n\n[[package]]\nname = \"version_check\"\nversion = \"0.9.5\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a\"\n\n[[package]]\nname = \"walkdir\"\nversion = \"2.5.0\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b\"\ndependencies = [\n \"same-file\",\n \"winapi-util\",\n]\n\n[[package]]\nname = \"wasi\"\nversion = \"0.11.1+wasi-snapshot-preview1\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b\"\n\n[[package]]\nname = \"wasip2\"\nversion = \"1.0.1+wasi-0.2.4\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7\"\ndependencies = [\n \"wit-bindgen\",\n]\n\n[[package]]\nname = \"wasm-bindgen\"\nversion = \"0.2.106\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd\"\ndependencies = [\n \"cfg-if\",\n \"once_cell\",\n \"rustversion\",\n \"wasm-bindgen-macro\",\n \"wasm-bindgen-shared\",\n]\n\n[[package]]\nname = \"wasm-bindgen-macro\"\nversion = \"0.2.106\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3\"\ndependencies = [\n \"quote\",\n \"wasm-bindgen-macro-support\",\n]\n\n[[package]]\nname = \"wasm-bindgen-macro-support\"\nversion = \"0.2.106\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40\"\ndependencies = [\n \"bumpalo\",\n \"proc-macro2\",\n \"quote\",\n \"syn\",\n \"wasm-bindgen-shared\",\n]\n\n[[package]]\nname = \"wasm-bindgen-shared\"\nversion = \"0.2.106\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4\"\ndependencies = [\n \"unicode-ident\",\n]\n\n[[package]]\nname = \"winapi-util\"\nversion = \"0.1.11\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22\"\ndependencies = [\n \"windows-sys 0.61.2\",\n]\n\n[[package]]\nname = \"windows-core\"\nversion = \"0.62.2\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb\"\ndependencies = [\n \"windows-implement\",\n \"windows-interface\",\n \"windows-link\",\n \"windows-result\",\n \"windows-strings\",\n]\n\n[[package]]\nname = \"windows-implement\"\nversion = \"0.60.2\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf\"\ndependencies = [\n \"proc-macro2\",\n \"quote\",\n \"syn\",\n]\n\n[[package]]\nname = \"windows-interface\"\nversion = \"0.59.3\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358\"\ndependencies = [\n \"proc-macro2\",\n \"quote\",\n \"syn\",\n]\n\n[[package]]\nname = \"windows-link\"\nversion = \"0.2.1\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5\"\n\n[[package]]\nname = \"windows-result\"\nversion = \"0.4.1\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5\"\ndependencies = [\n \"windows-link\",\n]\n\n[[package]]\nname = \"windows-strings\"\nversion = \"0.5.1\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091\"\ndependencies = [\n \"windows-link\",\n]\n\n[[package]]\nname = \"windows-sys\"\nversion = \"0.59.0\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b\"\ndependencies = [\n \"windows-targets 0.52.6\",\n]\n\n[[package]]\nname = \"windows-sys\"\nversion = \"0.60.2\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb\"\ndependencies = [\n \"windows-targets 0.53.5\",\n]\n\n[[package]]\nname = \"windows-sys\"\nversion = \"0.61.2\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc\"\ndependencies = [\n \"windows-link\",\n]\n\n[[package]]\nname = \"windows-targets\"\nversion = \"0.52.6\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973\"\ndependencies = [\n \"windows_aarch64_gnullvm 0.52.6\",\n \"windows_aarch64_msvc 0.52.6\",\n \"windows_i686_gnu 0.52.6\",\n \"windows_i686_gnullvm 0.52.6\",\n \"windows_i686_msvc 0.52.6\",\n \"windows_x86_64_gnu 0.52.6\",\n \"windows_x86_64_gnullvm 0.52.6\",\n \"windows_x86_64_msvc 0.52.6\",\n]\n\n[[package]]\nname = \"windows-targets\"\nversion = \"0.53.5\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3\"\ndependencies = [\n \"windows-link\",\n \"windows_aarch64_gnullvm 0.53.1\",\n \"windows_aarch64_msvc 0.53.1\",\n \"windows_i686_gnu 0.53.1\",\n \"windows_i686_gnullvm 0.53.1\",\n \"windows_i686_msvc 0.53.1\",\n \"windows_x86_64_gnu 0.53.1\",\n \"windows_x86_64_gnullvm 0.53.1\",\n \"windows_x86_64_msvc 0.53.1\",\n]\n\n[[package]]\nname = \"windows_aarch64_gnullvm\"\nversion = \"0.52.6\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3\"\n\n[[package]]\nname = \"windows_aarch64_gnullvm\"\nversion = \"0.53.1\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53\"\n\n[[package]]\nname = \"windows_aarch64_msvc\"\nversion = \"0.52.6\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469\"\n\n[[package]]\nname = \"windows_aarch64_msvc\"\nversion = \"0.53.1\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006\"\n\n[[package]]\nname = \"windows_i686_gnu\"\nversion = \"0.52.6\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b\"\n\n[[package]]\nname = \"windows_i686_gnu\"\nversion = \"0.53.1\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3\"\n\n[[package]]\nname = \"windows_i686_gnullvm\"\nversion = \"0.52.6\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66\"\n\n[[package]]\nname = \"windows_i686_gnullvm\"\nversion = \"0.53.1\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c\"\n\n[[package]]\nname = \"windows_i686_msvc\"\nversion = \"0.52.6\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66\"\n\n[[package]]\nname = \"windows_i686_msvc\"\nversion = \"0.53.1\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2\"\n\n[[package]]\nname = \"windows_x86_64_gnu\"\nversion = \"0.52.6\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78\"\n\n[[package]]\nname = \"windows_x86_64_gnu\"\nversion = \"0.53.1\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499\"\n\n[[package]]\nname = \"windows_x86_64_gnullvm\"\nversion = \"0.52.6\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d\"\n\n[[package]]\nname = \"windows_x86_64_gnullvm\"\nversion = \"0.53.1\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1\"\n\n[[package]]\nname = \"windows_x86_64_msvc\"\nversion = \"0.52.6\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec\"\n\n[[package]]\nname = \"windows_x86_64_msvc\"\nversion = \"0.53.1\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650\"\n\n[[package]]\nname = \"winnow\"\nversion = \"0.7.14\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829\"\ndependencies = [\n \"memchr\",\n]\n\n[[package]]\nname = \"wit-bindgen\"\nversion = \"0.46.0\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59\"\n\n[[package]]\nname = \"zerocopy\"\nversion = \"0.8.31\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3\"\ndependencies = [\n \"zerocopy-derive\",\n]\n\n[[package]]\nname = \"zerocopy-derive\"\nversion = \"0.8.31\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a\"\ndependencies = [\n \"proc-macro2\",\n \"quote\",\n \"syn\",\n]\n\n[[package]]\nname = \"zeroize\"\nversion = \"1.8.2\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\nchecksum = \"b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0\"\n"
  },
  {
    "path": "loco-new/Cargo.toml",
    "content": "[workspace]\n\n[package]\nname = \"loco\"\nversion = \"0.16.3\"\nedition = \"2021\"\ndescription = \"Loco new app generator\"\nlicense = \"Apache-2.0\"\nhomepage = \"https://docs.rs/loco\"\ndocumentation = \"https://docs.rs/loco\"\nreadme = \"README.md\"\ninclude = [\"src/**\", \"base_template/**\", \"Cargo.toml\", \"setup.rhai\"]\n\n[features]\ntest-wizard = []\n\n[profile.release]\nstrip = true\n\n[[bin]]\nname = \"loco\"\npath = \"src/bin/main.rs\"\nrequired-features = []\n\n\n[dependencies]\nthiserror = { version = \"1.0.63\" }\nclap = { version = \"4.4.7\", features = [\"derive\"] }\nserde = { version = \"1\", features = [\"derive\"] }\ntracing = { version = \"0.1.40\" }\ntracing-subscriber = { version = \"0.3.16\", features = [\"env-filter\"] }\nheck = { version = \"0.5.0\" }\ndialoguer = \"0.11.0\"\nstrum = { version = \"0.26\", features = [\"derive\"] }\nunicode-xid = { version = \"0.2.6\" }\nrhai = { version = \"1.23\" }\ninclude_dir = { version = \"0.7.4\" }\nfs_extra = { version = \"1.3.0\" }\nwalkdir = { version = \"2.5.0\" }\ntera = { version = \"1.20.0\" }\ncolored = { version = \"2\" }\nduct = { version = \"0.13.6\" }\nrand = { version = \"0.8.5\" }\ntree-fs = { version = \"0.3\" }\n\n[dev-dependencies]\nuuid = { version = \"1.11.0\", features = [\"v4\", \"fast-rng\"] }\nserde_yaml = { version = \"0.9\" }\ninsta = { version = \"1.41.1\", features = [\"redactions\", \"yaml\", \"filters\"] }\nrstest = { version = \"0.23.0\" }\nmockall = \"0.13.0\"\ntoml = \"0.8.19\"\nregex = \"1.11.1\"\n"
  },
  {
    "path": "loco-new/README.md",
    "content": "# Loco CLI\n\nLoco is a powerful framework designed to streamline the development of modern web applications with a focus on ease of use and flexibility. Whether you're building a SaaS app, a REST API, or a minimal service, Loco provides the tools you need to get started quickly and scale as your application grows. With built-in configuration for popular databases, background workers, and asset serving options, Loco gives you the power to customize your project to fit your needs.\n\n## Templates Versatile Options\nLoco empowers you to tailor your project to fit a variety of needs. Here are some of the versatile options it offers:\n\n### Application Types\n* **SaaS Applications:** Create platforms with features like user authentication, database integration, and scalable background processing.\n* **REST APIs:** Build robust APIs with database support, authentication, and modular controllers.\n* **Lightweight** Services: Focus on simplicity with minimal setups that include only essential controllers and views.\n\n#### Advanced Customization\nLoco is designed to offer advanced customization to meet the unique needs of your project. Whether you need a simple app that can evolve over time or a complex application that requires a specific configuration, Loco provides the flexibility to fine-tune your setup.\n\n\n## Getting Started\n\nTo install the Loco CLI on your machine, simply run the following command in your terminal:\n```sh\ncargo install loco\n```\nThis will install the latest version of Loco globally, making it accessible from anywhere in your terminal.\n\n## Create a New Project:\nOnce installed, you can create a new Loco project by running the following command:\n```sh\nloco new\n```\nThis will initiate a wizard that will guide you through the process of setting up your project.\n\n## Upgrade\nThe Loco CLI is bundled with the Loco framework create version. To ensure you're using the latest version of Loco and to get the most up-to-date templates, simply run the following command:\n```sh\ncargo install loco\n```\nThis will update the Loco CLI to the latest version, replacing the existing loco binary with the newest release. After upgrading, any new templates you generate will reflect the latest features and improvements."
  },
  {
    "path": "loco-new/base_template/.cargo/config.toml.t",
    "content": "[alias]\nloco = \"run --\"\n{%- if settings.os == \"windows\" %}\nloco-tool = \"run --bin tool --\"\n{% else %}\nloco-tool = \"run --\"\n{%- endif %}\n\nplayground = \"run --example playground\"\n\n# https://github.com/rust-lang/rust/issues/141626\n# (can be removed once link.exe is fixed)\n[target.x86_64-pc-windows-msvc]\nlinker = \"rust-lld\"\n"
  },
  {
    "path": "loco-new/base_template/.github/workflows/ci.yaml.t",
    "content": "name: CI\non:\n  push:\n    branches:\n      - master\n      - main\n  pull_request:\n\nenv:\n  RUST_TOOLCHAIN: stable\n  TOOLCHAIN_PROFILE: minimal\n\njobs:\n  rustfmt:\n    name: Check Style\n    runs-on: ubuntu-latest\n\n    permissions:\n      contents: read\n\n    steps:\n      - name: Checkout the code\n        uses: actions/checkout@v4\n      - uses: dtolnay/rust-toolchain@stable\n        with:\n          toolchain: ${% raw %}{{ env.RUST_TOOLCHAIN }}{% endraw %}\n          components: rustfmt\n      - name: Run cargo fmt\n        uses: actions-rs/cargo@v1\n        with:\n          command: fmt\n          args: --all -- --check\n\n  clippy:\n    name: Run Clippy\n    runs-on: ubuntu-latest\n\n    permissions:\n      contents: read\n\n    steps:\n      - name: Checkout the code\n        uses: actions/checkout@v4\n      - uses: dtolnay/rust-toolchain@stable\n        with:\n          toolchain: ${% raw %}{{ env.RUST_TOOLCHAIN }}{% endraw %}\n      - name: Setup Rust cache\n        uses: Swatinem/rust-cache@v2\n      - name: Run cargo clippy\n        uses: actions-rs/cargo@v1\n        with:\n          command: clippy\n          args: --all-features -- -D warnings -W clippy::pedantic -W clippy::nursery -W rust-2018-idioms\n\n  test:\n    name: Run Tests\n    runs-on: ubuntu-latest\n\n    permissions:\n      contents: read\n    \n    services:\n      redis:\n        image: redis\n        options: >-\n          --health-cmd \"redis-cli ping\"\n          --health-interval 10s\n          --health-timeout 5s\n          --health-retries 5\n        ports:\n          - \"6379:6379\"\n      postgres:\n        image: postgres\n        env:\n          POSTGRES_DB: postgres_test\n          POSTGRES_USER: postgres\n          POSTGRES_PASSWORD: postgres\n        ports:\n          - \"5432:5432\"\n        # Set health checks to wait until postgres has started\n        options: --health-cmd pg_isready\n          --health-interval 10s\n          --health-timeout 5s\n          --health-retries 5\n\n    steps:\n      - name: Checkout the code\n        uses: actions/checkout@v4\n      - uses: dtolnay/rust-toolchain@stable\n        with:\n          toolchain: ${% raw %}{{ env.RUST_TOOLCHAIN }}{% endraw %}\n    \n      {%- if settings.asset %}\n      {%- if settings.asset.kind == \"client\" %}\n      - name: Setup node\n        uses: actions/setup-node@v4\n        with:\n          node-version: ${% raw %}{{matrix.node-version}}{% endraw %}\n      - name: Build frontend\n        run: npm install && npm run build\n        working-directory: ./frontend\n      {%- endif -%}\n      {%- endif %}\n      - name: Setup Rust cache\n        uses: Swatinem/rust-cache@v2\n      - name: Run cargo test\n        uses: actions-rs/cargo@v1\n        with:\n          command: test\n          args: --all-features --all\n        env:\n          REDIS_URL: redis://localhost:${% raw %}{{job.services.redis.ports[6379]}}{% endraw %}\n          DATABASE_URL: postgres://postgres:postgres@localhost:5432/postgres_test\n          \n"
  },
  {
    "path": "loco-new/base_template/.gitignore",
    "content": "**/config/local.yaml\n**/config/*.local.yaml\n**/config/production.yaml\n\n# Generated by Cargo\n# will have compiled files and executables\ndebug/\ntarget/\n\n# include cargo lock\n!Cargo.lock\n\n# These are backup files generated by rustfmt\n**/*.rs.bk\n\n# MSVC Windows builds of rustc generate these, which store debugging information\n*.pdb\n\n*.sqlite\n*.sqlite-*"
  },
  {
    "path": "loco-new/base_template/.rustfmt.toml",
    "content": "max_width = 100\nuse_small_heuristics = \"Default\"\n"
  },
  {
    "path": "loco-new/base_template/Cargo.toml.t",
    "content": "{%- set_global feature_list = [] -%}\n{%- if settings.features.names | length > 0 -%}\n    {%- for name in settings.features.names -%}\n        {%- set_global feature_list = feature_list | concat(with=['\"' ~ name ~ '\"']) -%}\n    {%- endfor -%}\n{%- endif -%}\n[workspace]\n\n[package]\nname = \"{{settings.package_name}}\"\nversion = \"0.1.0\"\nedition = \"2021\"\npublish = false\ndefault-run = \"{{settings.module_name}}-cli\"\n\n# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html\n\n[workspace.dependencies]\nloco-rs = { {{settings.loco_version_text}} {%- if not settings.features.default_features  %}, default-features = false {%- endif %} }\n\n[dependencies]\nloco-rs = { workspace = true {% if feature_list | length > 0 %}, features = {{feature_list}}{% endif %} }\nserde = { version = \"1\", features = [\"derive\"] }\nserde_json = { version = \"1\" }\ntokio = { version = \"1.45\", default-features = false, features = [\n  \"rt-multi-thread\",\n] }\nasync-trait = { version = \"0.1\" }\naxum = { version = \"0.8\" }\ntracing = { version = \"0.1\" }\ntracing-subscriber = { version = \"0.3\", features = [\"env-filter\", \"json\"] }\nregex = { version = \"1.11\" }\n{%- if settings.db %}\nmigration = { path = \"migration\" }\nsea-orm = { version = \"1.1\", features = [\n  \"sqlx-sqlite\",\n  \"sqlx-postgres\",\n  \"runtime-tokio-rustls\",\n  \"macros\",\n] }\nchrono = { version = \"0.4\" }\nvalidator = { version = \"0.20\" }\nuuid = { version = \"1.6\", features = [\"v4\"] }\n{%- endif %}\n\n{%- if settings.mailer %}\ninclude_dir = { version = \"0.7\" }\n{%- endif %}\n\n{%- if settings.asset %}\n{%- if settings.asset.kind == \"server\" %}\n# view engine i18n\nfluent-templates = { version = \"0.13\", features = [\"tera\"] }\nunic-langid = { version = \"0.9\" }\n# /view engine\n{%- endif %}\naxum-extra = { version = \"0.10\", features = [\"form\"] }\n{%- endif %}\n\n[[bin]]\nname = \"{{settings.module_name}}-cli\"\npath = \"src/bin/main.rs\"\nrequired-features = []\n\n{%- if settings.os == \"windows\" %}\n[[bin]]\nname = \"tool\"\npath = \"src/bin/tool.rs\"\nrequired-features = []\n{%- endif %}\n\n[dev-dependencies]\nloco-rs = { workspace = true, features = [\"testing\"] }\nserial_test = { version = \"3.1.1\" }\nrstest = { version = \"0.25\" }\ninsta = { version = \"1.34\", features = [\"redactions\", \"yaml\", \"filters\"] }\n"
  },
  {
    "path": "loco-new/base_template/README.md",
    "content": "# Welcome to Loco :train:\n\n[Loco](https://loco.rs) is a web and API framework running on Rust.\n\nThis is the **SaaS starter** which includes a `User` model and authentication based on JWT.\nIt also include configuration sections that help you pick either a frontend or a server-side template set up for your fullstack server.\n\n\n## Quick Start\n\n```sh\ncargo loco start\n```\n\n```sh\n$ cargo loco start\nFinished dev [unoptimized + debuginfo] target(s) in 21.63s\n    Running `target/debug/myapp start`\n\n    :\n    :\n    :\n\ncontroller/app_routes.rs:203: [Middleware] Adding log trace id\n\n                      ▄     ▀\n                                 ▀  ▄\n                  ▄       ▀     ▄  ▄ ▄▀\n                                    ▄ ▀▄▄\n                        ▄     ▀    ▀  ▀▄▀█▄\n                                          ▀█▄\n▄▄▄▄▄▄▄  ▄▄▄▄▄▄▄▄▄   ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄ ▀▀█\n ██████  █████   ███ █████   ███ █████   ███ ▀█\n ██████  █████   ███ █████   ▀▀▀ █████   ███ ▄█▄\n ██████  █████   ███ █████       █████   ███ ████▄\n ██████  █████   ███ █████   ▄▄▄ █████   ███ █████\n ██████  █████   ███  ████   ███ █████   ███ ████▀\n   ▀▀▀██▄ ▀▀▀▀▀▀▀▀▀▀  ▀▀▀▀▀▀▀▀▀▀  ▀▀▀▀▀▀▀▀▀▀ ██▀\n       ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀\n                https://loco.rs\n\nenvironment: development\n   database: automigrate\n     logger: debug\ncompilation: debug\n      modes: server\n\nlistening on http://localhost:5150\n```\n\n## Full Stack Serving\n\nYou can check your [configuration](config/development.yaml) to pick either frontend setup or server-side rendered template, and activate the relevant configuration sections.\n\n\n## Getting help\n\nCheck out [a quick tour](https://loco.rs/docs/getting-started/tour/) or [the complete guide](https://loco.rs/docs/getting-started/guide/).\n"
  },
  {
    "path": "loco-new/base_template/assets/i18n/de-DE/main.ftl",
    "content": "hello-world = Hallo Welt!\ngreeting = Hallochen { $name }!\n        .placeholder = Hallo Freund!\nabout = Uber\n"
  },
  {
    "path": "loco-new/base_template/assets/i18n/en-US/main.ftl",
    "content": "hello-world = Hello World!\ngreeting = Hello { $name }!\n        .placeholder = Hello Friend!\nabout = About\nsimple = simple text\nreference = simple text with a reference: { -something }\nparameter = text with a { $param }\nparameter2 = text one { $param } second { $multi-word-param }\nemail = text with an EMAIL(\"example@example.org\")\nfallback = this should fall back\n"
  },
  {
    "path": "loco-new/base_template/assets/i18n/shared.ftl",
    "content": "-something = foo\n"
  },
  {
    "path": "loco-new/base_template/assets/static/404.html",
    "content": "<html><body>\nnot found :-(\n</body></html>\n"
  },
  {
    "path": "loco-new/base_template/assets/views/home/hello.html",
    "content": "<html><body>\n  <img src=\"/static/image.png\" width=\"200\"/>\n  <br/>\n  find this tera template at <code>assets/views/home/hello.html</code>: \n  <br/>\n  <br/>\n  {{ t(key=\"hello-world\", lang=\"en-US\") }}, \n  <br/>\n  {{ t(key=\"hello-world\", lang=\"de-DE\") }}\n  \n</body></html>\n  "
  },
  {
    "path": "loco-new/base_template/config/development.yaml.t",
    "content": "# Loco configuration file documentation\n\n# Application logging configuration\nlogger:\n  # Enable or disable logging.\n  enable: true\n  # Enable pretty backtrace (sets RUST_BACKTRACE=1)\n  pretty_backtrace: true\n  # Log level, options: trace, debug, info, warn or error.\n  level: {{ get_env(name=\"LOG_LEVEL\", default=\"debug\") }}\n  # Define the logging format. options: compact, pretty or json\n  format: compact\n  # By default the logger has filtering only logs that came from your code or logs that came from `loco` framework. to see all third party libraries\n  # Uncomment the line below to override to see all third party libraries you can enable this config and override the logger filters.\n  # override_filter: trace\n\n# Web server configuration\nserver:\n  # Port on which the server will listen. the server binding is 0.0.0.0:{PORT}\n  port: {{ get_env(name=\"PORT\", default=\"5150\") }}\n  # Binding for the server (which interface to bind to)\n  binding: {{ get_env(name=\"BINDING\", default=\"localhost\") }}\n  # The UI hostname or IP address that mailers will point to.\n  host: http://localhost\n  # Out of the box middleware configuration. to disable middleware you can changed the `enable` field to `false` of comment the middleware block\n  middlewares:\n  {%- if settings.asset %}\n    {%- if settings.asset.kind == \"server\" %}\n    static:\n      enable: true\n      must_exist: true\n      precompressed: false\n      folder:\n        uri: \"/static\"\n        path: \"assets/static\"\n      fallback: \"assets/static/404.html\"\n  {%- elif settings.asset.kind == \"client\" %}\n    fallback:\n      enable: false\n    static:\n      enable: true\n      must_exist: true\n      precompressed: false\n      folder:\n        uri: \"/\"\n        path: \"frontend/dist\"\n      fallback: \"frontend/dist/index.html\"\n  {%- endif -%}\n\n  {%- endif -%}\n\n{%- if settings.background%}\n\n# Worker Configuration\nworkers:\n  # specifies the worker mode. Options:\n  #   - BackgroundQueue - Workers operate asynchronously in the background, processing queued.\n  #   - ForegroundBlocking - Workers operate in the foreground and block until tasks are completed.\n  #   - BackgroundAsync - Workers operate asynchronously in the background, processing tasks with async capabilities.\n  mode: {{settings.background.kind}}\n\n  {% if settings.background.kind == \"BackgroundQueue\"%}\n# Queue Configuration\nqueue:\n  kind: Redis\n  # Redis connection URI\n  uri: {% raw %}{{{% endraw %} get_env(name=\"REDIS_URL\", default=\"redis://127.0.0.1\") {% raw %}}}{% endraw %}\n  # Dangerously flush all data in Redis on startup. dangerous operation, make sure that you using this flag only on dev environments or test mode\n  dangerously_flush: false\n  {%- endif %}\n{%- endif -%}\n\n{%- if settings.mailer %}\n\n# Mailer Configuration.\nmailer:\n  # SMTP mailer configuration.\n  smtp:\n    # Enable/Disable smtp mailer.\n    enable: true\n    # SMTP server host. e.x localhost, smtp.gmail.com\n    host: {{ get_env(name=\"MAILER_HOST\", default=\"localhost\") }}\n    # SMTP server port\n    port: 1025\n    # Use secure connection (SSL/TLS).\n    secure: false\n    # auth:\n    #   user:\n    #   password:\n    # Override the SMTP hello name (default is the machine's hostname)\n    # hello_name:\n{%- endif %}\n\n# Initializers Configuration\n# initializers:\n#  oauth2:\n#    authorization_code: # Authorization code grant type\n#      - client_identifier: google # Identifier for the OAuth2 provider. Replace 'google' with your provider's name if different, must be unique within the oauth2 config.\n#        ... other fields\n\n{%- if settings.db %}\n\n# Database Configuration\ndatabase:\n  # Database connection URI\n  uri: {% raw %}{{{% endraw %} get_env(name=\"DATABASE_URL\", default=\"{{settings.db.endpoint | replace(from='NAME', to=settings.package_name) | replace(from='ENV', to='development')}}\") {% raw %}}}{% endraw %}\n  # When enabled, the sql query will be logged.\n  enable_logging: {{ get_env(name=\"DB_LOGGING\", default=\"false\") }}\n  # Set the timeout duration when acquiring a connection.\n  connect_timeout: {% raw %}{{{% endraw %} get_env(name=\"DB_CONNECT_TIMEOUT\", default=\"500\") {% raw %}}}{% endraw %}\n  # Set the idle duration before closing a connection.\n  idle_timeout: {% raw %}{{{% endraw %} get_env(name=\"DB_IDLE_TIMEOUT\", default=\"500\") {% raw %}}}{% endraw %}\n  # Minimum number of connections for a pool.\n  min_connections: {% raw %}{{{% endraw %} get_env(name=\"DB_MIN_CONNECTIONS\", default=\"1\") {% raw %}}}{% endraw %}\n  # Maximum number of connections for a pool.\n  max_connections: {% raw %}{{{% endraw %} get_env(name=\"DB_MAX_CONNECTIONS\", default=\"1\") {% raw %}}}{% endraw %}\n  # Run migration up when application loaded\n  auto_migrate: true\n  # Truncate database when application loaded. This is a dangerous operation, make sure that you using this flag only on dev environments or test mode\n  dangerously_truncate: false\n  # Recreating schema when application loaded.  This is a dangerous operation, make sure that you using this flag only on dev environments or test mode\n  dangerously_recreate: false\n{%- endif %}\n\n{%- if settings.auth %}\n\n# Authentication Configuration\nauth:\n  # JWT authentication\n  jwt:\n    # Secret key for token generation and verification\n    secret: {{20 | random_string }}\n    # Token expiration time in seconds\n    expiration: 604800 # 7 days\n{%- endif %}\n"
  },
  {
    "path": "loco-new/base_template/config/test.yaml.t",
    "content": "# Loco configuration file documentation\n\n# Application logging configuration\nlogger:\n  # Enable or disable logging.\n  enable: false\n  # Enable pretty backtrace (sets RUST_BACKTRACE=1)\n  pretty_backtrace: true\n  # Log level, options: trace, debug, info, warn or error.\n  level: debug\n  # Define the logging format. options: compact, pretty or json\n  format: compact\n  # By default the logger has filtering only logs that came from your code or logs that came from `loco` framework. to see all third party libraries\n  # Uncomment the line below to override to see all third party libraries you can enable this config and override the logger filters.\n  # override_filter: trace\n\n# Web server configuration\nserver:\n  # Port on which the server will listen. the server binding is 0.0.0.0:{PORT}\n  port: 5150\n  # The UI hostname or IP address that mailers will point to.\n  host: http://localhost\n  # Out of the box middleware configuration. to disable middleware you can changed the `enable` field to `false` of comment the middleware block\n  middlewares:\n  {%- if settings.asset %}   \n    {%- if settings.asset.kind == \"server\" %} \n    static:\n      enable: true\n      must_exist: true\n      precompressed: false\n      folder:\n        uri: \"/static\"\n        path: \"assets/static\"\n      fallback: \"assets/static/404.html\"\n  {%- elif settings.asset.kind == \"client\" %} \n    fallback:\n      enable: false\n    static:\n      enable: true\n      must_exist: true\n      precompressed: false\n      folder:\n        uri: \"/\"\n        path: \"frontend/dist\"\n      fallback: \"frontend/dist/index.html\"\n  {%- endif -%}\n  \n  {%- endif -%}\n   \n{%- if settings.background%}\n\n# Worker Configuration\nworkers:\n  # specifies the worker mode. Options:\n  #   - BackgroundQueue - Workers operate asynchronously in the background, processing queued.\n  #   - ForegroundBlocking - Workers operate in the foreground and block until tasks are completed.\n  #   - BackgroundAsync - Workers operate asynchronously in the background, processing tasks with async capabilities.\n  mode: ForegroundBlocking\n\n  {% if settings.background.kind == \"BackgroundQueue\"%}\n# Queue Configuration\nqueue:\n  kind: Redis\n  # Redis connection URI\n  uri: {% raw %}{{{% endraw %} get_env(name=\"REDIS_URL\", default=\"redis://127.0.0.1\") {% raw %}}}{% endraw %}\n  # Dangerously flush all data in Redis on startup. dangerous operation, make sure that you using this flag only on dev environments or test mode\n  dangerously_flush: false\n  {%- endif %}\n{%- endif -%}\n\n{%- if settings.mailer %}\n\n# Mailer Configuration.\nmailer:\n  stub: true\n  # SMTP mailer configuration.\n  smtp:\n    # Enable/Disable smtp mailer.\n    enable: true\n    # SMTP server host. e.x localhost, smtp.gmail.com\n    host: {{ get_env(name=\"MAILER_HOST\", default=\"localhost\") }}\n    # SMTP server port\n    port: 1025\n    # Use secure connection (SSL/TLS).\n    secure: false\n    # auth:\n    #   user:\n    #   password:\n{%- endif %}\n\n# Initializers Configuration\n# initializers:\n#  oauth2:\n#    authorization_code: # Authorization code grant type\n#      - client_identifier: google # Identifier for the OAuth2 provider. Replace 'google' with your provider's name if different, must be unique within the oauth2 config.\n#        ... other fields\n\n{%- if settings.db %}\n\n# Database Configuration\ndatabase:\n  # Database connection URI\n  uri: {% raw %}{{{% endraw %} get_env(name=\"DATABASE_URL\", default=\"{{settings.db.endpoint | replace(from='NAME', to=settings.package_name) | replace(from='ENV', to='test')}}\") {% raw %}}}{% endraw %}\n  # When enabled, the sql query will be logged.\n  enable_logging: false\n  # Set the timeout duration when acquiring a connection.\n  connect_timeout: {% raw %}{{{% endraw %} get_env(name=\"DB_CONNECT_TIMEOUT\", default=\"500\") {% raw %}}}{% endraw %}\n  # Set the idle duration before closing a connection.\n  idle_timeout: {% raw %}{{{% endraw %} get_env(name=\"DB_IDLE_TIMEOUT\", default=\"500\") {% raw %}}}{% endraw %}\n  # Minimum number of connections for a pool.\n  min_connections: {% raw %}{{{% endraw %} get_env(name=\"DB_MIN_CONNECTIONS\", default=\"1\") {% raw %}}}{% endraw %}\n  # Maximum number of connections for a pool.\n  max_connections: {% raw %}{{{% endraw %} get_env(name=\"DB_MAX_CONNECTIONS\", default=\"1\") {% raw %}}}{% endraw %}\n  # Run migration up when application loaded\n  auto_migrate: true\n  # Truncate database when application loaded. This is a dangerous operation, make sure that you using this flag only on dev environments or test mode\n  dangerously_truncate: true\n  # Recreating schema when application loaded.  This is a dangerous operation, make sure that you using this flag only on dev environments or test mode\n  dangerously_recreate: true\n{%- endif %}\n\n{%- if settings.auth %}\n\n# Authentication Configuration\nauth:\n  # JWT authentication\n  jwt:\n    # Secret key for token generation and verification\n    secret: {{20 | random_string }}\n    # Token expiration time in seconds\n    expiration: 604800 # 7 days\n{%- endif %}\n"
  },
  {
    "path": "loco-new/base_template/examples/playground.rs.t",
    "content": "#[allow(unused_imports)]\nuse loco_rs::{cli::playground, prelude::*};\nuse {{settings.module_name}}::app::App;\n\n#[tokio::main]\nasync fn main() -> loco_rs::Result<()> {\n    let _ctx = playground::<App>().await?;\n\n    // let active_model: articles::ActiveModel = articles::ActiveModel {\n    //     title: Set(Some(\"how to build apps in 3 steps\".to_string())),\n    //     content: Set(Some(\"use Loco: https://loco.rs\".to_string())),\n    //     ..Default::default()\n    // };\n    // active_model.insert(&ctx.db).await.unwrap();\n\n    // let res = articles::Entity::find().all(&ctx.db).await.unwrap();\n    // println!(\"{:?}\", res);\n    println!(\"welcome to playground. edit me at `examples/playground.rs`\");\n\n    Ok(())\n}\n"
  },
  {
    "path": "loco-new/base_template/frontend/.gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist-ssr\ndist/\n*.local\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n\n# Common local dotenv files popularised by Create React App & Next.js\n# https://rsbuild.dev/guide/advanced/env-vars#env-file\n.env.local\n.env.development.local\n.env.production.local\n.env.test.local\n"
  },
  {
    "path": "loco-new/base_template/frontend/README.md",
    "content": "# SaaS Frontend\n\n## Batteries included\n\n- [TypeScript](https://www.typescriptlang.org/): A typed superset of JavaScript\n- [Rsbuild](https://rsbuild.dev/): A Rust-based web build tool\n- [Biome](https://biomejs.dev/): A Rust-based formatter and sensible linter for the web\n- [React](https://reactjs.org/): A JavaScript library for building user interfaces\n\nIf you don't like React for some reason, Rsbuild makes it easy to replace it with something else!\n\n# Development\n\nTo get started with the development of the SaaS frontend, follow these steps:\n\n### 1. Install Packages\n\nUse the following command to install the required packages using pnpm:\n\n```sh\npnpm install\n```\n\n### 2. Run in Development Mode\n\nOnce the packages are installed, run your frontend application in development mode with the following command:\n\n```sh\npnpm dev\n```\n\nThis will start the development frontend server serving via vit\n\n### 3. Build The application\n\nTo build your application run the following command:\n\n```sh\npnpm build\n```\n\nAfter the build `dist` folder is ready to served by loco. run loco `cargo loco start` and the frontend application will served via Loco"
  },
  {
    "path": "loco-new/base_template/frontend/biome.json",
    "content": "{\n  \"$schema\": \"https://biomejs.dev/schemas/1.8.2/schema.json\",\n  \"organizeImports\": {\n    \"enabled\": true\n  },\n  \"linter\": {\n    \"enabled\": true,\n    \"rules\": {\n      \"recommended\": true\n    }\n  },\n  \"javascript\": {\n    \"formatter\": {\n      \"enabled\": true,\n      \"indentStyle\": \"space\"\n    }\n  },\n  \"json\": {\n    \"formatter\": {\n      \"enabled\": true,\n      \"indentStyle\": \"space\"\n    }\n  }\n}\n"
  },
  {
    "path": "loco-new/base_template/frontend/package.json",
    "content": "{\n  \"name\": \"frontend\",\n  \"private\": true,\n  \"version\": \"0.0.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"rsbuild dev --open\",\n    \"build\": \"rsbuild build\",\n    \"lint\": \"biome check src/\",\n    \"preview\": \"rsbuild preview\"\n  },\n  \"dependencies\": {\n    \"react\": \"^19\",\n    \"react-dom\": \"^19\"\n  },\n  \"devDependencies\": {\n    \"@biomejs/biome\": \"^2\",\n    \"@rsbuild/core\": \"^1\",\n    \"@rsbuild/plugin-react\": \"^1\",\n    \"@types/react\": \"^19\",\n    \"@types/react-dom\": \"^19\",\n    \"typescript\": \"^5\"\n  }\n}\n"
  },
  {
    "path": "loco-new/base_template/frontend/rsbuild.config.ts",
    "content": "import { defineConfig } from \"@rsbuild/core\";\nimport { pluginReact } from \"@rsbuild/plugin-react\";\n\n// https://rsbuild.dev/guide/basic/configure-rsbuild\nexport default defineConfig({\n  plugins: [pluginReact()],\n  html: {\n    favicon: \"src/assets/favicon.ico\",\n    title: \"Loco SaaS Starter\",\n  },\n  server: {\n    proxy: {\n      \"/api\": {\n        target: \"http://127.0.0.1:5150\",\n        changeOrigin: true,\n        secure: false,\n      },\n    },\n  },\n});\n"
  },
  {
    "path": "loco-new/base_template/frontend/src/LocoSplash.tsx",
    "content": "export const LocoSplash = () => {\n  return (\n    <div>\n      <header className=\"navbar fixed-top\">\n        <div className=\"container\">\n          <a href=\"https://loco.rs?ref=starter\">Loco</a>\n          <ul className=\"navbar-nav \">\n            <li className=\"\">\n              <a\n                className=\"\"\n                href=\"https://github.com/loco-rs/loco?ref=starter\"\n                target=\"_blank\"\n                rel=\"noreferrer\"\n              >\n                <svg\n                  xmlns=\"http://www.w3.org/2000/svg\"\n                  width=\"20\"\n                  height=\"20\"\n                  viewBox=\"0 0 24 24\"\n                  fill=\"none\"\n                  stroke=\"currentColor\"\n                  stroke-width=\"2\"\n                  stroke-linecap=\"round\"\n                  stroke-linejoin=\"round\"\n                  className=\"feather feather-github\"\n                >\n                  <title>Loco GitHub repo</title>\n                  <path d=\"M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22\" />\n                </svg>\n              </a>\n            </li>\n            <li className=\"\">\n              <a\n                className=\"\"\n                href=\"https://github.com/loco-rs/loco/stargazers?ref=starter\"\n                target=\"_blank\"\n                rel=\"noreferrer\"\n              >\n                <svg\n                  xmlns=\"http://www.w3.org/2000/svg\"\n                  width=\"20\"\n                  height=\"20\"\n                  viewBox=\"0 0 24 24\"\n                  fill=\"none\"\n                  stroke=\"currentColor\"\n                  stroke-width=\"2\"\n                  stroke-linecap=\"round\"\n                  stroke-linejoin=\"round\"\n                  className=\"feather feather-star\"\n                >\n                  <title>Loco GitHub stars</title>\n                  <polygon points=\"12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2\" />\n                </svg>\n              </a>\n            </li>\n          </ul>\n        </div>\n      </header>\n      <div className=\"logo\">\n        <h1>Loco: SaaS application</h1>\n        <img src=\"https://loco.rs/icon.svg\" className=\"logo\" alt=\"Loco logo\" />\n      </div>\n      <footer>\n        <ul>\n          <li>\n            <a\n              href=\"https://loco.rs?ref=starter\"\n              target=\"_blank\"\n              rel=\"noreferrer\"\n            >\n              Our Documentation\n            </a>\n          </li>\n          <li>\n            <a\n              href=\"https://github.com/loco-rs/loco?ref=starter\"\n              target=\"_blank\"\n              rel=\"noreferrer\"\n            >\n              GitHub\n            </a>\n          </li>\n          <li>\n            <a\n              href=\"https://github.com/loco-rs/loco/issues?ref=starter\"\n              target=\"_blank\"\n              rel=\"noreferrer\"\n            >\n              Found a bug?\n            </a>\n          </li>\n          <li>\n            <a\n              href=\"https://github.com/loco-rs/loco/discussions?ref=starter\"\n              target=\"_blank\"\n              rel=\"noreferrer\"\n            >\n              Needs help?\n            </a>\n          </li>\n        </ul>\n      </footer>\n    </div>\n  );\n};\n"
  },
  {
    "path": "loco-new/base_template/frontend/src/env.d.ts",
    "content": "/// <reference types=\"@rsbuild/core/types\" />\n"
  },
  {
    "path": "loco-new/base_template/frontend/src/index.css",
    "content": "body {\n  margin: 0;\n  font-family: \"Arimo\", -apple-system, blinkmacsystemfont, \"Segoe UI\", roboto, \"Helvetica Neue\", arial, \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n  font-size: 1rem;\n  font-weight: 400;\n  line-height: 1.5;\n  background: #212529;\n  color: #dee2e6;\n  -webkit-text-size-adjust: 100%;\n  -webkit-tap-highlight-color: rgba(29, 45, 53, 0)\n}\n\nul {\n  margin-top: 0;\n  margin-bottom: 1rem;\n  list-style: none;\n}\n\na {\n  color: #dee2e6;\n  text-decoration: none\n}\n\n.container {\n  max-width: 1320px;\n  padding-right: var(--bs-gutter-x, 24px);\n  padding-left: var(--bs-gutter-x, 24px);\n  margin-right: auto;\n  margin-left: auto\n}\n\n\n.navbar {\n  padding-top: .5rem;\n  padding-bottom: .5rem\n}\n\n.navbar .container {\n  display: flex;\n  justify-content: space-between\n}\n\n.navbar-nav {\n  margin-bottom: 0;\n}\n.navbar-nav li {\n    display: inline-flex;\n    margin-right: 10px;\n}\n\n.fixed-top {\n  position: fixed;\n  top: 0;\n  right: 0;\n  left: 0;\n}\n\nbody {\n  font-size: 1rem;\n  padding-top: 6rem !important\n}\n\n.navbar {\n  border-bottom: 1px solid #2a2f34;\n}\n\n\n.logo {\n  max-width: 1280px;\n  margin: 0 auto;\n  text-align: center;\n}\n\n.logo img {\n  width: 250px;\n}\nfooter {\n  position: absolute;\n  bottom: 0;\n  width: 100%;\n  text-align: center;\n}\n\nfooter ul {\n  display: inline-block;\n  padding: 0;\n}\n\nfooter ul li {\n  display: inline-flex;\n  align-items: center;\n  margin: 0 5px;\n  list-style: none;\n}\n\nfooter ul li:not(:last-child) {\n  border-right: 1px solid #ccc;\n  padding-right: 5px;\n  height: 15px;\n}"
  },
  {
    "path": "loco-new/base_template/frontend/src/index.tsx",
    "content": "import React from \"react\";\nimport ReactDOM from \"react-dom/client\";\nimport { LocoSplash } from \"./LocoSplash\";\n\nimport \"./index.css\";\n\nconst root = document.getElementById(\"root\");\n\nif (!root) {\n  throw new Error(\"No root element found\");\n}\n\nReactDOM.createRoot(root).render(\n  <React.StrictMode>\n    <LocoSplash />\n  </React.StrictMode>,\n);\n"
  },
  {
    "path": "loco-new/base_template/frontend/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"lib\": [\"DOM\", \"ES2020\"],\n    \"module\": \"ESNext\",\n    \"jsx\": \"react-jsx\",\n    \"strict\": true,\n    \"skipLibCheck\": true,\n    \"isolatedModules\": true,\n    \"resolveJsonModule\": true,\n    \"moduleResolution\": \"bundler\",\n    \"useDefineForClassFields\": true\n  },\n  \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "loco-new/base_template/migration/Cargo.toml.t",
    "content": "[package]\nname = \"migration\"\nversion = \"0.1.0\"\nedition = \"2021\"\npublish = false\n\n[lib]\nname = \"migration\"\npath = \"src/lib.rs\"\n\n[dependencies]\nloco-rs = { workspace = true }\n\n\n[dependencies.sea-orm-migration]\nversion = \"1.1.0\"\nfeatures = [\n  # Enable at least one `ASYNC_RUNTIME` and `DATABASE_DRIVER` feature if you want to run migration via CLI.\n  # View the list of supported features at https://www.sea-ql.org/SeaORM/docs/install-and-config/database-and-async-runtime.\n  # e.g.\n  \"runtime-tokio-rustls\", # `ASYNC_RUNTIME` feature\n]\n"
  },
  {
    "path": "loco-new/base_template/migration/src/lib.rs.t",
    "content": "#![allow(elided_lifetimes_in_paths)]\n#![allow(clippy::wildcard_imports)]\npub use sea_orm_migration::prelude::*;\n\n{%- if settings.auth %}\nmod m20220101_000001_users;\n{%- endif %}\n\npub struct Migrator;\n\n#[async_trait::async_trait]\nimpl MigratorTrait for Migrator {\n    fn migrations() -> Vec<Box<dyn MigrationTrait>> {\n        vec![\n            {%- if settings.auth %}\n            Box::new(m20220101_000001_users::Migration),\n            {%- endif %}\n            // inject-above (do not remove this comment)\n        ]\n    }\n}\n"
  },
  {
    "path": "loco-new/base_template/migration/src/m20220101_000001_users.rs",
    "content": "use loco_rs::schema::*;\nuse sea_orm_migration::prelude::*;\n\n#[derive(DeriveMigrationName)]\npub struct Migration;\n\n#[async_trait::async_trait]\nimpl MigrationTrait for Migration {\n    async fn up(&self, m: &SchemaManager) -> Result<(), DbErr> {\n        create_table(\n            m,\n            \"users\",\n            &[\n                (\"id\", ColType::PkAuto),\n                (\"pid\", ColType::Uuid),\n                (\"email\", ColType::StringUniq),\n                (\"password\", ColType::String),\n                (\"api_key\", ColType::StringUniq),\n                (\"name\", ColType::String),\n                (\"reset_token\", ColType::StringNull),\n                (\"reset_sent_at\", ColType::TimestampWithTimeZoneNull),\n                (\"email_verification_token\", ColType::StringNull),\n                (\n                    \"email_verification_sent_at\",\n                    ColType::TimestampWithTimeZoneNull,\n                ),\n                (\"email_verified_at\", ColType::TimestampWithTimeZoneNull),\n                (\"magic_link_token\", ColType::StringNull),\n                (\"magic_link_expiration\", ColType::TimestampWithTimeZoneNull),\n            ],\n            &[],\n        )\n        .await?;\n        Ok(())\n    }\n\n    async fn down(&self, m: &SchemaManager) -> Result<(), DbErr> {\n        drop_table(m, \"users\").await?;\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "loco-new/base_template/src/app.rs.t",
    "content": "{%- if settings.db -%}\nuse std::path::Path;\n{% endif -%}\nuse async_trait::async_trait;\nuse loco_rs::{\n    app::{AppContext, Hooks, Initializer},\n    bgworker::{\n        {%- if settings.background %}\n        BackgroundWorker,\n        {%- endif %}\n        Queue},\n    boot::{create_app, BootResult, StartMode},\n    config::Config,\n    controller::AppRoutes,\n    {%- if settings.auth %}\n    db::{self, truncate_table},\n    {%- endif %}\n    environment::Environment,\n    task::Tasks,\n    Result,\n};\n{%- if settings.db %}\nuse migration::Migrator;\n{%- endif %}\n\n#[allow(unused_imports)]\nuse crate::{\n    controllers ,tasks\n    {%- if settings.initializers -%}\n    , initializers\n    {%- endif %} \n    {%- if settings.auth %}\n    , models::_entities::users\n    {%- endif %}\n    {%- if settings.background %}\n    , workers::downloader::DownloadWorker\n    {%- endif %}\n};\n\npub struct App;\n#[async_trait]\nimpl Hooks for App {\n    fn app_name() -> &'static str {\n        env!(\"CARGO_CRATE_NAME\")\n    }\n\n    fn app_version() -> String {\n        format!(\n            \"{} ({})\",\n            env!(\"CARGO_PKG_VERSION\"),\n            option_env!(\"BUILD_SHA\")\n                .or(option_env!(\"GITHUB_SHA\"))\n                .unwrap_or(\"dev\")\n        )\n    }\n\n    async fn boot(mode: StartMode, environment: &Environment, config: Config) -> Result<BootResult> {\n        {%- if settings.db %}\n        create_app::<Self, Migrator>(mode, environment, config).await\n        {% else %}\n        create_app::<Self>(mode, environment, config).await\n        {%- endif %}\n    }\n\n    async fn initializers(_ctx: &AppContext) -> Result<Vec<Box<dyn Initializer>>> {\n        Ok(vec![\n        {%- if settings.initializers.view_engine -%}\n        Box::new(initializers::view_engine::ViewEngineInitializer)\n        {%- endif -%}\n        ])\n    }\n\n    fn routes(_ctx: &AppContext) -> AppRoutes {\n        AppRoutes::with_default_routes() // controller routes below\n        {%- if settings.auth %}\n            .add_route(controllers::auth::routes())\n        {%- else %}\n            .add_route(controllers::home::routes())\n        {%- endif %}\n    }\n\n    {%- if settings.background %}\n    async fn connect_workers(ctx: &AppContext, queue: &Queue) -> Result<()> {\n    {%- else %}\n    async fn connect_workers(_ctx: &AppContext, _queue: &Queue) -> Result<()> {\n    {%- endif %} \n        {%- if settings.background %}\n        queue.register(DownloadWorker::build(ctx)).await?;\n        {%- endif %}\n        Ok(())\n    }\n\n    #[allow(unused_variables)]\n    fn register_tasks(tasks: &mut Tasks) {\n        // tasks-inject (do not remove)\n        {%- if settings.auth %}\n        tasks.register(tasks::user_create::UserCreate);\n        {%- endif %} \n    }\n\n    {%- if settings.db %}\n\n    {%- if settings.auth %}\n    async fn truncate(ctx: &AppContext) -> Result<()> {\n    {%- else %}\n    async fn truncate(_ctx: &AppContext) -> Result<()> {\n    {%- endif %} \n        {%- if settings.auth %}\n        truncate_table(&ctx.db, users::Entity).await?;\n        {%- endif %}\n        Ok(())\n    }\n\n    {%- if settings.auth %}\n    async fn seed(ctx: &AppContext, base: &Path) -> Result<()> {\n    {%- else %} \n    async fn seed(_ctx: &AppContext, _base: &Path) -> Result<()> {\n    {%- endif %} \n        {%- if settings.auth %}\n        db::seed::<users::ActiveModel>(&ctx.db, &base.join(\"users.yaml\").display().to_string()).await?;\n        {%- endif %}\n        Ok(())\n    }\n    {%- endif %}\n}\n"
  },
  {
    "path": "loco-new/base_template/src/bin/main.rs.t",
    "content": "use loco_rs::cli;\nuse {{settings.module_name}}::app::App;\n{%- if settings.db %}\nuse migration::Migrator;\n{%- endif %}\n\n#[tokio::main]\nasync fn main() -> loco_rs::Result<()> {\n    {%- if settings.db %}\n    cli::main::<App, Migrator>().await\n    {%- else %}\n    cli::main::<App>().await\n    {%- endif %}\n}\n"
  },
  {
    "path": "loco-new/base_template/src/bin/tool.rs.t",
    "content": "use loco_rs::cli;\nuse {{settings.module_name}}::app::App;\n{%- if settings.db %}\nuse migration::Migrator;\n{%- endif %}\n\n#[tokio::main]\nasync fn main() -> loco_rs::Result<()> {\n    {%- if settings.db %}\n    cli::main::<App, Migrator>().await\n    {%- else %}\n    cli::main::<App>().await    \n    {%- endif %}\n}\n"
  },
  {
    "path": "loco-new/base_template/src/controllers/auth.rs",
    "content": "use crate::{\n    mailers::auth::AuthMailer,\n    models::{\n        _entities::users,\n        users::{LoginParams, RegisterParams},\n    },\n    views::auth::{CurrentResponse, LoginResponse},\n};\nuse loco_rs::prelude::*;\nuse regex::Regex;\nuse serde::{Deserialize, Serialize};\nuse std::sync::OnceLock;\n\npub static EMAIL_DOMAIN_RE: OnceLock<Regex> = OnceLock::new();\n\nfn get_allow_email_domain_re() -> &'static Regex {\n    EMAIL_DOMAIN_RE.get_or_init(|| {\n        Regex::new(r\"@example\\.com$|@gmail\\.com$\").expect(\"Failed to compile regex\")\n    })\n}\n\n#[derive(Debug, Deserialize, Serialize)]\npub struct ForgotParams {\n    pub email: String,\n}\n\n#[derive(Debug, Deserialize, Serialize)]\npub struct ResetParams {\n    pub token: String,\n    pub password: String,\n}\n\n#[derive(Debug, Deserialize, Serialize)]\npub struct MagicLinkParams {\n    pub email: String,\n}\n\n#[derive(Debug, Deserialize, Serialize)]\npub struct ResendVerificationParams {\n    pub email: String,\n}\n\n/// Register function creates a new user with the given parameters and sends a\n/// welcome email to the user\n#[debug_handler]\nasync fn register(\n    State(ctx): State<AppContext>,\n    Json(params): Json<RegisterParams>,\n) -> Result<Response> {\n    let res = users::Model::create_with_password(&ctx.db, &params).await;\n\n    let user = match res {\n        Ok(user) => user,\n        Err(err) => {\n            tracing::info!(\n                message = err.to_string(),\n                user_email = &params.email,\n                \"could not register user\",\n            );\n            return format::json(());\n        }\n    };\n\n    let user = user\n        .into_active_model()\n        .set_email_verification_sent(&ctx.db)\n        .await?;\n\n    AuthMailer::send_welcome(&ctx, &user).await?;\n\n    format::json(())\n}\n\n/// Verify register user. if the user not verified his email, he can't login to\n/// the system.\n#[debug_handler]\nasync fn verify(State(ctx): State<AppContext>, Path(token): Path<String>) -> Result<Response> {\n    let Ok(user) = users::Model::find_by_verification_token(&ctx.db, &token).await else {\n        return unauthorized(\"invalid token\");\n    };\n\n    if user.email_verified_at.is_some() {\n        tracing::info!(pid = user.pid.to_string(), \"user already verified\");\n    } else {\n        let active_model = user.into_active_model();\n        let user = active_model.verified(&ctx.db).await?;\n        tracing::info!(pid = user.pid.to_string(), \"user verified\");\n    }\n\n    format::json(())\n}\n\n/// In case the user forgot his password  this endpoints generate a forgot token\n/// and send email to the user. In case the email not found in our DB, we are\n/// returning a valid request for for security reasons (not exposing users DB\n/// list).\n#[debug_handler]\nasync fn forgot(\n    State(ctx): State<AppContext>,\n    Json(params): Json<ForgotParams>,\n) -> Result<Response> {\n    let Ok(user) = users::Model::find_by_email(&ctx.db, &params.email).await else {\n        // we don't want to expose our users email. if the email is invalid we still\n        // returning success to the caller\n        return format::json(());\n    };\n\n    let user = user\n        .into_active_model()\n        .set_forgot_password_sent(&ctx.db)\n        .await?;\n\n    AuthMailer::forgot_password(&ctx, &user).await?;\n\n    format::json(())\n}\n\n/// reset user password by the given parameters\n#[debug_handler]\nasync fn reset(State(ctx): State<AppContext>, Json(params): Json<ResetParams>) -> Result<Response> {\n    let Ok(user) = users::Model::find_by_reset_token(&ctx.db, &params.token).await else {\n        // we don't want to expose our users email. if the email is invalid we still\n        // returning success to the caller\n        tracing::info!(\"reset token not found\");\n\n        return format::json(());\n    };\n    user.into_active_model()\n        .reset_password(&ctx.db, &params.password)\n        .await?;\n\n    format::json(())\n}\n\n/// Creates a user login and returns a token\n#[debug_handler]\nasync fn login(State(ctx): State<AppContext>, Json(params): Json<LoginParams>) -> Result<Response> {\n    let Ok(user) = users::Model::find_by_email(&ctx.db, &params.email).await else {\n        tracing::debug!(\n            email = params.email,\n            \"login attempt with non-existent email\"\n        );\n        return unauthorized(\"Invalid credentials!\");\n    };\n\n    let valid = user.verify_password(&params.password);\n\n    if !valid {\n        return unauthorized(\"unauthorized!\");\n    }\n\n    let jwt_secret = ctx.config.get_jwt_config()?;\n\n    let token = user\n        .generate_jwt(&jwt_secret.secret, jwt_secret.expiration)\n        .or_else(|_| unauthorized(\"unauthorized!\"))?;\n\n    format::json(LoginResponse::new(&user, &token))\n}\n\n#[debug_handler]\nasync fn current(auth: auth::JWT, State(ctx): State<AppContext>) -> Result<Response> {\n    let user = users::Model::find_by_pid(&ctx.db, &auth.claims.pid).await?;\n    format::json(CurrentResponse::new(&user))\n}\n\n/// Magic link authentication provides a secure and passwordless way to log in to the application.\n///\n/// # Flow\n/// 1. **Request a Magic Link**:\n///    A registered user sends a POST request to `/magic-link` with their email.\n///    If the email exists, a short-lived, one-time-use token is generated and sent to the user's email.\n///    For security and to avoid exposing whether an email exists, the response always returns 200, even if the email is invalid.\n///\n/// 2. **Click the Magic Link**:\n///    The user clicks the link (/magic-link/{token}), which validates the token and its expiration.\n///    If valid, the server generates a JWT and responds with a [`LoginResponse`].\n///    If invalid or expired, an unauthorized response is returned.\n///\n/// This flow enhances security by avoiding traditional passwords and providing a seamless login experience.\nasync fn magic_link(\n    State(ctx): State<AppContext>,\n    Json(params): Json<MagicLinkParams>,\n) -> Result<Response> {\n    let email_regex = get_allow_email_domain_re();\n    if !email_regex.is_match(&params.email) {\n        tracing::debug!(\n            email = params.email,\n            \"The provided email is invalid or does not match the allowed domains\"\n        );\n        return bad_request(\"invalid request\");\n    }\n\n    let Ok(user) = users::Model::find_by_email(&ctx.db, &params.email).await else {\n        // we don't want to expose our users email. if the email is invalid we still\n        // returning success to the caller\n        tracing::debug!(email = params.email, \"user not found by email\");\n        return format::empty_json();\n    };\n\n    let user = user.into_active_model().create_magic_link(&ctx.db).await?;\n    AuthMailer::send_magic_link(&ctx, &user).await?;\n\n    format::empty_json()\n}\n\n/// Verifies a magic link token and authenticates the user.\nasync fn magic_link_verify(\n    Path(token): Path<String>,\n    State(ctx): State<AppContext>,\n) -> Result<Response> {\n    let Ok(user) = users::Model::find_by_magic_token(&ctx.db, &token).await else {\n        // we don't want to expose our users email. if the email is invalid we still\n        // returning success to the caller\n        return unauthorized(\"unauthorized!\");\n    };\n\n    let user = user.into_active_model().clear_magic_link(&ctx.db).await?;\n\n    let jwt_secret = ctx.config.get_jwt_config()?;\n\n    let token = user\n        .generate_jwt(&jwt_secret.secret, jwt_secret.expiration)\n        .or_else(|_| unauthorized(\"unauthorized!\"))?;\n\n    format::json(LoginResponse::new(&user, &token))\n}\n\n#[debug_handler]\nasync fn resend_verification_email(\n    State(ctx): State<AppContext>,\n    Json(params): Json<ResendVerificationParams>,\n) -> Result<Response> {\n    let Ok(user) = users::Model::find_by_email(&ctx.db, &params.email).await else {\n        tracing::info!(\n            email = params.email,\n            \"User not found for resend verification\"\n        );\n        return format::json(());\n    };\n\n    if user.email_verified_at.is_some() {\n        tracing::info!(\n            pid = user.pid.to_string(),\n            \"User already verified, skipping resend\"\n        );\n        return format::json(());\n    }\n\n    let user = user\n        .into_active_model()\n        .set_email_verification_sent(&ctx.db)\n        .await?;\n\n    AuthMailer::send_welcome(&ctx, &user).await?;\n    tracing::info!(pid = user.pid.to_string(), \"Verification email re-sent\");\n\n    format::json(())\n}\n\npub fn routes() -> Routes {\n    Routes::new()\n        .prefix(\"/api/auth\")\n        .add(\"/register\", post(register))\n        .add(\"/verify/{token}\", get(verify))\n        .add(\"/login\", post(login))\n        .add(\"/forgot\", post(forgot))\n        .add(\"/reset\", post(reset))\n        .add(\"/current\", get(current))\n        .add(\"/magic-link\", post(magic_link))\n        .add(\"/magic-link/{token}\", get(magic_link_verify))\n        .add(\"/resend-verification-mail\", post(resend_verification_email))\n}\n"
  },
  {
    "path": "loco-new/base_template/src/controllers/home.rs",
    "content": "use loco_rs::prelude::*;\n\nuse crate::views::home::HomeResponse;\n\n#[debug_handler]\nasync fn current() -> Result<Response> {\n    format::json(HomeResponse::new(\"loco\"))\n}\n\npub fn routes() -> Routes {\n    Routes::new().prefix(\"/api\").add(\"/\", get(current))\n}\n"
  },
  {
    "path": "loco-new/base_template/src/controllers/mod.rs.t",
    "content": "{%- if settings.auth -%} \npub mod auth;\n{%- else -%} \npub mod home;\n{%- endif -%}\n\n"
  },
  {
    "path": "loco-new/base_template/src/data/mod.rs.t",
    "content": ""
  },
  {
    "path": "loco-new/base_template/src/fixtures/users.yaml",
    "content": "---\n- id: 1\n  pid: 11111111-1111-1111-1111-111111111111\n  email: user1@example.com\n  password: \"$argon2id$v=19$m=19456,t=2,p=1$ETQBx4rTgNAZhSaeYZKOZg$eYTdH26CRT6nUJtacLDEboP0li6xUwUF/q5nSlQ8uuc\"\n  api_key: lo-95ec80d7-cb60-4b70-9b4b-9ef74cb88758\n  name: user1\n  created_at: \"2023-11-12T12:34:56.789Z\"\n  updated_at: \"2023-11-12T12:34:56.789Z\"\n- id: 2\n  pid: 22222222-2222-2222-2222-222222222222\n  email: user2@example.com\n  password: \"$argon2id$v=19$m=19456,t=2,p=1$ETQBx4rTgNAZhSaeYZKOZg$eYTdH26CRT6nUJtacLDEboP0li6xUwUF/q5nSlQ8uuc\"\n  api_key: lo-153561ca-fa84-4e1b-813a-c62526d0a77e\n  name: user2\n  created_at: \"2023-11-12T12:34:56.789Z\"\n  updated_at: \"2023-11-12T12:34:56.789Z\"\n"
  },
  {
    "path": "loco-new/base_template/src/initializers/mod.rs.t",
    "content": "{%- if settings.initializers.view_engine %}\npub mod view_engine;\n{%- endif %}"
  },
  {
    "path": "loco-new/base_template/src/initializers/view_engine.rs",
    "content": "use async_trait::async_trait;\nuse axum::{Extension, Router as AxumRouter};\nuse fluent_templates::{ArcLoader, FluentLoader};\nuse loco_rs::{\n    app::{AppContext, Initializer},\n    controller::views::{engines, ViewEngine},\n    Error, Result,\n};\nuse tracing::info;\n\nconst I18N_DIR: &str = \"assets/i18n\";\nconst I18N_SHARED: &str = \"assets/i18n/shared.ftl\";\n#[allow(clippy::module_name_repetitions)]\npub struct ViewEngineInitializer;\n\n#[async_trait]\nimpl Initializer for ViewEngineInitializer {\n    fn name(&self) -> String {\n        \"view-engine\".to_string()\n    }\n\n    async fn after_routes(&self, router: AxumRouter, _ctx: &AppContext) -> Result<AxumRouter> {\n        let tera_engine = if std::path::Path::new(I18N_DIR).exists() {\n            let arc = std::sync::Arc::new(\n                ArcLoader::builder(&I18N_DIR, unic_langid::langid!(\"en-US\"))\n                    .shared_resources(Some(&[I18N_SHARED.into()]))\n                    .customize(|bundle| bundle.set_use_isolating(false))\n                    .build()\n                    .map_err(|e| Error::string(&e.to_string()))?,\n            );\n            info!(\"locales loaded\");\n\n            engines::TeraView::build_with_post_process(move |tera| {\n                tera.register_function(\"t\", FluentLoader::new(arc.clone()));\n                Ok(())\n            })?\n        } else {\n            engines::TeraView::build()?\n        };\n\n        Ok(router.layer(Extension(ViewEngine::from(tera_engine))))\n    }\n}\n"
  },
  {
    "path": "loco-new/base_template/src/lib.rs.t",
    "content": "pub mod app;\npub mod controllers;\npub mod initializers;\npub mod data;\n{%- if settings.mailer %}\npub mod mailers;\n{%- endif %}\n{%- if settings.db %}\npub mod models;\n{%- endif %}\npub mod tasks;\npub mod views;\n{%- if settings.background %}\npub mod workers;\n{%- endif %}\n"
  },
  {
    "path": "loco-new/base_template/src/mailers/auth/forgot/html.t",
    "content": ";<html>\n\n<body>\n  Hey {{name}},\n  Forgot your password? No worries! You can reset it by clicking the link below:\n  <a href=\"{{host}}/reset#{{resetToken}}\">Reset Your Password</a>\n  If you didn't request a password reset, please ignore this email.\n  Best regards,<br>The Loco Team</br>\n</body>\n\n</html>\n"
  },
  {
    "path": "loco-new/base_template/src/mailers/auth/forgot/subject.t",
    "content": "Your reset password link\n"
  },
  {
    "path": "loco-new/base_template/src/mailers/auth/forgot/text.t",
    "content": "Reset your password with this link:\n\n{{host}}/reset#{{resetToken}}\n"
  },
  {
    "path": "loco-new/base_template/src/mailers/auth/magic_link/html.t",
    "content": ";<html>\n<body>\n<p>Magic link example:</p>\n<a href=\"{{host}}/api/auth/magic-link/{{token}}\">\nVerify Your Account\n</a>\n</body>\n</html>\n"
  },
  {
    "path": "loco-new/base_template/src/mailers/auth/magic_link/subject.t",
    "content": "Magic link example\n"
  },
  {
    "path": "loco-new/base_template/src/mailers/auth/magic_link/text.t",
    "content": "Magic link with this link: \n{{host}}/api/auth/magic-link/{{token}}"
  },
  {
    "path": "loco-new/base_template/src/mailers/auth/welcome/html.t",
    "content": ";<html>\n\n<body>\n  Dear {{name}},\n  Welcome to Loco! You can now log in to your account.\n  Before you get started, please verify your account by clicking the link below:\n  <a href=\"{{host}}/api/auth/verify/{{verifyToken}}\">\n    Verify Your Account\n  </a>\n  <p>Best regards,<br>The Loco Team</p>\n</body>\n\n</html>\n"
  },
  {
    "path": "loco-new/base_template/src/mailers/auth/welcome/subject.t",
    "content": "Welcome {{name}}\n"
  },
  {
    "path": "loco-new/base_template/src/mailers/auth/welcome/text.t",
    "content": "Welcome {{name}}, you can now log in.\n  Verify your account with the link below:\n\n  {{host}}/api/auth/verify/{{verifyToken}}\n"
  },
  {
    "path": "loco-new/base_template/src/mailers/auth.rs",
    "content": "// auth mailer\n#![allow(non_upper_case_globals)]\n\nuse loco_rs::prelude::*;\nuse serde_json::json;\n\nuse crate::models::users;\n\nstatic welcome: Dir<'_> = include_dir!(\"src/mailers/auth/welcome\");\nstatic forgot: Dir<'_> = include_dir!(\"src/mailers/auth/forgot\");\nstatic magic_link: Dir<'_> = include_dir!(\"src/mailers/auth/magic_link\");\n\n#[allow(clippy::module_name_repetitions)]\npub struct AuthMailer {}\nimpl Mailer for AuthMailer {}\nimpl AuthMailer {\n    /// Sending welcome email the the given user\n    ///\n    /// # Errors\n    ///\n    /// When email sending is failed\n    pub async fn send_welcome(ctx: &AppContext, user: &users::Model) -> Result<()> {\n        Self::mail_template(\n            ctx,\n            &welcome,\n            mailer::Args {\n                to: user.email.clone(),\n                locals: json!({\n                  \"name\": user.name,\n                  \"verifyToken\": user.email_verification_token,\n                  \"host\": ctx.config.server.full_url()\n                }),\n                ..Default::default()\n            },\n        )\n        .await?;\n\n        Ok(())\n    }\n\n    /// Sending forgot password email\n    ///\n    /// # Errors\n    ///\n    /// When email sending is failed\n    pub async fn forgot_password(ctx: &AppContext, user: &users::Model) -> Result<()> {\n        Self::mail_template(\n            ctx,\n            &forgot,\n            mailer::Args {\n                to: user.email.clone(),\n                locals: json!({\n                  \"name\": user.name,\n                  \"resetToken\": user.reset_token,\n                  \"host\": ctx.config.server.full_url()\n                }),\n                ..Default::default()\n            },\n        )\n        .await?;\n\n        Ok(())\n    }\n\n    /// Sends a magic link authentication email to the user.\n    ///\n    /// # Errors\n    ///\n    /// When email sending is failed\n    pub async fn send_magic_link(ctx: &AppContext, user: &users::Model) -> Result<()> {\n        Self::mail_template(\n            ctx,\n            &magic_link,\n            mailer::Args {\n                to: user.email.clone(),\n                locals: json!({\n                  \"name\": user.name,\n                  \"token\": user.magic_link_token.clone().ok_or_else(|| Error::string(\n                            \"the user model not contains magic link token\",\n                    ))?,\n                  \"host\": ctx.config.server.full_url()\n                }),\n                ..Default::default()\n            },\n        )\n        .await?;\n\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "loco-new/base_template/src/mailers/mod.rs",
    "content": "pub mod auth;\n"
  },
  {
    "path": "loco-new/base_template/src/models/_entities/mod.rs.t",
    "content": "//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.0\n\npub mod prelude;\n{%- if settings.auth %}\npub mod users;\n{%- endif %}"
  },
  {
    "path": "loco-new/base_template/src/models/_entities/prelude.rs.t",
    "content": "//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.0\n\n{%- if settings.auth %}\npub use super::users::Entity as Users;\n{%- endif %}"
  },
  {
    "path": "loco-new/base_template/src/models/_entities/users.rs",
    "content": "//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.0\n\nuse sea_orm::entity::prelude::*;\nuse serde::{Deserialize, Serialize};\n\n#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]\n#[sea_orm(table_name = \"users\")]\npub struct Model {\n    pub created_at: DateTimeWithTimeZone,\n    pub updated_at: DateTimeWithTimeZone,\n    #[sea_orm(primary_key)]\n    pub id: i32,\n    pub pid: Uuid,\n    #[sea_orm(unique)]\n    pub email: String,\n    pub password: String,\n    #[sea_orm(unique)]\n    pub api_key: String,\n    pub name: String,\n    pub reset_token: Option<String>,\n    pub reset_sent_at: Option<DateTimeWithTimeZone>,\n    pub email_verification_token: Option<String>,\n    pub email_verification_sent_at: Option<DateTimeWithTimeZone>,\n    pub email_verified_at: Option<DateTimeWithTimeZone>,\n    pub magic_link_token: Option<String>,\n    pub magic_link_expiration: Option<DateTimeWithTimeZone>,\n}\n\n#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]\npub enum Relation {}\n"
  },
  {
    "path": "loco-new/base_template/src/models/mod.rs.t",
    "content": "pub mod _entities;\n{%- if settings.auth %}\npub mod users;\n{%- endif %}"
  },
  {
    "path": "loco-new/base_template/src/models/users.rs",
    "content": "use async_trait::async_trait;\nuse chrono::{offset::Local, Duration};\nuse loco_rs::{auth::jwt, hash, prelude::*};\nuse serde::{Deserialize, Serialize};\nuse serde_json::Map;\nuse uuid::Uuid;\n\npub use super::_entities::users::{self, ActiveModel, Entity, Model};\n\npub const MAGIC_LINK_LENGTH: i8 = 32;\npub const MAGIC_LINK_EXPIRATION_MIN: i8 = 5;\n\n#[derive(Debug, Deserialize, Serialize)]\npub struct LoginParams {\n    pub email: String,\n    pub password: String,\n}\n\n#[derive(Debug, Deserialize, Serialize)]\npub struct RegisterParams {\n    pub email: String,\n    pub password: String,\n    pub name: String,\n}\n\n#[derive(Debug, Validate, Deserialize)]\npub struct Validator {\n    #[validate(length(min = 2, message = \"Name must be at least 2 characters long.\"))]\n    pub name: String,\n    #[validate(email(message = \"invalid email\"))]\n    pub email: String,\n}\n\nimpl Validatable for ActiveModel {\n    fn validator(&self) -> Box<dyn Validate> {\n        Box::new(Validator {\n            name: self.name.as_ref().to_owned(),\n            email: self.email.as_ref().to_owned(),\n        })\n    }\n}\n\n#[async_trait::async_trait]\nimpl ActiveModelBehavior for super::_entities::users::ActiveModel {\n    async fn before_save<C>(self, _db: &C, insert: bool) -> Result<Self, DbErr>\n    where\n        C: ConnectionTrait,\n    {\n        self.validate()?;\n        if insert {\n            let mut this = self;\n            this.pid = ActiveValue::Set(Uuid::new_v4());\n            this.api_key = ActiveValue::Set(format!(\"lo-{}\", Uuid::new_v4()));\n            Ok(this)\n        } else {\n            Ok(self)\n        }\n    }\n}\n\n#[async_trait]\nimpl Authenticable for Model {\n    async fn find_by_api_key(db: &DatabaseConnection, api_key: &str) -> ModelResult<Self> {\n        Self::find_by_api_key(db, api_key).await\n    }\n\n    async fn find_by_claims_key(db: &DatabaseConnection, claims_key: &str) -> ModelResult<Self> {\n        Self::find_by_pid(db, claims_key).await\n    }\n}\n\nimpl Model {\n    /// finds a user by the provided email\n    ///\n    /// # Errors\n    ///\n    /// When could not find user by the given token or DB query error\n    pub async fn find_by_email(db: &DatabaseConnection, email: &str) -> ModelResult<Self> {\n        let user = users::Entity::find()\n            .filter(\n                model::query::condition()\n                    .eq(users::Column::Email, email)\n                    .build(),\n            )\n            .one(db)\n            .await?;\n        user.ok_or_else(|| ModelError::EntityNotFound)\n    }\n\n    /// finds a user by the provided verification token\n    ///\n    /// # Errors\n    ///\n    /// When could not find user by the given token or DB query error\n    pub async fn find_by_verification_token(\n        db: &DatabaseConnection,\n        token: &str,\n    ) -> ModelResult<Self> {\n        let user = users::Entity::find()\n            .filter(\n                model::query::condition()\n                    .eq(users::Column::EmailVerificationToken, token)\n                    .build(),\n            )\n            .one(db)\n            .await?;\n        user.ok_or_else(|| ModelError::EntityNotFound)\n    }\n\n    /// finds a user by the magic token and verify and token expiration\n    ///\n    /// # Errors\n    ///\n    /// When could not find user by the given token or DB query error ot token expired\n    pub async fn find_by_magic_token(db: &DatabaseConnection, token: &str) -> ModelResult<Self> {\n        let user = users::Entity::find()\n            .filter(\n                query::condition()\n                    .eq(users::Column::MagicLinkToken, token)\n                    .build(),\n            )\n            .one(db)\n            .await?;\n\n        let user = user.ok_or_else(|| ModelError::EntityNotFound)?;\n        if let Some(expired_at) = user.magic_link_expiration {\n            if expired_at >= Local::now() {\n                Ok(user)\n            } else {\n                tracing::debug!(\n                    user_pid = user.pid.to_string(),\n                    token_expiration = expired_at.to_string(),\n                    \"magic token expired for the user.\"\n                );\n                Err(ModelError::msg(\"magic token expired\"))\n            }\n        } else {\n            tracing::error!(\n                user_pid = user.pid.to_string(),\n                \"magic link expiration time not exists\"\n            );\n            Err(ModelError::msg(\"expiration token not exists\"))\n        }\n    }\n\n    /// finds a user by the provided reset token\n    ///\n    /// # Errors\n    ///\n    /// When could not find user by the given token or DB query error\n    pub async fn find_by_reset_token(db: &DatabaseConnection, token: &str) -> ModelResult<Self> {\n        let user = users::Entity::find()\n            .filter(\n                model::query::condition()\n                    .eq(users::Column::ResetToken, token)\n                    .build(),\n            )\n            .one(db)\n            .await?;\n        user.ok_or_else(|| ModelError::EntityNotFound)\n    }\n\n    /// finds a user by the provided pid\n    ///\n    /// # Errors\n    ///\n    /// When could not find user  or DB query error\n    pub async fn find_by_pid(db: &DatabaseConnection, pid: &str) -> ModelResult<Self> {\n        let parse_uuid = Uuid::parse_str(pid).map_err(|e| ModelError::Any(e.into()))?;\n        let user = users::Entity::find()\n            .filter(\n                model::query::condition()\n                    .eq(users::Column::Pid, parse_uuid)\n                    .build(),\n            )\n            .one(db)\n            .await?;\n        user.ok_or_else(|| ModelError::EntityNotFound)\n    }\n\n    /// finds a user by the provided api key\n    ///\n    /// # Errors\n    ///\n    /// When could not find user by the given token or DB query error\n    pub async fn find_by_api_key(db: &DatabaseConnection, api_key: &str) -> ModelResult<Self> {\n        let user = users::Entity::find()\n            .filter(\n                model::query::condition()\n                    .eq(users::Column::ApiKey, api_key)\n                    .build(),\n            )\n            .one(db)\n            .await?;\n        user.ok_or_else(|| ModelError::EntityNotFound)\n    }\n\n    /// Verifies whether the provided plain password matches the hashed password\n    ///\n    /// # Errors\n    ///\n    /// when could not verify password\n    #[must_use]\n    pub fn verify_password(&self, password: &str) -> bool {\n        hash::verify_password(password, &self.password)\n    }\n\n    /// Asynchronously creates a user with a password and saves it to the\n    /// database.\n    ///\n    /// # Errors\n    ///\n    /// When could not save the user into the DB\n    pub async fn create_with_password(\n        db: &DatabaseConnection,\n        params: &RegisterParams,\n    ) -> ModelResult<Self> {\n        let txn = db.begin().await?;\n\n        if users::Entity::find()\n            .filter(\n                model::query::condition()\n                    .eq(users::Column::Email, &params.email)\n                    .build(),\n            )\n            .one(&txn)\n            .await?\n            .is_some()\n        {\n            return Err(ModelError::EntityAlreadyExists {});\n        }\n\n        let password_hash =\n            hash::hash_password(&params.password).map_err(|e| ModelError::Any(e.into()))?;\n        let user = users::ActiveModel {\n            email: ActiveValue::set(params.email.clone()),\n            password: ActiveValue::set(password_hash),\n            name: ActiveValue::set(params.name.clone()),\n            ..Default::default()\n        }\n        .insert(&txn)\n        .await?;\n\n        txn.commit().await?;\n\n        Ok(user)\n    }\n\n    /// Creates a JWT\n    ///\n    /// # Errors\n    ///\n    /// when could not convert user claims to jwt token\n    pub fn generate_jwt(&self, secret: &str, expiration: u64) -> ModelResult<String> {\n        jwt::JWT::new(secret)\n            .generate_token(expiration, self.pid.to_string(), Map::new())\n            .map_err(ModelError::from)\n    }\n}\n\nimpl ActiveModel {\n    /// Sets the email verification information for the user and\n    /// updates it in the database.\n    ///\n    /// This method is used to record the timestamp when the email verification\n    /// was sent and generate a unique verification token for the user.\n    ///\n    /// # Errors\n    ///\n    /// when has DB query error\n    pub async fn set_email_verification_sent(\n        mut self,\n        db: &DatabaseConnection,\n    ) -> ModelResult<Model> {\n        self.email_verification_sent_at = ActiveValue::set(Some(Local::now().into()));\n        self.email_verification_token = ActiveValue::Set(Some(Uuid::new_v4().to_string()));\n        self.update(db).await.map_err(ModelError::from)\n    }\n\n    /// Sets the information for a reset password request,\n    /// generates a unique reset password token, and updates it in the\n    /// database.\n    ///\n    /// This method records the timestamp when the reset password token is sent\n    /// and generates a unique token for the user.\n    ///\n    /// # Arguments\n    ///\n    /// # Errors\n    ///\n    /// when has DB query error\n    pub async fn set_forgot_password_sent(mut self, db: &DatabaseConnection) -> ModelResult<Model> {\n        self.reset_sent_at = ActiveValue::set(Some(Local::now().into()));\n        self.reset_token = ActiveValue::Set(Some(Uuid::new_v4().to_string()));\n        self.update(db).await.map_err(ModelError::from)\n    }\n\n    /// Records the verification time when a user verifies their\n    /// email and updates it in the database.\n    ///\n    /// This method sets the timestamp when the user successfully verifies their\n    /// email.\n    ///\n    /// # Errors\n    ///\n    /// when has DB query error\n    pub async fn verified(mut self, db: &DatabaseConnection) -> ModelResult<Model> {\n        self.email_verified_at = ActiveValue::set(Some(Local::now().into()));\n        self.update(db).await.map_err(ModelError::from)\n    }\n\n    /// Resets the current user password with a new password and\n    /// updates it in the database.\n    ///\n    /// This method hashes the provided password and sets it as the new password\n    /// for the user.\n    ///\n    /// # Errors\n    ///\n    /// when has DB query error or could not hashed the given password\n    pub async fn reset_password(\n        mut self,\n        db: &DatabaseConnection,\n        password: &str,\n    ) -> ModelResult<Model> {\n        self.password =\n            ActiveValue::set(hash::hash_password(password).map_err(|e| ModelError::Any(e.into()))?);\n        self.reset_token = ActiveValue::Set(None);\n        self.reset_sent_at = ActiveValue::Set(None);\n        self.update(db).await.map_err(ModelError::from)\n    }\n\n    /// Creates a magic link token for passwordless authentication.\n    ///\n    /// Generates a random token with a specified length and sets an expiration time\n    /// for the magic link. This method is used to initiate the magic link authentication flow.\n    ///\n    /// # Errors\n    /// - Returns an error if database update fails\n    pub async fn create_magic_link(mut self, db: &DatabaseConnection) -> ModelResult<Model> {\n        let random_str = hash::random_string(MAGIC_LINK_LENGTH as usize);\n        let expired = Local::now() + Duration::minutes(MAGIC_LINK_EXPIRATION_MIN.into());\n\n        self.magic_link_token = ActiveValue::set(Some(random_str));\n        self.magic_link_expiration = ActiveValue::set(Some(expired.into()));\n        self.update(db).await.map_err(ModelError::from)\n    }\n\n    /// Verifies and invalidates the magic link after successful authentication.\n    ///\n    /// Clears the magic link token and expiration time after the user has\n    /// successfully authenticated using the magic link.\n    ///\n    /// # Errors\n    /// - Returns an error if database update fails\n    pub async fn clear_magic_link(mut self, db: &DatabaseConnection) -> ModelResult<Model> {\n        self.magic_link_token = ActiveValue::set(None);\n        self.magic_link_expiration = ActiveValue::set(None);\n        self.update(db).await.map_err(ModelError::from)\n    }\n}\n"
  },
  {
    "path": "loco-new/base_template/src/tasks/mod.rs.t",
    "content": "{%- if settings.auth %}\npub mod user_create;\n{%- endif %}"
  },
  {
    "path": "loco-new/base_template/src/tasks/user_create.rs",
    "content": "use loco_rs::prelude::*;\n\nuse crate::{\n    mailers::auth::AuthMailer,\n    models::{_entities::users, users::RegisterParams},\n};\n\npub struct UserCreate;\n#[async_trait]\nimpl Task for UserCreate {\n    fn task(&self) -> TaskInfo {\n        TaskInfo {\n            name: \"user:create\".to_string(),\n            detail: \"Create a new user with email, name, and password. Sends welcome email and sets up email verification.\\nUsage:\\ncargo run task user:create email:user@example.com name:\\\"John Doe\\\" password:\\\"securepassword\\\"\".to_string(),\n        }\n    }\n    async fn run(&self, app_context: &AppContext, vars: &task::Vars) -> Result<()> {\n        let email = vars\n            .cli_arg(\"email\")\n            .map_err(|_| Error::string(\"email is mandatory\"))?;\n        let name = vars\n            .cli_arg(\"name\")\n            .map_err(|_| Error::string(\"name is mandatory\"))?;\n        let password = vars\n            .cli_arg(\"password\")\n            .map_err(|_| Error::string(\"password is mandatory\"))?;\n\n        let register_params = RegisterParams {\n            email: email.clone(),\n            password: password.clone(),\n            name: name.clone(),\n        };\n\n        // Create user with password using the same logic as register controller\n        let res = users::Model::create_with_password(&app_context.db, &register_params).await;\n\n        let user = match res {\n            Ok(user) => {\n                tracing::info!(\n                    message = \"User created successfully\",\n                    user_email = &register_params.email,\n                    user_pid = user.pid.to_string(),\n                    \"user created via task\"\n                );\n                user\n            }\n            Err(err) => {\n                tracing::error!(\n                    message = err.to_string(),\n                    user_email = &register_params.email,\n                    \"could not create user via task\"\n                );\n                return Err(Error::string(\n                    &format!(\"Failed to create user. err: {err}\",),\n                ));\n            }\n        };\n\n        // Set email verification sent (same as register controller)\n        let user = user\n            .into_active_model()\n            .set_email_verification_sent(&app_context.db)\n            .await\n            .map_err(|err| {\n                tracing::error!(\n                    message = err.to_string(),\n                    user_email = &register_params.email,\n                    \"could not set email verification\"\n                );\n                Error::string(\"Failed to set email verification\")\n            })?;\n\n        // Send welcome email (same as register controller)\n        AuthMailer::send_welcome(app_context, &user)\n            .await\n            .map_err(|err| {\n                tracing::error!(\n                    message = err.to_string(),\n                    user_email = &register_params.email,\n                    \"could not send welcome email\"\n                );\n                Error::string(\"Failed to send welcome email\")\n            })?;\n\n        tracing::info!(\n            message = \"User creation task completed successfully\",\n            user_email = &register_params.email,\n            user_pid = user.pid.to_string(),\n            \"user creation task finished\"\n        );\n\n        println!(\"✅ User created successfully!\");\n        println!(\"   Email: {}\", user.email);\n        println!(\"   Name: {}\", user.name);\n        println!(\"   PID: {}\", user.pid);\n\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "loco-new/base_template/src/views/auth.rs",
    "content": "use serde::{Deserialize, Serialize};\n\nuse crate::models::_entities::users;\n\n#[derive(Debug, Deserialize, Serialize)]\npub struct LoginResponse {\n    pub token: String,\n    pub pid: String,\n    pub name: String,\n    pub is_verified: bool,\n}\n\nimpl LoginResponse {\n    #[must_use]\n    pub fn new(user: &users::Model, token: &str) -> Self {\n        Self {\n            token: token.to_owned(),\n            pid: user.pid.to_string(),\n            name: user.name.clone(),\n            is_verified: user.email_verified_at.is_some(),\n        }\n    }\n}\n\n#[derive(Debug, Deserialize, Serialize)]\npub struct CurrentResponse {\n    pub pid: String,\n    pub name: String,\n    pub email: String,\n}\n\nimpl CurrentResponse {\n    #[must_use]\n    pub fn new(user: &users::Model) -> Self {\n        Self {\n            pid: user.pid.to_string(),\n            name: user.name.clone(),\n            email: user.email.clone(),\n        }\n    }\n}\n"
  },
  {
    "path": "loco-new/base_template/src/views/home.rs",
    "content": "use serde::{Deserialize, Serialize};\n\nimpl HomeResponse {\n    #[must_use]\n    pub fn new(app_name: &str) -> Self {\n        Self {\n            app_name: app_name.to_string(),\n        }\n    }\n}\n\n#[derive(Debug, Deserialize, Serialize)]\n#[allow(clippy::module_name_repetitions)]\npub struct HomeResponse {\n    pub app_name: String,\n}\n"
  },
  {
    "path": "loco-new/base_template/src/views/mod.rs.t",
    "content": "{%- if settings.auth -%} \npub mod auth;\n{%- else -%} \npub mod home;\n{%- endif -%}"
  },
  {
    "path": "loco-new/base_template/src/workers/downloader.rs",
    "content": "use loco_rs::prelude::*;\nuse serde::{Deserialize, Serialize};\n\npub struct DownloadWorker {\n    pub ctx: AppContext,\n}\n\n#[derive(Deserialize, Debug, Serialize)]\npub struct DownloadWorkerArgs {\n    pub user_guid: String,\n}\n\n#[async_trait]\nimpl BackgroundWorker<DownloadWorkerArgs> for DownloadWorker {\n    fn build(ctx: &AppContext) -> Self {\n        Self { ctx: ctx.clone() }\n    }\n    async fn perform(&self, _args: DownloadWorkerArgs) -> Result<()> {\n        // TODO: Some actual work goes here...\n\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "loco-new/base_template/src/workers/mod.rs.t",
    "content": "pub mod downloader;"
  },
  {
    "path": "loco-new/base_template/tests/mod.rs.t",
    "content": "{%- if settings.db %}\nmod models;\n{%- endif %}\nmod requests;\nmod tasks;\n{%- if settings.background %}\nmod workers;\n{%- endif %}\n\n"
  },
  {
    "path": "loco-new/base_template/tests/models/mod.rs.t",
    "content": "{%- if settings.auth %}\nmod users;\n{%- endif %}"
  },
  {
    "path": "loco-new/base_template/tests/models/snapshots/can_create_with_password@users.snap",
    "content": "---\nsource: tests/models/users.rs\nexpression: res\n---\nOk(\n    Model {\n        created_at: DATE,\n        updated_at: DATE,\n        id: ID\n        pid: PID,\n        email: \"test@framework.com\",\n        password: \"PASSWORD\",\n        api_key: \"lo-PID\",\n        name: \"framework\",\n        reset_token: None,\n        reset_sent_at: None,\n        email_verification_token: None,\n        email_verification_sent_at: None,\n        email_verified_at: None,\n        magic_link_token: None,\n        magic_link_expiration: None,\n    },\n)\n"
  },
  {
    "path": "loco-new/base_template/tests/models/snapshots/can_find_by_email@users-2.snap",
    "content": "---\nsource: tests/models/users.rs\nexpression: non_existing_user_results\n---\nErr(\n    EntityNotFound,\n)\n"
  },
  {
    "path": "loco-new/base_template/tests/models/snapshots/can_find_by_email@users.snap",
    "content": "---\nsource: tests/models/users.rs\nexpression: existing_user\n---\nOk(\n    Model {\n        created_at: 2023-11-12T12:34:56.789+00:00,\n        updated_at: 2023-11-12T12:34:56.789+00:00,\n        id: 1,\n        pid: 11111111-1111-1111-1111-111111111111,\n        email: \"user1@example.com\",\n        password: \"$argon2id$v=19$m=19456,t=2,p=1$ETQBx4rTgNAZhSaeYZKOZg$eYTdH26CRT6nUJtacLDEboP0li6xUwUF/q5nSlQ8uuc\",\n        api_key: \"lo-95ec80d7-cb60-4b70-9b4b-9ef74cb88758\",\n        name: \"user1\",\n        reset_token: None,\n        reset_sent_at: None,\n        email_verification_token: None,\n        email_verification_sent_at: None,\n        email_verified_at: None,\n        magic_link_token: None,\n        magic_link_expiration: None,\n    },\n)\n"
  },
  {
    "path": "loco-new/base_template/tests/models/snapshots/can_find_by_pid@users-2.snap",
    "content": "---\nsource: tests/models/users.rs\nexpression: non_existing_user_results\n---\nErr(\n    EntityNotFound,\n)\n"
  },
  {
    "path": "loco-new/base_template/tests/models/snapshots/can_find_by_pid@users.snap",
    "content": "---\nsource: tests/models/users.rs\nexpression: existing_user\n---\nOk(\n    Model {\n        created_at: 2023-11-12T12:34:56.789+00:00,\n        updated_at: 2023-11-12T12:34:56.789+00:00,\n        id: 1,\n        pid: 11111111-1111-1111-1111-111111111111,\n        email: \"user1@example.com\",\n        password: \"$argon2id$v=19$m=19456,t=2,p=1$ETQBx4rTgNAZhSaeYZKOZg$eYTdH26CRT6nUJtacLDEboP0li6xUwUF/q5nSlQ8uuc\",\n        api_key: \"lo-95ec80d7-cb60-4b70-9b4b-9ef74cb88758\",\n        name: \"user1\",\n        reset_token: None,\n        reset_sent_at: None,\n        email_verification_token: None,\n        email_verification_sent_at: None,\n        email_verified_at: None,\n        magic_link_token: None,\n        magic_link_expiration: None,\n    },\n)\n"
  },
  {
    "path": "loco-new/base_template/tests/models/snapshots/can_validate_model@users.snap",
    "content": "---\nsource: tests/models/users.rs\nexpression: res\n---\nErr(\n    Custom(\n        \"{\\\"email\\\":[{\\\"code\\\":\\\"email\\\",\\\"message\\\":\\\"invalid email\\\"}],\\\"name\\\":[{\\\"code\\\":\\\"length\\\",\\\"message\\\":\\\"Name must be at least 2 characters long.\\\"}]}\",\n    ),\n)\n"
  },
  {
    "path": "loco-new/base_template/tests/models/snapshots/handle_create_with_password_with_duplicate@users.snap",
    "content": "---\nsource: tests/models/users.rs\nexpression: new_user\n---\nErr(\n    EntityAlreadyExists,\n)\n"
  },
  {
    "path": "loco-new/base_template/tests/models/users.rs.t",
    "content": "use chrono::{offset::Local, Duration};\nuse insta::assert_debug_snapshot;\nuse loco_rs::testing::prelude::*;\nuse {{settings.module_name}}::{\n    app::App,\n    models::users::{self, Model, RegisterParams},\n};\nuse sea_orm::{ActiveModelTrait, ActiveValue, IntoActiveModel};\nuse serial_test::serial;\n\nmacro_rules! configure_insta {\n    ($($expr:expr),*) => {\n        let mut settings = insta::Settings::clone_current();\n        settings.set_prepend_module_to_snapshot(false);\n        settings.set_snapshot_suffix(\"users\");\n        let _guard = settings.bind_to_scope();\n    };\n}\n\n#[tokio::test]\n#[serial]\nasync fn test_can_validate_model() {\n    configure_insta!();\n\n    let boot = boot_test::<App>().await.expect(\"Failed to boot test application\");\n\n    let invalid_user = users::ActiveModel {\n        name: ActiveValue::set(\"1\".to_string()),\n        email: ActiveValue::set(\"invalid-email\".to_string()),\n        ..Default::default()\n    };\n\n    let res = invalid_user.insert(&boot.app_context.db).await;\n\n    assert_debug_snapshot!(res);\n}\n\n#[tokio::test]\n#[serial]\nasync fn can_create_with_password() {\n    configure_insta!();\n\n    let boot = boot_test::<App>().await.expect(\"Failed to boot test application\");\n\n    let params = RegisterParams {\n        email: \"test@framework.com\".to_string(),\n        password: \"1234\".to_string(),\n        name: \"framework\".to_string(),\n    };\n\n    let res = Model::create_with_password(&boot.app_context.db, &params).await;\n\n    insta::with_settings!({\n        filters => cleanup_user_model()\n    }, {\n        assert_debug_snapshot!(res);\n    });\n}\n#[tokio::test]\n#[serial]\nasync fn handle_create_with_password_with_duplicate() {\n    configure_insta!();\n\n    let boot = boot_test::<App>().await.expect(\"Failed to boot test application\");\n    seed::<App>(&boot.app_context).await.expect(\"Failed to seed database\");\n\n    let new_user = Model::create_with_password(\n        &boot.app_context.db,\n        &RegisterParams {\n            email: \"user1@example.com\".to_string(),\n            password: \"1234\".to_string(),\n            name: \"framework\".to_string(),\n        },\n    )\n    .await;\n\n    assert_debug_snapshot!(new_user);\n}\n\n#[tokio::test]\n#[serial]\nasync fn can_find_by_email() {\n    configure_insta!();\n\n    let boot = boot_test::<App>().await.expect(\"Failed to boot test application\");\n    seed::<App>(&boot.app_context).await.expect(\"Failed to seed database\");\n\n    let existing_user = Model::find_by_email(&boot.app_context.db, \"user1@example.com\").await;\n    let non_existing_user_results = Model::find_by_email(&boot.app_context.db, \"un@existing-email.com\").await;\n\n    assert_debug_snapshot!(existing_user);\n    assert_debug_snapshot!(non_existing_user_results);\n}\n\n#[tokio::test]\n#[serial]\nasync fn can_find_by_pid() {\n    configure_insta!();\n\n    let boot = boot_test::<App>().await.expect(\"Failed to boot test application\");\n    seed::<App>(&boot.app_context).await.expect(\"Failed to seed database\");\n\n    let existing_user = Model::find_by_pid(&boot.app_context.db, \"11111111-1111-1111-1111-111111111111\").await;\n    let non_existing_user_results = Model::find_by_pid(&boot.app_context.db, \"23232323-2323-2323-2323-232323232323\").await;\n\n    assert_debug_snapshot!(existing_user);\n    assert_debug_snapshot!(non_existing_user_results);\n}\n\n#[tokio::test]\n#[serial]\nasync fn can_verification_token() {\n    configure_insta!();\n\n    let boot = boot_test::<App>().await.expect(\"Failed to boot test application\");\n    seed::<App>(&boot.app_context).await.expect(\"Failed to seed database\");\n\n    let user = Model::find_by_pid(&boot.app_context.db, \"11111111-1111-1111-1111-111111111111\")\n        .await\n        .expect(\"Failed to find user by PID\");\n\n    assert!(user.email_verification_sent_at.is_none(), \"Expected no email verification sent timestamp\");\n    assert!(user.email_verification_token.is_none(), \"Expected no email verification token\");\n\n    let result = user\n        .into_active_model()\n        .set_email_verification_sent(&boot.app_context.db)\n        .await;\n\n    assert!(result.is_ok(), \"Failed to set email verification sent\");\n\n    let user = Model::find_by_pid(&boot.app_context.db, \"11111111-1111-1111-1111-111111111111\")\n        .await\n        .expect(\"Failed to find user by PID after setting verification sent\");\n\n    assert!(user.email_verification_sent_at.is_some(), \"Expected email verification sent timestamp to be present\");\n    assert!(user.email_verification_token.is_some(), \"Expected email verification token to be present\");\n}\n\n\n#[tokio::test]\n#[serial]\nasync fn can_set_forgot_password_sent() {\n    configure_insta!();\n\n    let boot = boot_test::<App>().await.expect(\"Failed to boot test application\");\n    seed::<App>(&boot.app_context).await.expect(\"Failed to seed database\");\n\n    let user = Model::find_by_pid(&boot.app_context.db, \"11111111-1111-1111-1111-111111111111\")\n        .await\n        .expect(\"Failed to find user by PID\");\n\n    assert!(user.reset_sent_at.is_none(), \"Expected no reset sent timestamp\");\n    assert!(user.reset_token.is_none(), \"Expected no reset token\");\n\n    let result = user\n        .into_active_model()\n        .set_forgot_password_sent(&boot.app_context.db)\n        .await;\n\n    assert!(result.is_ok(), \"Failed to set forgot password sent\");\n\n    let user = Model::find_by_pid(&boot.app_context.db, \"11111111-1111-1111-1111-111111111111\")\n        .await\n        .expect(\"Failed to find user by PID after setting forgot password sent\");\n\n    assert!(user.reset_sent_at.is_some(), \"Expected reset sent timestamp to be present\");\n    assert!(user.reset_token.is_some(), \"Expected reset token to be present\");\n}\n\n\n#[tokio::test]\n#[serial]\nasync fn can_verified() {\n    configure_insta!();\n\n    let boot = boot_test::<App>().await.expect(\"Failed to boot test application\");\n    seed::<App>(&boot.app_context).await.expect(\"Failed to seed database\");\n\n    let user = Model::find_by_pid(&boot.app_context.db, \"11111111-1111-1111-1111-111111111111\")\n        .await\n        .expect(\"Failed to find user by PID\");\n\n    assert!(user.email_verified_at.is_none(), \"Expected email to be unverified\");\n\n    let result = user\n        .into_active_model()\n        .verified(&boot.app_context.db)\n        .await;\n\n    assert!(result.is_ok(), \"Failed to mark email as verified\");\n\n    let user = Model::find_by_pid(&boot.app_context.db, \"11111111-1111-1111-1111-111111111111\")\n        .await\n        .expect(\"Failed to find user by PID after verification\");\n\n    assert!(user.email_verified_at.is_some(), \"Expected email to be verified\");\n}\n\n\n#[tokio::test]\n#[serial]\nasync fn can_reset_password() {\n    configure_insta!();\n\n    let boot = boot_test::<App>().await.expect(\"Failed to boot test application\");\n    seed::<App>(&boot.app_context).await.expect(\"Failed to seed database\");\n\n    let user = Model::find_by_pid(&boot.app_context.db, \"11111111-1111-1111-1111-111111111111\")\n        .await\n        .expect(\"Failed to find user by PID\");\n\n    assert!(user.verify_password(\"12341234\"), \"Password verification failed for original password\");\n\n    let result = user\n        .clone()\n        .into_active_model()\n        .reset_password(&boot.app_context.db, \"new-password\")\n        .await;\n\n    assert!(result.is_ok(), \"Failed to reset password\");\n\n    let user = Model::find_by_pid(&boot.app_context.db, \"11111111-1111-1111-1111-111111111111\")\n        .await\n        .expect(\"Failed to find user by PID after password reset\");\n\n    assert!(user.verify_password(\"new-password\"), \"Password verification failed for new password\");\n}\n\n\n#[tokio::test]\n#[serial]\nasync fn magic_link() {\n    let boot = boot_test::<App>().await.unwrap();\n    seed::<App>(&boot.app_context).await.unwrap();\n\n    let user = Model::find_by_pid(&boot.app_context.db, \"11111111-1111-1111-1111-111111111111\")\n        .await\n        .unwrap();\n\n    assert!(\n        user.magic_link_token.is_none(),\n        \"Magic link token should be initially unset\"\n    );\n    assert!(\n        user.magic_link_expiration.is_none(),\n        \"Magic link expiration should be initially unset\"\n    );\n\n    let create_result = user\n        .into_active_model()\n        .create_magic_link(&boot.app_context.db)\n        .await;\n\n    assert!(\n        create_result.is_ok(),\n        \"Failed to create magic link: {:?}\",\n        create_result.unwrap_err()\n    );\n\n    let updated_user =\n        Model::find_by_pid(&boot.app_context.db, \"11111111-1111-1111-1111-111111111111\")\n            .await\n            .expect(\"Failed to refetch user after magic link creation\");\n\n    assert!(\n        updated_user.magic_link_token.is_some(),\n        \"Magic link token should be set after creation\"\n    );\n\n    let magic_link_token = updated_user.magic_link_token.unwrap();\n    assert_eq!(\n        magic_link_token.len(),\n        users::MAGIC_LINK_LENGTH as usize,\n        \"Magic link token length does not match expected length\"\n    );\n\n    assert!(\n        updated_user.magic_link_expiration.is_some(),\n        \"Magic link expiration should be set after creation\"\n    );\n\n    let now = Local::now();\n    let should_expired_at = now + Duration::minutes(users::MAGIC_LINK_EXPIRATION_MIN.into());\n    let actual_expiration = updated_user.magic_link_expiration.unwrap();\n\n    assert!(\n        actual_expiration >= now,\n        \"Magic link expiration should be in the future or now\"\n    );\n\n    assert!(\n        actual_expiration <= should_expired_at,\n        \"Magic link expiration exceeds expected maximum expiration time\"\n    );\n}"
  },
  {
    "path": "loco-new/base_template/tests/requests/auth.rs.t",
    "content": "use insta::{assert_debug_snapshot, with_settings};\nuse loco_rs::testing::prelude::*;\nuse {{settings.module_name}}::{app::App, models::users};\nuse rstest::rstest;\nuse serial_test::serial;\n\nuse super::prepare_data;\n\n// TODO: see how to dedup / extract this to app-local test utils\n// not to framework, because that would require a runtime dep on insta\nmacro_rules! configure_insta {\n    ($($expr:expr),*) => {\n        let mut settings = insta::Settings::clone_current();\n        settings.set_prepend_module_to_snapshot(false);\n        settings.set_snapshot_suffix(\"auth_request\");\n        let _guard = settings.bind_to_scope();\n    };\n}\n\n#[tokio::test]\n#[serial]\nasync fn can_register() {\n    configure_insta!();\n\n    request::<App, _, _>(|request, ctx| async move {\n        let email = \"test@loco.com\";\n        let payload = serde_json::json!({\n            \"name\": \"loco\",\n            \"email\": email,\n            \"password\": \"12341234\"\n        });\n\n        let response = request.post(\"/api/auth/register\").json(&payload).await;\n        assert_eq!(response.status_code(), 200, \"Register request should succeed\");\n        let saved_user = users::Model::find_by_email(&ctx.db, email).await;\n\n        with_settings!({\n            filters => cleanup_user_model()\n        }, {\n            assert_debug_snapshot!(saved_user);\n        });\n\n        let deliveries = ctx.mailer.unwrap().deliveries();\n        assert_eq!(deliveries.count, 1, \"Exactly one email should be sent\");\n\n        // with_settings!({\n        //     filters => cleanup_email()\n        // }, {\n        //     assert_debug_snapshot!(ctx.mailer.unwrap().deliveries());\n        // });\n    })\n    .await;\n}\n\n#[rstest]\n#[case(\"login_with_valid_password\", \"12341234\")]\n#[case(\"login_with_invalid_password\", \"invalid-password\")]\n#[tokio::test]\n#[serial]\nasync fn can_login_with_verify(#[case] test_name: &str, #[case] password: &str) {\n    configure_insta!();\n\n    request::<App, _, _>(|request, ctx| async move {\n        let email = \"test@loco.com\";\n        let register_payload = serde_json::json!({\n            \"name\": \"loco\",\n            \"email\": email,\n            \"password\": \"12341234\"\n        });\n\n        //Creating a new user\n        let register_response = request\n            .post(\"/api/auth/register\")\n            .json(&register_payload)\n            .await;\n\n        assert_eq!(register_response.status_code(), 200, \"Register request should succeed\");\n        \n        let user = users::Model::find_by_email(&ctx.db, email).await.unwrap();\n        let email_verification_token = user.email_verification_token\n                    .expect(\"Email verification token should be generated\");\n        request.get(&format!(\"/api/auth/verify/{email_verification_token}\")).await;\n\n        //verify user request\n        let response = request\n            .post(\"/api/auth/login\")\n            .json(&serde_json::json!({\n                \"email\": email,\n                \"password\": password\n            }))\n            .await;\n\n        // Make sure email_verified_at is set\n        let user = users::Model::find_by_email(&ctx.db, email)\n            .await\n            .expect(\"Failed to find user by email\");\n\n        assert!(\n            user.email_verified_at.is_some(),\n            \"Expected the email to be verified, but it was not. User: {:?}\",\n            user\n        );\n\n        with_settings!({\n            filters => cleanup_user_model()\n        }, {\n            assert_debug_snapshot!(test_name, (response.status_code(), response.text()));\n        });\n    })\n    .await;\n}\n\n#[tokio::test]\n#[serial]\nasync fn login_with_un_existing_email() {\n    configure_insta!();\n\n    request::<App, _, _>(|request, _ctx| async move {\n\n        let login_response = request\n            .post(\"/api/auth/login\")\n            .json(&serde_json::json!({\n                \"email\": \"un_existing@loco.rs\",\n                \"password\":  \"1234\"\n            }))\n            .await;\n\n        assert_eq!(login_response.status_code(), 401, \"Login request should return 401\");\n        login_response.assert_json(&serde_json::json!({\"error\": \"unauthorized\", \"description\": \"You do not have permission to access this resource\"}));\n    })\n    .await;\n}\n\n#[tokio::test]\n#[serial]\nasync fn can_login_without_verify() {\n    configure_insta!();\n\n    request::<App, _, _>(|request, _ctx| async move {\n        let email = \"test@loco.com\";\n        let password = \"12341234\";\n        let register_payload = serde_json::json!({\n            \"name\": \"loco\",\n            \"email\": email,\n            \"password\": password\n        });\n\n        //Creating a new user\n        let register_response = request\n            .post(\"/api/auth/register\")\n            .json(&register_payload)\n            .await;\n\n        assert_eq!(register_response.status_code(), 200, \"Register request should succeed\");\n\n        //verify user request\n        let login_response = request\n            .post(\"/api/auth/login\")\n            .json(&serde_json::json!({\n                \"email\": email,\n                \"password\": password\n            }))\n            .await;\n\n        assert_eq!(login_response.status_code(), 200, \"Login request should succeed\");\n\n        with_settings!({\n            filters => cleanup_user_model()\n        }, {\n            assert_debug_snapshot!(login_response.text());\n        });\n    })\n    .await;\n}\n\n#[tokio::test]\n#[serial]\nasync fn invalid_verification_token() {\n    configure_insta!();\n\n    request::<App, _, _>(|request, _ctx| async move {\n        let response = request\n            .get(\"/api/auth/verify/invalid-token\")\n            .await;\n\n\n        assert_eq!(\n            response.status_code(),\n            401,\n            \"Verify request should reject\"\n        );\n    })\n    .await;\n}\n\n#[tokio::test]\n#[serial]\nasync fn can_reset_password() {\n    configure_insta!();\n\n    request::<App, _, _>(|request, ctx| async move {\n        let login_data = prepare_data::init_user_login(&request, &ctx).await;\n\n        let forgot_payload = serde_json::json!({\n            \"email\": login_data.user.email,\n        });\n        let forget_response = request.post(\"/api/auth/forgot\").json(&forgot_payload).await;\n        assert_eq!(forget_response.status_code(), 200, \"Forget request should succeed\");\n\n        let user = users::Model::find_by_email(&ctx.db, &login_data.user.email)\n            .await\n            .expect(\"Failed to find user by email\");\n\n        assert!(\n            user.reset_token.is_some(),\n            \"Expected reset_token to be set, but it was None. User: {user:?}\"\n        );\n        assert!(\n            user.reset_sent_at.is_some(),\n            \"Expected reset_sent_at to be set, but it was None. User: {user:?}\"\n        );\n\n        let new_password = \"new-password\";\n        let reset_payload = serde_json::json!({\n            \"token\": user.reset_token,\n            \"password\": new_password,\n        });\n\n        let reset_response = request.post(\"/api/auth/reset\").json(&reset_payload).await;\n        assert_eq!(reset_response.status_code(), 200, \"Reset password request should succeed\");\n\n        let user = users::Model::find_by_email(&ctx.db, &user.email)\n            .await\n            .unwrap();\n\n        assert!(user.reset_token.is_none());\n        assert!(user.reset_sent_at.is_none());\n\n        assert_debug_snapshot!(reset_response.text());\n\n        let login_response = request\n            .post(\"/api/auth/login\")\n            .json(&serde_json::json!({\n                \"email\": user.email,\n                \"password\": new_password\n            }))\n            .await;\n\n        assert_eq!(login_response.status_code(), 200, \"Login request should succeed\");\n\n        let deliveries = ctx.mailer.unwrap().deliveries();\n        assert_eq!(deliveries.count, 2, \"Exactly one email should be sent\");\n        // with_settings!({\n        //     filters => cleanup_email()\n        // }, {\n        //     assert_debug_snapshot!(deliveries.messages);\n        // });\n    })\n    .await;\n}\n\n#[tokio::test]\n#[serial]\nasync fn can_get_current_user() {\n    configure_insta!();\n\n    request::<App, _, _>(|request, ctx| async move {\n        let user = prepare_data::init_user_login(&request, &ctx).await;\n\n        let (auth_key, auth_value) = prepare_data::auth_header(&user.token);\n        let response = request\n            .get(\"/api/auth/current\")\n            .add_header(auth_key, auth_value)\n            .await;\n\n        assert_eq!(response.status_code(), 200, \"Current request should succeed\");\n\n        with_settings!({\n            filters => cleanup_user_model()\n        }, {\n            assert_debug_snapshot!((response.status_code(), response.text()));\n        });\n    })\n    .await;\n}\n\n#[tokio::test]\n#[serial]\nasync fn can_auth_with_magic_link() {\n    configure_insta!();\n    request::<App, _, _>(|request, ctx| async move {\n        seed::<App>(&ctx).await.unwrap();\n\n        let payload = serde_json::json!({\n            \"email\": \"user1@example.com\",\n        });\n        let response = request.post(\"/api/auth/magic-link\").json(&payload).await;\n        assert_eq!(response.status_code(), 200, \"Magic link request should succeed\");\n\n        let deliveries = ctx.mailer.unwrap().deliveries();\n        assert_eq!(deliveries.count, 1, \"Exactly one email should be sent\");\n\n        // let redact_token = format!(\"[a-zA-Z0-9]{% raw %}{{{}}}{% endraw %}\", users::MAGIC_LINK_LENGTH);\n        // with_settings!({\n        //      filters => {\n        //          let mut combined_filters = cleanup_email().clone();\n        //         combined_filters.extend(vec![(r\"(\\\\r\\\\n|=\\\\r\\\\n)\", \"\"), (redact_token.as_str(), \"[REDACT_TOKEN]\") ]);\n        //         combined_filters\n        //     }\n        // }, {\n        //     assert_debug_snapshot!(deliveries.messages);\n        // });\n\n        let user = users::Model::find_by_email(&ctx.db, \"user1@example.com\")\n            .await\n            .expect(\"User should be found\");\n\n        let magic_link_token = user.magic_link_token\n            .expect(\"Magic link token should be generated\");\n        let magic_link_response = request.get(&format!(\"/api/auth/magic-link/{magic_link_token}\")).await;\n        assert_eq!(magic_link_response.status_code(), 200, \"Magic link authentication should succeed\");\n\n        with_settings!({\n            filters => cleanup_user_model()\n        }, {\n            assert_debug_snapshot!(magic_link_response.text());\n        });\n\n    })\n    .await;\n}\n\n#[tokio::test]\n#[serial]\nasync fn can_reject_invalid_email() {\n    configure_insta!();\n    request::<App, _, _>(|request, _ctx| async move {\n        let invalid_email = \"user1@temp-mail.com\";\n        let payload = serde_json::json!({\n            \"email\": invalid_email,\n        });\n        let response = request.post(\"/api/auth/magic-link\").json(&payload).await;\n        assert_eq!(\n            response.status_code(),\n            400,\n            \"Expected request with invalid email '{invalid_email}' to be blocked, but it was allowed.\"\n        );\n    })\n    .await;\n}\n\n#[tokio::test]\n#[serial]\nasync fn can_reject_invalid_magic_link_token() {\n    configure_insta!();\n    request::<App, _, _>(|request, ctx| async move {\n        seed::<App>(&ctx).await.unwrap();\n\n        let magic_link_response = request.get(\"/api/auth/magic-link/invalid-token\").await;\n        assert_eq!(\n            magic_link_response.status_code(),\n            401,\n            \"Magic link authentication should be rejected\"\n        );\n    })\n    .await;\n}\n\n\n#[tokio::test]\n#[serial]\nasync fn can_resend_verification_email() {\n    configure_insta!();\n\n    request::<App, _, _>(|request, ctx| async move {\n        let email = \"test@loco.com\";\n        let payload = serde_json::json!({\n            \"name\": \"loco\",\n            \"email\": email,\n            \"password\": \"12341234\"\n        });\n\n        let response = request.post(\"/api/auth/register\").json(&payload).await;\n        assert_eq!(response.status_code(), 200, \"Register request should succeed\");\n\n        let resend_payload = serde_json::json!({ \"email\": email });\n\n        let resend_response = request\n            .post(\"/api/auth/resend-verification-mail\")\n            .json(&resend_payload)\n            .await;\n\n        assert_eq!(\n            resend_response.status_code(),\n            200,\n            \"Resend verification email should succeed\"\n        );\n\n        let deliveries = ctx.mailer.unwrap().deliveries();\n\n        assert_eq!(\n            deliveries.count,\n            2,\n            \"Two emails should have been sent: welcome and re-verification\"\n        );\n\n        let user = users::Model::find_by_email(&ctx.db, email)\n            .await\n            .expect(\"User should exist\");\n\n        with_settings!({\n            filters => cleanup_user_model()\n        }, {\n            assert_debug_snapshot!(\"resend_verification_user\", user);\n        });\n    }).await; \n}\n\n#[tokio::test]\n#[serial]\nasync fn cannot_resend_email_if_already_verified() {\n    configure_insta!();\n\n    request::<App, _, _>(|request, ctx| async move {\n        let email = \"verified@loco.com\";\n        let payload = serde_json::json!({\n            \"name\": \"verified\",\n            \"email\": email,\n            \"password\": \"12341234\"\n        });\n\n        request.post(\"/api/auth/register\").json(&payload).await;\n\n        // Verify user\n        let user = users::Model::find_by_email(&ctx.db, email).await.unwrap();\n        if let Some(token) = user.email_verification_token.clone() {\n            request.get(&format!(\"/api/auth/verify/{token}\")).await;\n        }\n\n        // Try resending verification email\n        let resend_payload = serde_json::json!({ \"email\": email });\n\n        let resend_response = request\n            .post(\"/api/auth/resend-verification-mail\")\n            .json(&resend_payload)\n            .await;\n\n        assert_eq!(\n            resend_response.status_code(),\n            200,\n            \"Should return 200 even if already verified\"\n        );\n\n        let deliveries = ctx.mailer.unwrap().deliveries();\n        assert_eq!(\n            deliveries.count, 1,\n            \"Only the original welcome email should be sent\"\n        );\n    })\n    .await;\n}\n"
  },
  {
    "path": "loco-new/base_template/tests/requests/home.rs.t",
    "content": "use loco_rs::testing::prelude::*;\nuse {{settings.module_name}}::app::App;\nuse serial_test::serial;\n\n#[tokio::test]\n#[serial]\nasync fn can_get_home() {\n\n    request::<App, _, _>(|request, _ctx| async move {\n        let res = request.get(\"/api\").await;\n\n        assert_eq!(res.status_code(), 200);\n        res.assert_json(&serde_json::json!({\"app_name\":\"loco\"}));\n    })\n    .await;\n}\n"
  },
  {
    "path": "loco-new/base_template/tests/requests/mod.rs.t",
    "content": "{%- if settings.auth -%} \nmod auth;\nmod prepare_data;\n{%- else -%} \nmod home;\n{%- endif -%}"
  },
  {
    "path": "loco-new/base_template/tests/requests/prepare_data.rs.t",
    "content": "use axum::http::{HeaderName, HeaderValue};\nuse loco_rs::{app::AppContext, TestServer};\nuse {{settings.module_name}}::{models::users, views::auth::LoginResponse};\n\nconst USER_EMAIL: &str = \"test@loco.com\";\nconst USER_PASSWORD: &str = \"1234\";\n\npub struct LoggedInUser {\n    pub user: users::Model,\n    pub token: String,\n}\n\npub async fn init_user_login(request: &TestServer, ctx: &AppContext) -> LoggedInUser {\n    let register_payload = serde_json::json!({\n        \"name\": \"loco\",\n        \"email\": USER_EMAIL,\n        \"password\": USER_PASSWORD\n    });\n\n    //Creating a new user\n    request\n        .post(\"/api/auth/register\")\n        .json(&register_payload)\n        .await;\n    let user = users::Model::find_by_email(&ctx.db, USER_EMAIL)\n        .await\n        .unwrap();\n\n    let verify_payload = serde_json::json!({\n        \"token\": user.email_verification_token,\n    });\n\n    request.post(\"/api/auth/verify\").json(&verify_payload).await;\n\n    let response = request\n        .post(\"/api/auth/login\")\n        .json(&serde_json::json!({\n            \"email\": USER_EMAIL,\n            \"password\": USER_PASSWORD\n        }))\n        .await;\n\n    let login_response: LoginResponse = serde_json::from_str(&response.text()).unwrap();\n\n    LoggedInUser {\n        user: users::Model::find_by_email(&ctx.db, USER_EMAIL)\n            .await\n            .unwrap(),\n        token: login_response.token,\n    }\n}\n\npub fn auth_header(token: &str) -> (HeaderName, HeaderValue) {\n    let auth_header_value = HeaderValue::from_str(&format!(\"Bearer {}\", &token)).unwrap();\n\n    (HeaderName::from_static(\"authorization\"), auth_header_value)\n}\n"
  },
  {
    "path": "loco-new/base_template/tests/requests/snapshots/can_auth_with_magic_link@auth_request.snap",
    "content": "---\nsource: tests/requests/auth.rs\nexpression: magic_link_response.text()\n---\n\"{\\\"token\\\":\\\"TOKEN\\\",\\\"pid\\\":\\\"PID\\\",\\\"name\\\":\\\"user1\\\",\\\"is_verified\\\":false}\"\n"
  },
  {
    "path": "loco-new/base_template/tests/requests/snapshots/can_get_current_user@auth_request.snap",
    "content": "---\nsource: tests/requests/auth.rs\nexpression: \"(response.status_code(), response.text())\"\n---\n(\n    200,\n    \"{\\\"pid\\\":\\\"PID\\\",\\\"name\\\":\\\"loco\\\",\\\"email\\\":\\\"test@loco.com\\\"}\",\n)\n"
  },
  {
    "path": "loco-new/base_template/tests/requests/snapshots/can_login_without_verify@auth_request.snap",
    "content": "---\nsource: tests/requests/auth.rs\nexpression: login_response.text()\n---\n\"{\\\"token\\\":\\\"TOKEN\\\",\\\"pid\\\":\\\"PID\\\",\\\"name\\\":\\\"loco\\\",\\\"is_verified\\\":false}\"\n"
  },
  {
    "path": "loco-new/base_template/tests/requests/snapshots/can_register@auth_request.snap",
    "content": "---\nsource: tests/requests/auth.rs\nexpression: saved_user\n---\nOk(\n    Model {\n        created_at: DATE,\n        updated_at: DATE,\n        id: ID\n        pid: PID,\n        email: \"test@loco.com\",\n        password: \"PASSWORD\",\n        api_key: \"lo-PID\",\n        name: \"loco\",\n        reset_token: None,\n        reset_sent_at: None,\n        email_verification_token: Some(\n            \"PID\",\n        ),\n        email_verification_sent_at: Some(\n            DATE,\n        ),\n        email_verified_at: None,\n        magic_link_token: None,\n        magic_link_expiration: None,\n    },\n)\n"
  },
  {
    "path": "loco-new/base_template/tests/requests/snapshots/can_reset_password@auth_request.snap",
    "content": "---\nsource: tests/requests/auth.rs\nexpression: \"(reset_response.status_code(), reset_response.text())\"\n---\n\"null\""
  },
  {
    "path": "loco-new/base_template/tests/requests/snapshots/login_with_invalid_password@auth_request.snap",
    "content": "---\nsource: tests/requests/auth.rs\nexpression: \"(response.status_code(), response.text())\"\n---\n(\n    401,\n    \"{\\\"error\\\":\\\"unauthorized\\\",\\\"description\\\":\\\"You do not have permission to access this resource\\\"}\",\n)\n"
  },
  {
    "path": "loco-new/base_template/tests/requests/snapshots/login_with_valid_password@auth_request.snap",
    "content": "---\nsource: tests/requests/auth.rs\nexpression: \"(response.status_code(), response.text())\"\n---\n(\n    200,\n    \"{\\\"token\\\":\\\"TOKEN\\\",\\\"pid\\\":\\\"PID\\\",\\\"name\\\":\\\"loco\\\",\\\"is_verified\\\":true}\",\n)\n"
  },
  {
    "path": "loco-new/base_template/tests/requests/snapshots/resend_verification_user@auth_request.snap",
    "content": "---\nsource: tests/requests/auth.rs\nassertion_line: 414\nexpression: user\n---\nModel {\n    created_at: DATE,\n    updated_at: DATE,\n    id: ID\n    pid: PID,\n    email: \"test@loco.com\",\n    password: \"PASSWORD\",\n    api_key: \"lo-PID\",\n    name: \"loco\",\n    reset_token: None,\n    reset_sent_at: None,\n    email_verification_token: Some(\n        \"PID\",\n    ),\n    email_verification_sent_at: Some(\n        DATE,\n    ),\n    email_verified_at: None,\n    magic_link_token: None,\n    magic_link_expiration: None,\n}\n"
  },
  {
    "path": "loco-new/base_template/tests/tasks/mod.rs.t",
    "content": "{%- if settings.auth %}\npub mod user_create;\n{%- endif %}"
  },
  {
    "path": "loco-new/base_template/tests/tasks/user_create.rs.t",
    "content": "use loco_rs::{task, testing::prelude::*};\nuse {{settings.module_name}}::{app::App, models::users};\n\nuse loco_rs::boot::run_task;\nuse serial_test::serial;\n\n#[tokio::test]\n#[serial]\nasync fn test_can_run_user_create() {\n    let boot = boot_test::<App>().await.unwrap();\n\n    let email = \"test@example.com\";\n    let user = users::Model::find_by_email(&boot.app_context.db, email).await;\n    assert!(user.is_err());\n\n    let vars = task::Vars::from_cli_args(vec![\n        (\"email\".to_string(), email.to_string()),\n        (\"name\".to_string(), \"Test User\".to_string()),\n        (\"password\".to_string(), \"securepassword\".to_string()),\n    ]);\n    assert!(\n        run_task::<App>(&boot.app_context, Some(&\"user:create\".to_string()), &vars)\n            .await\n            .is_ok()\n    );\n\n    let deliveries = boot.app_context.mailer.unwrap().deliveries();\n    assert_eq!(deliveries.count, 1, \"Exactly one email should be sent\");\n\n    let user = users::Model::find_by_email(&boot.app_context.db, email).await;\n    assert!(user.is_ok());\n}\n\n#[tokio::test]\n#[serial]\nasync fn test_user_email_already_exists() {\n    let boot = boot_test::<App>().await.unwrap();\n    seed::<App>(&boot.app_context).await.unwrap();\n\n    let email = \"user1@example.com\";\n\n    let vars = task::Vars::from_cli_args(vec![\n        (\"email\".to_string(), email.to_string()),\n        (\"name\".to_string(), \"Test User\".to_string()),\n        (\"password\".to_string(), \"securepassword\".to_string()),\n    ]);\n    let err = run_task::<App>(&boot.app_context, Some(&\"user:create\".to_string()), &vars)\n        .await\n        .expect_err(\"err\");\n\n    assert_eq!(\n        err.to_string(),\n        \"Failed to create user. err: Entity already exists\"\n    );\n\n    let deliveries = boot.app_context.mailer.unwrap().deliveries();\n    assert_eq!(deliveries.count, 0, \"No email should be sent\");\n}"
  },
  {
    "path": "loco-new/base_template/tests/workers/mod.rs",
    "content": "\n"
  },
  {
    "path": "loco-new/devnew.sh",
    "content": "LOCO_DEV_MODE_PATH=../../ cargo run -- new\n"
  },
  {
    "path": "loco-new/setup.rhai",
    "content": "// =====================\n// Base Files\n// =====================\n// Copy core project structure files and directories that are fundamental\n// to the Rust environment, GitHub actions, and formatting settings.\n\ngen.copy_template(\".github/workflows/ci.yaml.t\");     // Actions ci template\ngen.copy_files([\".gitignore\", \".rustfmt.toml\", \"README.md\"]);\n\ngen.copy_template_dir(\".cargo\");\n\n// =====================\n// Core Source Files\n// =====================\ngen.copy_template(\"src/controllers/mod.rs.t\");      // Main controller module template\ngen.copy_template(\"src/views/mod.rs.t\");            // Main views module template\ngen.copy_template(\"src/tasks/mod.rs.t\");            // Main tasks module template\ngen.copy_template(\"src/initializers/mod.rs.t\");     // initializer module\ngen.copy_template(\"src/data/mod.rs.t\");             // data loaders module\n\n\n// Main application and library templates\ngen.copy_template(\"src/app.rs.t\");                  // App root file\ngen.copy_template(\"src/lib.rs.t\");                  // Library entry file\ngen.copy_template(\"Cargo.toml.t\");                  // Project’s cargo configuration\n\n// bin\ngen.copy_template(\"src/bin/main.rs.t\");\nif windows {\n  gen.copy_template(\"src/bin/tool.rs.t\");\n}\n\n\n// =====================\n// Test Files\n// =====================\n// Generates and organizes tests modules and templates for different areas of the application.\n\ngen.copy_template(\"tests/mod.rs.t\");                // Main tests module template\ngen.copy_template(\"tests/requests/mod.rs.t\");       // HTTP requests tests module\ngen.copy_template(\"tests/tasks/mod.rs.t\");          // Tasks tests module\n\n\n// =====================\n// App Configuration\n// =====================\ngen.copy_template(\"config/development.yaml.t\");     // Development config template\ngen.copy_template(\"config/test.yaml.t\");            // Test config template\ngen.copy_file(\"config/production.yaml\");            // Production config\n\n// =====================\n// Database-Related Files\n// =====================\nif db {\n   // Database migrations configuration and setup\n    gen.copy_template(\"migration/Cargo.toml.t\");                  // Database migrations Cargo configuration\n    gen.copy_template(\"migration/src/lib.rs.t\");                  // Database migrations library\n\n    // Entity modules for database models\n    gen.copy_template(\"src/models/_entities/mod.rs.t\");            // Root module for database entities\n    gen.copy_template(\"src/models/_entities/prelude.rs.t\");            // Root module for database entities\n    gen.copy_template(\"src/models/mod.rs.t\");            // Root module for database entities\n\n    // Test modules related to database models\n    gen.copy_template(\"tests/models/mod.rs.t\");                   // Models tests root module\n\n    if (settings.auth) {\n        // Authentication-related models and migrations\n        gen.copy_file(\"migration/src/m20220101_000001_users.rs\");  // Users migration file\n        gen.copy_file(\"src/models/_entities/users.rs\");             // Users entity definition\n        gen.copy_file(\"src/models/users.rs\");                      // Users model logic\n         gen.copy_file(\"src/tasks/user_create.rs\");                      \n\n        // Fixtures and test setup for authentication\n        gen.copy_dir(\"src/fixtures\");                              // Database fixtures directory\n\n        // Test modules related to user models\n        gen.copy_dir(\"tests/models/snapshots\");                    // Test snapshots for models\n        gen.copy_template(\"tests/models/users.rs.t\");              // User model test template\n        gen.copy_template(\"tests/requests/prepare_data.rs.t\");     // Data preparation template for tests\n        gen.copy_template(\"tests/tasks/user_create.rs.t\");     \n\n    }\n   \n   gen.copy_template(\"examples/playground.rs.t\");                 // Example playground template with DB setup\n}\n\n// =====================\n// Initializers Support\n// =====================\nif settings.initializers?.view_engine ?? false {\n   gen.copy_file(\"src/initializers/view_engine.rs\"); // Template for view engine initializer\n}\n\n// =====================\n// Authentication Setup\n// =====================\nif settings.auth {\n   gen.copy_file(\"src/controllers/auth.rs\");        // Auth controller\n   gen.copy_file(\"src/views/auth.rs\");              // Auth views\n\n   gen.copy_template(\"tests/requests/auth.rs.t\");   // Auth tests template\n   gen.copy_dir(\"tests/requests/snapshots\");        // Snapshots directory for auth tests\n} \nelse {\n   gen.copy_file(\"src/controllers/home.rs\");        // Home controller if auth not enabled\n   gen.copy_file(\"src/views/home.rs\");              // Home views\n   gen.copy_template(\"tests/requests/home.rs.t\");   // Home tests template\n}\n\n// =====================\n// Mailer Setup\n// =====================\nif settings.mailer {\n    gen.copy_dir(\"src/mailers\");                    // Mailers directory, copied if enabled\n}\n\n// =====================\n// Background Processing\n// =====================\ngen.copy_template(\"src/workers/mod.rs.t\");       // Workers directory\ngen.copy_dir(\"tests/workers\");                   // Workers test directory\ngen.copy_file(\"src/workers/downloader.rs\");\n\n// =====================\n// Server side\n// =====================\nif settings.asset?.is_server_side ?? false {\n    gen.copy_dir(\"assets\");                        // Static assets directory\n}\n\n// =====================\n// Client side\n// =====================\nif settings.asset?.is_client_side ?? false {\n   gen.copy_dir(\"frontend\");\n   gen.create_file(\"frontend/dist/index.html\", \"this is a placeholder. please run your frontend build (npm build)\");\n}\n"
  },
  {
    "path": "loco-new/src/bin/main.rs",
    "content": "use std::{\n    env,\n    path::{Path, PathBuf},\n    process::{exit, Command},\n    sync::Arc,\n};\n\nuse clap::{Parser, Subcommand};\nuse duct::cmd;\nuse loco::{\n    generator::{executer, extract_default_template, Generator},\n    settings::Settings,\n    wizard, Result, OS,\n};\nuse tracing::level_filters::LevelFilter;\nuse tracing_subscriber::EnvFilter;\n\n#[derive(Parser)]\n#[command(author, version, about, long_about = None)]\n#[command(propagate_version = true)]\nstruct Cli {\n    #[arg(global = true, short, long, value_enum, default_value = \"ERROR\")]\n    /// Verbosity level\n    log: LevelFilter,\n\n    #[command(subcommand)]\n    command: Commands,\n}\n\n#[derive(Subcommand)]\nenum Commands {\n    /// Create a new Loco app\n    New {\n        /// Local path to generate into\n        #[arg(short, long, default_value = \".\")]\n        path: PathBuf,\n\n        /// App name\n        #[arg(short, long)]\n        name: Option<String>,\n\n        /// DB Provider\n        #[arg(long)]\n        db: Option<wizard::DBOption>,\n\n        /// Background worker configuration\n        #[arg(long)]\n        bg: Option<wizard::BackgroundOption>,\n\n        /// Assets serving configuration\n        #[arg(long)]\n        assets: Option<wizard::AssetsOption>,\n\n        /// Create the starter in target git repository\n        #[arg(short, long)]\n        allow_in_git_repo: bool,\n\n        /// Create a Unix (linux, mac) or Windows optimized starter\n        #[arg(long, default_value = DEFAULT_OS)]\n        os: OS,\n    },\n}\n\n#[cfg(unix)]\nconst DEFAULT_OS: &str = \"linux\";\n#[cfg(not(unix))]\nconst DEFAULT_OS: &str = \"windows\";\n\n#[allow(clippy::cognitive_complexity)]\nfn main() -> Result<()> {\n    let cli = Cli::parse();\n    tracing_subscriber::fmt()\n        .with_env_filter(\n            EnvFilter::builder()\n                .with_default_directive(cli.log.into())\n                .from_env_lossy(),\n        )\n        .init();\n\n    let res = match cli.command {\n        Commands::New {\n            path,\n            db,\n            bg,\n            assets,\n            name,\n            allow_in_git_repo,\n            os,\n        } => {\n            tracing::debug!(path = ?path, db = ?db, bg=?bg, assets=?assets,name=?name, allow_in_git_repo=allow_in_git_repo, os=?os, \"CLI options\");\n            if !allow_in_git_repo && is_a_git_repo(path.as_path()).unwrap_or(false) {\n                tracing::debug!(\"the target directory is a Git repository\");\n                wizard::warn_if_in_git_repo()?;\n            }\n\n            let app_name = wizard::app_name(name)?;\n\n            let to: PathBuf = path.canonicalize()?.join(&app_name);\n\n            if to.exists() {\n                CmdExit::error_with_message(format!(\n                    \"The specified path '{}' already exist\",\n                    to.display()\n                ))\n            } else {\n                tracing::debug!(dir = %to.display(), \"creating application directory\");\n                let temp_to = tree_fs::TreeBuilder::default().create()?;\n\n                let args = wizard::ArgsPlaceholder { db, bg, assets };\n                let user_selection = wizard::start(&args)?;\n\n                let generator_tmp_folder = extract_default_template()?;\n                tracing::debug!(\n                    dir = %generator_tmp_folder.root.display(),\n                    \"temporary template folder created\",\n\n                );\n\n                let executor = executer::FileSystem::new(\n                    generator_tmp_folder.root.as_path(),\n                    temp_to.root.as_path(),\n                );\n\n                let settings = Settings::from_wizard(&app_name, &user_selection, os);\n\n                if let Ok(path) = env::var(\"LOCO_DEV_MODE_PATH\") {\n                    println!(\"⚠️ NOTICE: working in dev mode, pointing to local Loco on '{path}'\");\n                }\n\n                let res = match Generator::new(Arc::new(executor), settings).run() {\n                    Ok(()) => {\n                        std::fs::create_dir_all(&to)?;\n                        let copy_options = fs_extra::dir::CopyOptions::new().content_only(true);\n                        fs_extra::dir::copy(&temp_to.root, &to, &copy_options)?;\n                        tracing::debug!(\"loco template app generated successfully\",);\n                        if let Err(err) = cmd!(\"cargo\", \"fmt\")\n                            .dir(&to)\n                            .stdout_null()\n                            .stderr_null()\n                            .run()\n                        {\n                            tracing::debug!(dir = %to.display(), err = %err,\"failed to run 'cargo fmt'\");\n                        }\n\n                        CmdExit::ok_with_message(format!(\n                            \"\\n🚂 Loco app generated successfully in:\\n{}\\n\\n{}\",\n                            to.display(),\n                            user_selection\n                                .message()\n                                .iter()\n                                .map(|m| format!(\"- {m}\"))\n                                .collect::<Vec<_>>()\n                                .join(\"\\n\")\n                        ))\n                    }\n                    Err(err) => {\n                        tracing::error!(error = %err, args = format!(\"{args:?}\"), \"app generation failed due to template error.\");\n                        CmdExit::error_with_message(\"generate template failed\")\n                    }\n                };\n\n                if let Err(err) = std::fs::remove_dir_all(&generator_tmp_folder.root) {\n                    tracing::warn!(\n                        error = %err,\n                        dir = %generator_tmp_folder.root.display(),\n                        \"failed to delete temporary generator folder\"\n                    );\n                }\n                res\n            }\n        }\n    };\n\n    res.exit();\n    Ok(())\n}\n\n/// Check if a given path is a Git repository\n///\n/// # Errors\n///\n/// when git binary is not found or could not canonicalize the given path\npub fn is_a_git_repo(destination_path: &Path) -> Result<bool> {\n    let destination_path = destination_path.canonicalize()?;\n    match Command::new(\"git\")\n        .arg(\"-C\")\n        .arg(destination_path)\n        .arg(\"rev-parse\")\n        .arg(\"--is-inside-work-tree\")\n        .output()\n    {\n        Ok(output) => {\n            if output.status.success() {\n                Ok(true)\n            } else {\n                Ok(false)\n            }\n        }\n        Err(err) => {\n            tracing::debug!(error = err.to_string(), \"git not found\");\n            Ok(false)\n        }\n    }\n}\n\n#[derive(Debug)]\npub struct CmdExit {\n    pub code: i32,\n    pub message: Option<String>,\n}\n\nimpl CmdExit {\n    #[must_use]\n    pub fn error_with_message<S: Into<String>>(msg: S) -> Self {\n        Self {\n            code: 1,\n            message: Some(format!(\"🙀 {}\", msg.into())),\n        }\n    }\n\n    #[must_use]\n    pub fn ok_with_message<S: Into<String>>(msg: S) -> Self {\n        Self {\n            code: 0,\n            message: Some(msg.into()),\n        }\n    }\n\n    pub fn exit(&self) {\n        if let Some(message) = &self.message {\n            eprintln!(\"{message}\");\n        }\n\n        exit(self.code);\n    }\n}\n"
  },
  {
    "path": "loco-new/src/generator/executer/filesystem.rs",
    "content": "use std::path::{Path, PathBuf};\n\nuse fs_extra::file::{move_file, write_all};\nuse walkdir::WalkDir;\n\nuse super::Executer;\nuse crate::{generator, settings::Settings};\n\n#[derive(Debug, Default, Clone)]\npub struct FileSystem {\n    pub source_dir: PathBuf,\n    pub target_dir: PathBuf,\n    pub template_engine: generator::template::Template,\n}\n\nimpl FileSystem {\n    #[must_use]\n    pub fn new(from: &Path, to: &Path) -> Self {\n        Self {\n            source_dir: from.to_path_buf(),\n            target_dir: to.to_path_buf(),\n            template_engine: generator::template::Template::default(),\n        }\n    }\n\n    #[must_use]\n    pub fn with_template_engine(\n        from: &Path,\n        to: &Path,\n        template_engine: generator::template::Template,\n    ) -> Self {\n        Self {\n            source_dir: from.to_path_buf(),\n            target_dir: to.to_path_buf(),\n            template_engine,\n        }\n    }\n\n    fn render_and_rename_template_file(\n        &self,\n        file_path: &Path,\n        settings: &Settings,\n    ) -> super::Result<()> {\n        let template_content = fs_extra::file::read_to_string(file_path).map_err(|err| {\n            tracing::debug!(err = %err, \"failed to read template file\");\n            err\n        })?;\n        let rendered_content = self.template_engine.render(&template_content, settings)?;\n        write_all(file_path, &rendered_content).map_err(|err| {\n            tracing::debug!(err = %err, \"failed to write rendered content to file\");\n            err\n        })?;\n\n        let renamed_path = self\n            .template_engine\n            .strip_template_extension(file_path)\n            .map_err(|err| {\n                tracing::debug!(err = %err, \"error stripping template extension from file\");\n                super::Error::msg(\"error striping template file\")\n            })?;\n        move_file(file_path, renamed_path, &fs_extra::file::CopyOptions::new())?;\n        Ok(())\n    }\n}\n\nimpl Executer for FileSystem {\n    fn copy_file(&self, path: &Path) -> super::Result<PathBuf> {\n        let source_path = self.source_dir.join(path);\n        let target_path = self.target_dir.join(path);\n\n        let span = tracing::error_span!(\"copy_file\", source_path = %source_path.display(), target_path = %target_path.display());\n        let _guard = span.enter();\n\n        tracing::debug!(\"starting file copy operation\");\n\n        fs_extra::dir::create_all(target_path.parent().unwrap(), false).map_err(|error| {\n            tracing::debug!(error = %error, \"error creating target parent directory\");\n            error\n        })?;\n\n        let copy_options = fs_extra::file::CopyOptions::new();\n        fs_extra::file::copy(source_path, &target_path, &copy_options)?;\n        tracing::debug!(\"file copy completed successfully\");\n\n        Ok(target_path)\n    }\n\n    fn create_file(&self, path: &Path, content: String) -> super::Result<PathBuf> {\n        let target_path = self.target_dir.join(path);\n        if let Some(parent) = path.parent() {\n            fs_extra::dir::create_all(self.target_dir.join(parent), false)?;\n        }\n\n        let span = tracing::info_span!(\"create_file\", target_path = %target_path.display());\n        let _guard = span.enter();\n\n        tracing::debug!(\"starting file copy operation\");\n\n        fs_extra::dir::create_all(target_path.parent().unwrap(), false).map_err(|error| {\n            tracing::debug!(error = %error, \"error creating target parent directory\");\n            error\n        })?;\n\n        fs_extra::file::write_all(&target_path, &content)?;\n        tracing::debug!(\"file created successfully\");\n\n        Ok(target_path)\n    }\n\n    fn copy_dir(&self, directory_path: &Path) -> super::Result<()> {\n        let source_path = self.source_dir.join(directory_path);\n        let target_path = self.target_dir.join(directory_path);\n\n        let span = tracing::error_span!(\"\", source_path = %source_path.display(), target_path = %target_path.display());\n        let _guard = span.enter();\n\n        tracing::debug!(\"starting directory copy operation\");\n        let copy_options = fs_extra::dir::CopyOptions::new().copy_inside(true);\n        fs_extra::dir::copy(source_path, target_path, &copy_options)?;\n        tracing::debug!(\"directory copy completed successfully\");\n        Ok(())\n    }\n\n    fn copy_template(&self, file_path: &Path, settings: &Settings) -> super::Result<()> {\n        let span = tracing::error_span!(\"copy_template\", file_path = %file_path.display());\n        let _guard: tracing::span::Entered<'_> = span.enter();\n        if !self.template_engine.is_template(file_path) {\n            tracing::debug!(\"file is not a template, skipping rendering\");\n            return Err(super::Error::msg(\"File is not a template\"));\n        }\n\n        //todo fix the if here\n        tracing::debug!(\"copying template file\");\n\n        let copied_path = self.copy_file(file_path)?;\n        self.render_and_rename_template_file(&copied_path, settings)\n    }\n\n    #[allow(clippy::cognitive_complexity)]\n    fn copy_template_dir(&self, directory_path: &Path, settings: &Settings) -> super::Result<()> {\n        let source_path = self.source_dir.join(directory_path);\n        let target_path = self.target_dir.join(directory_path);\n\n        let span = tracing::error_span!(\"copy_template_dir\", source_path = %source_path.display(), target_path = %target_path.display());\n        let _guard: tracing::span::Entered<'_> = span.enter();\n\n        tracing::debug!(\"starting template directory copy operation\");\n\n        let copy_options = fs_extra::dir::CopyOptions::new().copy_inside(true);\n        fs_extra::dir::copy(source_path, target_path, &copy_options)?;\n\n        tracing::debug!(\"scanning copied directory for template files to render\");\n        for entry in WalkDir::new(self.target_dir.join(directory_path))\n            .into_iter()\n            .filter_map(Result::ok)\n        {\n            let path = entry.path();\n            if self.template_engine.is_template(path) {\n                tracing::debug!(template_path = %path.display(), \"rendering template file in directory\");\n                self.render_and_rename_template_file(path, settings)?;\n            } else {\n                tracing::debug!(file_path = %path.display(), \"not a template file\");\n            }\n        }\n\n        Ok(())\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use tree_fs::TreeBuilder;\n\n    use super::*;\n\n    fn init_filesystem() -> (FileSystem, tree_fs::Tree) {\n        let tree_fs = TreeBuilder::default()\n            .add(\"test/foo.txt\", \"bar\")\n            .add(\"test/bar.txt.t\", \"crate: {{settings.package_name}}\")\n            .create()\n            .expect(\"Failed to create mock data\");\n\n        let copy_to = TreeBuilder::default()\n            .create()\n            .expect(\"Failed to create mock data\");\n        (FileSystem::new(&tree_fs.root, &copy_to.root), tree_fs)\n    }\n\n    #[test]\n    fn can_copy_file() {\n        let (fs, _tree_fs) = init_filesystem();\n\n        assert!(fs.copy_file(&PathBuf::from(\"test\").join(\"foo.txt\")).is_ok());\n        let copied_path = fs.target_dir.join(\"test\").join(\"foo.txt\");\n        assert!(copied_path.exists());\n        assert_eq!(\n            fs_extra::file::read_to_string(copied_path).expect(\"read content\"),\n            \"bar\"\n        );\n    }\n\n    #[test]\n    fn can_copy_dir() {\n        let (fs, _tree_fs) = init_filesystem();\n        assert!(fs.copy_dir(&PathBuf::from(\"test\")).is_ok());\n        let copied_path_1 = fs.target_dir.join(\"test\").join(\"foo.txt\");\n        let copied_path_2 = fs.target_dir.join(\"test\").join(\"bar.txt.t\");\n        assert!(copied_path_1.exists());\n        assert!(copied_path_2.exists());\n\n        assert_eq!(\n            fs_extra::file::read_to_string(copied_path_1).expect(\"read content\"),\n            \"bar\"\n        );\n\n        assert_eq!(\n            fs_extra::file::read_to_string(copied_path_2).expect(\"read content\"),\n            \"crate: {{settings.package_name}}\"\n        );\n    }\n\n    #[test]\n    fn can_copy_template() {\n        let (fs, _tree_fs) = init_filesystem();\n\n        let settings = Settings {\n            package_name: \"loco-app\".to_string(),\n            ..Default::default()\n        };\n\n        assert!(fs\n            .copy_template(&PathBuf::from(\"test\").join(\"bar.txt.t\"), &settings)\n            .is_ok());\n        let copied_path = fs.target_dir.join(\"test\").join(\"bar.txt\");\n        assert!(copied_path.exists());\n        assert_eq!(\n            fs_extra::file::read_to_string(copied_path).expect(\"read content\"),\n            \"crate: loco-app\"\n        );\n    }\n\n    #[test]\n    fn can_copy_template_dir() {\n        let (fs, _tree_fs) = init_filesystem();\n\n        let settings = Settings {\n            package_name: \"loco-app\".to_string(),\n            ..Default::default()\n        };\n\n        assert!(fs\n            .copy_template_dir(&PathBuf::from(\"test\"), &settings)\n            .is_ok());\n        let copied_path_1 = fs.target_dir.join(\"test\").join(\"foo.txt\");\n        let copied_path_2 = fs.target_dir.join(\"test\").join(\"bar.txt\");\n        assert!(copied_path_1.exists());\n        assert!(copied_path_2.exists());\n\n        assert_eq!(\n            fs_extra::file::read_to_string(copied_path_1).expect(\"read content\"),\n            \"bar\"\n        );\n\n        assert_eq!(\n            fs_extra::file::read_to_string(copied_path_2).expect(\"read content\"),\n            \"crate: loco-app\"\n        );\n    }\n}\n"
  },
  {
    "path": "loco-new/src/generator/executer/inmem.rs",
    "content": "use std::{\n    collections::BTreeMap,\n    path::{Path, PathBuf},\n    sync::Mutex,\n};\n\nuse super::Executer;\nuse crate::{generator, settings::Settings};\n\npub struct Inmem {\n    pub source_path: PathBuf,\n    pub file_store: Mutex<BTreeMap<PathBuf, String>>,\n    pub template_engine: generator::template::Template,\n}\n\nimpl Inmem {\n    #[must_use]\n    pub fn new(source: &Path) -> Self {\n        Self::with_template_engine(source, generator::template::Template::default())\n    }\n\n    #[must_use]\n    pub fn with_template_engine(\n        source: &Path,\n        template_engine: generator::template::Template,\n    ) -> Self {\n        Self {\n            source_path: source.to_path_buf(),\n            file_store: Mutex::new(BTreeMap::default()),\n            template_engine,\n        }\n    }\n\n    pub fn get_file_content(&self, path: &Path) -> Option<String> {\n        self.file_store\n            .lock()\n            .ok()\n            .and_then(|store| store.get(path).cloned())\n    }\n}\n\nimpl Executer for Inmem {\n    fn copy_file(&self, file_path: &Path) -> super::Result<PathBuf> {\n        let file_content = fs_extra::file::read_to_string(self.source_path.join(file_path))?;\n        self.file_store\n            .lock()\n            .unwrap()\n            .insert(file_path.to_path_buf(), file_content);\n        Ok(file_path.to_path_buf())\n    }\n\n    fn create_file(&self, path: &Path, content: String) -> super::Result<PathBuf> {\n        self.file_store\n            .lock()\n            .unwrap()\n            .insert(path.to_path_buf(), content);\n        Ok(path.to_path_buf())\n    }\n\n    fn copy_dir(&self, directory_path: &Path) -> super::Result<()> {\n        let directory_content = fs_extra::dir::get_dir_content(directory_path)?;\n        for file in directory_content.files {\n            let mut store = self.file_store.lock().unwrap();\n            store.insert(PathBuf::from(&file), fs_extra::file::read_to_string(file)?);\n        }\n        Ok(())\n    }\n\n    fn copy_template(&self, file_path: &Path, settings: &Settings) -> super::Result<()> {\n        let copied_path = self.copy_file(file_path)?;\n\n        if self.template_engine.is_template(&copied_path) {\n            let template_content = {\n                let store = self.file_store.lock().unwrap();\n                store.get(&copied_path).cloned()\n            };\n\n            if let Some(content) = template_content {\n                let rendered_content = self.template_engine.render(&content, settings)?;\n                self.file_store\n                    .lock()\n                    .unwrap()\n                    .insert(file_path.to_path_buf(), rendered_content);\n                Ok(())\n            } else {\n                Err(super::Error::msg(\"Template content not found\"))\n            }\n        } else {\n            Err(super::Error::msg(\"File is not a template\"))\n        }\n    }\n\n    fn copy_template_dir(&self, _path: &Path, _data: &Settings) -> super::Result<()> {\n        Ok(())\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use tree_fs::{Tree, TreeBuilder};\n\n    use super::*;\n\n    fn init_in_memory_store() -> (Inmem, Tree) {\n        let tree = TreeBuilder::default()\n            .drop(true)\n            .add(\"test/foo.txt\", \"bar\")\n            .add(\"test/bar.txt.t\", \"crate: {{settings.package_name}}\")\n            .create()\n            .expect(\"Failed to create mock data\");\n        (Inmem::new(&tree.root), tree)\n    }\n\n    #[test]\n    fn can_copy_file() {\n        let (store, source_dir) = init_in_memory_store();\n        let test_file_path = source_dir.root.join(\"test\").join(\"foo.txt\");\n\n        let copied_path = store.copy_file(&test_file_path).unwrap();\n\n        assert_eq!(copied_path, test_file_path);\n        assert_eq!(\n            store\n                .file_store\n                .lock()\n                .unwrap()\n                .get(&test_file_path)\n                .unwrap(),\n            \"bar\"\n        );\n    }\n\n    #[test]\n    fn test_copy_directory() {\n        let (store, source_dir) = init_in_memory_store();\n        let dir_path = source_dir.root.join(\"test\");\n\n        store.copy_dir(&dir_path).unwrap();\n\n        let file1_path = dir_path.join(\"foo.txt\");\n        let file2_path = dir_path.join(\"bar.txt.t\");\n\n        assert_eq!(\n            store.file_store.lock().unwrap().get(&file1_path).unwrap(),\n            \"bar\"\n        );\n        assert_eq!(\n            store.file_store.lock().unwrap().get(&file2_path).unwrap(),\n            \"crate: {{settings.package_name}}\"\n        );\n    }\n\n    #[test]\n    fn can_copy_template_file() {\n        let (store, source_dir) = init_in_memory_store();\n        let test_file_path = source_dir.root.join(\"test\").join(\"bar.txt.t\");\n\n        let settings = Settings {\n            package_name: \"loco-app\".to_string(),\n            ..Default::default()\n        };\n\n        store\n            .copy_template(&test_file_path, &settings)\n            .expect(\"copy template\");\n\n        assert_eq!(\n            store\n                .file_store\n                .lock()\n                .unwrap()\n                .get(&test_file_path)\n                .unwrap(),\n            \"crate: loco-app\"\n        );\n    }\n}\n"
  },
  {
    "path": "loco-new/src/generator/executer/mod.rs",
    "content": "//! This module defines error handling and the [`Executer`] trait\n\nuse crate::settings::Settings;\nmod filesystem;\nmod inmem;\nuse std::path::{Path, PathBuf};\n\npub use filesystem::FileSystem;\npub use inmem::Inmem;\n#[cfg(test)]\nuse mockall::{automock, predicate::*};\n\npub type Result<T> = std::result::Result<T, Error>;\n\n#[derive(thiserror::Error, Debug)]\npub enum Error {\n    #[error(\"{0}\")]\n    Message(String),\n\n    #[error(transparent)]\n    TemplateEngine(#[from] Box<rhai::EvalAltResult>),\n\n    #[error(transparent)]\n    FS(#[from] fs_extra::error::Error),\n\n    #[error(transparent)]\n    Template(#[from] tera::Error),\n}\nimpl Error {\n    /// Creates a new error with a custom message.\n    pub fn msg<S: Into<String>>(msg: S) -> Self {\n        Self::Message(msg.into())\n    }\n}\n\n#[cfg_attr(test, automock)]\npub trait Executer: Send + Sync {\n    /// Copies a single file from the specified path.\n    ///\n    /// # Errors\n    ///\n    /// Returns an error if the file cannot be copied, such as if the path is\n    /// invalid or if a file system error occurs.\n    fn copy_file(&self, path: &Path) -> Result<PathBuf>;\n\n    /// Copies a single file from the specified path.\n    ///\n    /// # Errors\n    ///\n    /// Returns an error if the file cannot be copied, such as if the path is\n    /// invalid or if a file system error occurs.\n    fn create_file(&self, path: &Path, content: String) -> Result<PathBuf>;\n\n    /// Copies an entire directory from the specified path.\n    ///\n    /// # Errors\n    ///\n    /// Returns an error if the directory cannot be copied, such as if the path\n    /// is invalid or if a file system error occurs.\n    fn copy_dir(&self, path: &Path) -> Result<()>;\n\n    /// Copies a template file from the specified path, applying settings.\n    ///\n    /// # Errors\n    ///\n    /// Returns an error if the template cannot be copied or if any\n    /// settings-related error occurs.\n    fn copy_template(&self, path: &Path, data: &Settings) -> Result<()>;\n\n    /// Copies an entire template directory from the specified path, applying\n    /// settings.\n    ///\n    /// # Errors\n    ///\n    /// Returns an error if the template directory cannot be copied or if any\n    /// settings-related error occurs.\n    fn copy_template_dir(&self, path: &Path, data: &Settings) -> Result<()>;\n}\n"
  },
  {
    "path": "loco-new/src/generator/mod.rs",
    "content": "//! This module defines the `Generator` struct, which is responsible for\n//! executing scripted commands\n\nuse std::path::Path;\npub mod executer;\npub mod template;\nuse std::sync::Arc;\n\nuse include_dir::{include_dir, Dir};\nuse rhai::{\n    export_module, exported_module,\n    plugin::{\n        Dynamic, FnNamespace, FuncRegistration, Module, NativeCallContext, PluginFunc, RhaiResult,\n        TypeId,\n    },\n    Engine, Scope,\n};\n\nuse crate::wizard::AssetsOption;\nuse crate::{settings, OS};\n\nstatic APP_TEMPLATE: Dir<'_> = include_dir!(\"base_template\");\n\n/// Extracts a default template to a temporary directory for use by the\n/// application.\n///\n/// # Errors\n/// when could not extract the the base template\npub fn extract_default_template() -> std::io::Result<tree_fs::Tree> {\n    let generator_tmp_folder = tree_fs::TreeBuilder::default().create()?;\n\n    APP_TEMPLATE.extract(&generator_tmp_folder.root)?;\n    Ok(generator_tmp_folder)\n}\n\n/// The `Generator` struct provides functionality to execute scripted\n/// operations, such as copying files and templates, based on the current\n/// settings.\n#[derive(Clone)]\npub struct Generator {\n    pub executer: Arc<dyn executer::Executer>,\n    pub settings: settings::Settings,\n}\nimpl Generator {\n    /// Creates a new [`Generator`] with a given executor and settings.\n    pub fn new(executer: Arc<dyn executer::Executer>, settings: settings::Settings) -> Self {\n        Self { executer, settings }\n    }\n\n    /// Runs the default script.\n    ///\n    /// # Errors\n    ///\n    /// Returns an error if the script execution fails.\n    pub fn run(&self) -> crate::Result<()> {\n        self.run_from_script(include_str!(\"../../setup.rhai\"))\n    }\n\n    /// Runs a custom script provided as a string.\n    ///\n    /// # Errors\n    ///\n    /// Returns an error if the script execution fails.\n    pub fn run_from_script(&self, script: &str) -> crate::Result<()> {\n        let mut engine = Engine::new();\n\n        tracing::debug!(\n            settings = format!(\"{:?}\", self.settings),\n            script,\n            \"prepare installation script\"\n        );\n        engine\n            .build_type::<settings::Settings>()\n            .build_type::<settings::Initializers>()\n            .build_type::<settings::Db>()\n            .build_type::<settings::Asset>()\n            .build_type::<settings::Background>()\n            .register_static_module(\n                \"rhai_settings_extensions\",\n                exported_module!(rhai_settings_extensions).into(),\n            )\n            .register_fn(\"copy_file\", Self::copy_file)\n            .register_fn(\"create_file\", Self::create_file)\n            .register_fn(\"copy_files\", Self::copy_files)\n            .register_fn(\"copy_dir\", Self::copy_dir)\n            .register_fn(\"copy_dirs\", Self::copy_dirs)\n            .register_fn(\"copy_template\", Self::copy_template)\n            .register_fn(\"copy_template_dir\", Self::copy_template_dir);\n\n        let settings_dynamic = rhai::Dynamic::from(self.settings.clone());\n\n        let mut scope = Scope::new();\n        scope.set_value(\"settings\", settings_dynamic);\n        scope.push(\"gen\", self.clone());\n        // TODO:: move it as part of the settings?\n        scope.push(\"db\", self.settings.db.is_some());\n        scope.push(\"background\", self.settings.background.is_some());\n        scope.push(\"initializers\", self.settings.initializers.is_some());\n        scope.push(\"asset\", self.settings.asset.is_some());\n        scope.push(\"windows\", self.settings.os == OS::Windows);\n\n        engine.run_with_scope(&mut scope, script)?;\n        Ok(())\n    }\n\n    /// Copies a single file from the specified path.\n    ///\n    /// # Errors\n    ///\n    /// Returns an error if the file copy operation fails.\n    pub fn copy_file(&mut self, path: &str) -> Result<(), Box<rhai::EvalAltResult>> {\n        let span = tracing::info_span!(\"copy_file\", path);\n        let _guard = span.enter();\n\n        self.executer.copy_file(Path::new(path)).map_err(|err| {\n            Box::new(rhai::EvalAltResult::ErrorSystem(\n                \"copy_file\".to_string(),\n                err.into(),\n            ))\n        })?;\n        Ok(())\n    }\n\n    /// Creates a single file in the specified path.\n    ///\n    /// # Errors\n    ///\n    /// Returns an error if the file copy operation fails.\n    pub fn create_file(\n        &mut self,\n        path: &str,\n        content: &str,\n    ) -> Result<(), Box<rhai::EvalAltResult>> {\n        let span = tracing::info_span!(\"create_file\", path);\n        let _guard = span.enter();\n\n        self.executer\n            .create_file(Path::new(path), content.to_string())\n            .map_err(|err| {\n                Box::new(rhai::EvalAltResult::ErrorSystem(\n                    \"create_file\".to_string(),\n                    err.into(),\n                ))\n            })?;\n        Ok(())\n    }\n\n    /// Copies list of files from the specified path.\n    ///\n    /// # Errors\n    ///\n    /// Returns an error if the file copy operation fails.\n    pub fn copy_files(&mut self, paths: rhai::Array) -> Result<(), Box<rhai::EvalAltResult>> {\n        let span = tracing::info_span!(\"copy_files\");\n        let _guard = span.enter();\n        for path in paths {\n            self.executer\n                .copy_file(Path::new(&path.to_string()))\n                .map_err(|err| {\n                    Box::new(rhai::EvalAltResult::ErrorSystem(\n                        \"copy_files\".to_string(),\n                        err.into(),\n                    ))\n                })?;\n        }\n\n        Ok(())\n    }\n\n    /// Copies an entire directory from the specified path.\n    ///\n    /// # Errors\n    ///\n    /// Returns an error if the directory copy operation fails.\n    pub fn copy_dir(&mut self, path: &str) -> Result<(), Box<rhai::EvalAltResult>> {\n        let span = tracing::info_span!(\"copy_dir\", path);\n        let _guard = span.enter();\n        self.executer.copy_dir(Path::new(path)).map_err(|err| {\n            Box::new(rhai::EvalAltResult::ErrorSystem(\n                \"copy_dir\".to_string(),\n                err.into(),\n            ))\n        })\n    }\n\n    /// Copies list of directories from the specified path.\n    ///\n    /// # Errors\n    ///\n    /// Returns an error if the directory copy operation fails.\n    pub fn copy_dirs(&mut self, paths: rhai::Array) -> Result<(), Box<rhai::EvalAltResult>> {\n        let span = tracing::info_span!(\"copy_dirs\");\n        let _guard = span.enter();\n        for path in paths {\n            self.executer\n                .copy_dir(Path::new(&path.to_string()))\n                .map_err(|err| {\n                    Box::new(rhai::EvalAltResult::ErrorSystem(\n                        \"copy_dirs\".to_string(),\n                        err.into(),\n                    ))\n                })?;\n        }\n        Ok(())\n    }\n\n    /// Copies a template file from the specified path, applying settings.\n    ///\n    /// # Errors\n    ///\n    /// Returns an error if the template copy operation fails.\n    pub fn copy_template(&mut self, path: &str) -> Result<(), Box<rhai::EvalAltResult>> {\n        let span = tracing::info_span!(\"copy_template\", path);\n        let _guard = span.enter();\n        self.executer\n            .copy_template(Path::new(path), &self.settings)\n            .map_err(|err| {\n                Box::new(rhai::EvalAltResult::ErrorSystem(\n                    \"copy_template\".to_string(),\n                    err.into(),\n                ))\n            })\n    }\n\n    /// Copies an entire template directory from the specified path, applying\n    /// settings.\n    ///\n    /// # Errors\n    ///\n    /// Returns an error if the template directory copy operation fails.\n    pub fn copy_template_dir(&mut self, path: &str) -> Result<(), Box<rhai::EvalAltResult>> {\n        let span = tracing::info_span!(\"copy_template_dir\", path);\n        let _guard = span.enter();\n        self.executer\n            .copy_template_dir(Path::new(path), &self.settings)\n            .map_err(|err| {\n                Box::new(rhai::EvalAltResult::ErrorSystem(\n                    \"copy_template_dir\".to_string(),\n                    err.into(),\n                ))\n            })\n    }\n}\n\n/// This module provides extensions to the [`rhai`] scripting language,\n/// enabling ergonomic access to specific.\n/// These extensions allow scripts to interact with internal settings\n/// in a controlled and expressive way.\n#[export_module]\nmod rhai_settings_extensions {\n    /// Checks if the rendering method is set to client-side rendering.\n    #[rhai_fn(global, get = \"is_client_side\", pure)]\n    pub const fn is_client_side(rendering_method: &mut settings::Asset) -> bool {\n        matches!(rendering_method.kind, AssetsOption::Clientside)\n    }\n\n    /// Checks if the rendering method is set to server-side rendering.\n    #[rhai_fn(global, get = \"is_server_side\", pure)]\n    pub const fn is_server_side(rendering_method: &mut settings::Asset) -> bool {\n        matches!(rendering_method.kind, AssetsOption::Serverside)\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use executer::MockExecuter;\n    use mockall::predicate::*;\n\n    use super::*;\n\n    #[test]\n    pub fn can_copy_file() {\n        let mut executor = MockExecuter::new();\n\n        executor\n            .expect_copy_file()\n            .with(eq(Path::new(\"test.rs\")))\n            .times(1)\n            .returning(|p| Ok(p.to_path_buf()));\n\n        let g = Generator::new(Arc::new(executor), settings::Settings::default());\n        let script_res = g.run_from_script(r#\"gen.copy_file(\"test.rs\");\"#);\n\n        assert!(script_res.is_ok());\n    }\n\n    #[test]\n    pub fn can_copy_files() {\n        let mut executor = MockExecuter::new();\n\n        executor\n            .expect_copy_file()\n            .with(eq(Path::new(\".gitignore\")))\n            .times(1)\n            .returning(|p| Ok(p.to_path_buf()));\n\n        executor\n            .expect_copy_file()\n            .with(eq(Path::new(\".rustfmt.toml\")))\n            .times(1)\n            .returning(|p| Ok(p.to_path_buf()));\n\n        executor\n            .expect_copy_file()\n            .with(eq(Path::new(\"README.md\")))\n            .times(1)\n            .returning(|p| Ok(p.to_path_buf()));\n\n        let g = Generator::new(Arc::new(executor), settings::Settings::default());\n        let script_res =\n            g.run_from_script(r#\"gen.copy_files([\".gitignore\", \".rustfmt.toml\", \"README.md\"]);\"#);\n\n        assert!(script_res.is_ok());\n    }\n\n    #[test]\n    pub fn can_copy_dir() {\n        let mut executor = MockExecuter::new();\n\n        executor\n            .expect_copy_dir()\n            .with(eq(Path::new(\"test\")))\n            .times(1)\n            .returning(|_| Ok(()));\n\n        let g = Generator::new(Arc::new(executor), settings::Settings::default());\n        let script_res = g.run_from_script(r#\"gen.copy_dir(\"test\");\"#);\n\n        assert!(script_res.is_ok());\n    }\n\n    #[test]\n    pub fn can_copy_dirs() {\n        let mut executor = MockExecuter::new();\n\n        executor\n            .expect_copy_dir()\n            .with(eq(Path::new(\"src\")))\n            .times(1)\n            .returning(|_| Ok(()));\n\n        executor\n            .expect_copy_dir()\n            .with(eq(Path::new(\"example\")))\n            .times(1)\n            .returning(|_| Ok(()));\n\n        executor\n            .expect_copy_dir()\n            .with(eq(Path::new(\".github\")))\n            .times(1)\n            .returning(|_| Ok(()));\n\n        let g = Generator::new(Arc::new(executor), settings::Settings::default());\n        let script_res = g.run_from_script(r#\"gen.copy_dirs([\"src\", \"example\", \".github\"]);\"#);\n\n        assert!(script_res.is_ok());\n    }\n\n    #[test]\n    pub fn can_copy_template() {\n        let mut executor = MockExecuter::new();\n\n        executor\n            .expect_copy_template()\n            .with(eq(Path::new(\"src/lib.rs.t\")), always())\n            .times(1)\n            .returning(|_, _| Ok(()));\n\n        let g = Generator::new(Arc::new(executor), settings::Settings::default());\n        let script_res = g.run_from_script(r#\"gen.copy_template(\"src/lib.rs.t\");\"#);\n\n        assert!(script_res.is_ok());\n    }\n\n    #[test]\n    pub fn can_copy_template_dir() {\n        let mut executor = MockExecuter::new();\n\n        executor\n            .expect_copy_template_dir()\n            .with(eq(Path::new(\"src/examples\")), always())\n            .times(1)\n            .returning(|_, _| Ok(()));\n\n        let g = Generator::new(Arc::new(executor), settings::Settings::default());\n        let script_res = g.run_from_script(r#\"gen.copy_template_dir(\"src/examples\");\"#);\n\n        assert!(script_res.is_ok());\n    }\n}\n"
  },
  {
    "path": "loco-new/src/generator/template.rs",
    "content": "//! This module defines a `Template` struct for handling template files.\n\nuse std::{\n    collections::HashMap,\n    path::{Path, PathBuf},\n    sync::{Arc, Mutex},\n};\n\nuse rand::{distributions::Alphanumeric, rngs::StdRng, Rng, SeedableRng};\nuse tera::{Context, Tera};\n\nuse crate::settings::Settings;\n\nconst TEMPLATE_EXTENSION: &str = \"t\";\n\nfn generate_random_string<R: Rng>(rng: &mut R, length: u64) -> String {\n    (0..length)\n        .map(|_| rng.sample(Alphanumeric) as char)\n        .collect()\n}\n\n/// Represents a template that can be rendered with injected settings.\n#[derive(Debug, Clone)]\npub struct Template {\n    rng: Arc<Mutex<StdRng>>,\n}\n\nimpl Default for Template {\n    fn default() -> Self {\n        #[cfg(test)]\n        let rng = StdRng::seed_from_u64(42);\n        #[cfg(not(test))]\n        let rng = StdRng::from_entropy();\n        Self {\n            rng: Arc::new(Mutex::new(rng)),\n        }\n    }\n}\n\nimpl Template {\n    #[must_use]\n    pub fn new(rng: StdRng) -> Self {\n        Self {\n            rng: Arc::new(Mutex::new(rng)),\n        }\n    }\n    /// Checks if the provided file path has a \".t\" extension, marking it as a\n    /// template.\n    ///\n    /// Returns `true` if the file has a \".t\" extension, otherwise `false`.\n    #[must_use]\n    pub fn is_template(&self, path: &Path) -> bool {\n        path.extension()\n            .and_then(|ext| ext.to_str())\n            .filter(|&ext| ext == TEMPLATE_EXTENSION)\n            .is_some()\n    }\n\n    // Method to register filters in the Tera instance.\n    fn register_filters(&self, tera_instance: &mut tera::Tera) {\n        // Clone the Arc to move it into the closure.\n        let rng_clone = Arc::clone(&self.rng);\n\n        tera_instance.register_filter(\n            \"random_string\",\n            move |value: &tera::Value, _args: &HashMap<String, tera::Value>| {\n                if let tera::Value::Number(length) = value {\n                    if let Some(length) = length.as_u64() {\n                        let rand_str: String = rng_clone.lock().map_or_else(\n                            |_| {\n                                let mut r = StdRng::from_entropy();\n                                generate_random_string(&mut r, length)\n                            },\n                            |mut rng| generate_random_string(&mut *rng, length),\n                        );\n                        return Ok(tera::Value::String(rand_str));\n                    }\n                }\n                // Ok(tera::Value::String(String::new()))\n                Err(tera::Error::msg(\"arg must be a number\"))\n            },\n        );\n    }\n\n    /// Renders a template with the provided content and settings.\n    ///\n    /// # Errors\n    /// when could not render the template\n    pub fn render(&self, template_content: &str, settings: &Settings) -> tera::Result<String> {\n        tracing::trace!(\n            template_content,\n            settings = format!(\"{settings:#?}\"),\n            \"render template\"\n        );\n\n        let mut tera_instance = Tera::default();\n        self.register_filters(&mut tera_instance);\n\n        let mut context = Context::new();\n        context.insert(\"settings\", &settings);\n\n        let rendered_output = tera_instance.render_str(template_content, &context)?;\n\n        Ok(rendered_output)\n    }\n\n    /// Removes the \".t\" extension from a template file path, if present.\n    ///\n    /// # Errors\n    /// if the given path is not contains template extension\n    pub fn strip_template_extension(&self, path: &Path) -> std::io::Result<PathBuf> {\n        path.file_stem().map_or_else(\n            || {\n                Err(std::io::Error::new(\n                    std::io::ErrorKind::InvalidInput,\n                    \"Failed to retrieve file stem\",\n                ))\n            },\n            |stem| {\n                let mut path_without_extension = path.to_path_buf();\n                path_without_extension.set_file_name(stem);\n                if let Some(parent_dir) = path.parent() {\n                    path_without_extension = parent_dir.join(stem.to_string_lossy().to_string());\n                }\n                Ok(path_without_extension)\n            },\n        )\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_is_template() {\n        let template = Template::default();\n\n        let path = Path::new(\"example.t\");\n        assert!(template.is_template(path));\n\n        let path = Path::new(\"example.txt\");\n        assert!(!template.is_template(path));\n\n        let path = Path::new(\"directory/\");\n        assert!(!template.is_template(path));\n    }\n\n    #[test]\n    fn test_render_template() {\n        let template = Template::default();\n        let template_content = \"crate: {{ settings.package_name }}\";\n\n        let mock_settings = Settings {\n            package_name: \"loco-app\".to_string(),\n            ..Default::default()\n        };\n\n        let result = template.render(template_content, &mock_settings);\n        assert!(result.is_ok());\n        assert_eq!(result.unwrap(), \"crate: loco-app\");\n    }\n\n    #[test]\n    fn test_strip_template_extension() {\n        let template = Template::default();\n\n        let path = Path::new(\"example.t\");\n        let result = template.strip_template_extension(path);\n        assert!(result.is_ok());\n        assert_eq!(result.unwrap(), Path::new(\"example\"));\n\n        let path = Path::new(\"example\");\n        let result = template.strip_template_extension(path);\n        assert!(result.is_ok());\n        assert_eq!(result.unwrap(), Path::new(\"example\"));\n\n        let path = Path::new(\"\");\n        let result = template.strip_template_extension(path);\n        assert!(result.is_err());\n    }\n\n    #[test]\n    fn can_create_random_string() {\n        let template = Template::default();\n        let template_content = \"rand: {{20 | random_string }}\";\n\n        let mock_settings = Settings {\n            package_name: \"loco-app\".to_string(),\n            ..Default::default()\n        };\n\n        let result = template.render(template_content, &mock_settings);\n        assert!(result.is_ok());\n        assert_eq!(result.unwrap(), \"rand: IhPi3oZCnaWvL2oIeA07\");\n        let result = template.render(template_content, &mock_settings);\n        assert!(result.is_ok());\n        assert_eq!(result.unwrap(), \"rand: mg3ZtJzh0NoAKhdDqpQ2\");\n    }\n}\n"
  },
  {
    "path": "loco-new/src/lib.rs",
    "content": "use clap::ValueEnum;\nuse serde::{Deserialize, Serialize};\nuse strum::Display;\n\npub mod generator;\npub mod settings;\npub mod wizard;\n\npub type Result<T> = std::result::Result<T, Error>;\n\n/// Matching minimal Loco version.\npub const LOCO_VERSION: &str = \"0.16\";\n\n#[derive(thiserror::Error, Debug)]\npub enum Error {\n    #[error(\"{0}\")]\n    Message(String),\n\n    #[error(transparent)]\n    Dialog(#[from] dialoguer::Error),\n\n    #[error(transparent)]\n    IO(#[from] std::io::Error),\n\n    #[error(transparent)]\n    FS(#[from] fs_extra::error::Error),\n\n    #[error(transparent)]\n    TemplateEngine(#[from] Box<rhai::EvalAltResult>),\n\n    #[error(transparent)]\n    Generator(#[from] crate::generator::executer::Error),\n}\nimpl Error {\n    pub fn msg<S: Into<String>>(msg: S) -> Self {\n        Self::Message(msg.into())\n    }\n}\n\n#[derive(Debug, Clone, Deserialize, Serialize, Display, Default, PartialEq, Eq, ValueEnum)]\npub enum OS {\n    #[cfg_attr(windows, default)]\n    #[serde(rename = \"windows\")]\n    Windows,\n\n    #[cfg_attr(unix, default)]\n    #[serde(rename = \"linux\")]\n    Linux,\n\n    #[serde(rename = \"macos\")]\n    Macos,\n}\n"
  },
  {
    "path": "loco-new/src/settings.rs",
    "content": "//! Defines configurable application settings.\n\nuse std::env;\n\nuse heck::ToSnakeCase;\n#[allow(unused_imports)]\nuse rhai::{CustomType, Dynamic, EvalAltResult, Position, TypeBuilder};\nuse serde::{Deserialize, Serialize};\n\nuse crate::{\n    wizard::{self, AssetsOption, BackgroundOption, DBOption},\n    LOCO_VERSION, OS,\n};\n\n/// Represents general application settings.\n#[derive(Serialize, Deserialize, Clone, Debug, CustomType)]\npub struct Settings {\n    pub package_name: String,\n    pub module_name: String,\n    pub db: Option<Db>,\n    pub background: Option<Background>,\n    pub asset: Option<Asset>,\n    pub auth: bool,\n    pub mailer: bool,\n    pub initializers: Option<Initializers>,\n    pub features: Features,\n    pub loco_version_text: String,\n    pub os: OS,\n}\n\nimpl From<DBOption> for Option<Db> {\n    fn from(db_option: DBOption) -> Self {\n        match db_option {\n            DBOption::None => None,\n            _ => Some(Db {\n                kind: db_option.clone(),\n                endpoint: db_option.endpoint_config().to_string(),\n            }),\n        }\n    }\n}\n\nimpl From<BackgroundOption> for Option<Background> {\n    fn from(bg: BackgroundOption) -> Self {\n        Some(Background { kind: bg })\n    }\n}\n\nimpl From<AssetsOption> for Option<Asset> {\n    fn from(asset: AssetsOption) -> Self {\n        match asset {\n            AssetsOption::None => None,\n            _ => Some(Asset { kind: asset }),\n        }\n    }\n}\n\nimpl Settings {\n    /// Creates a new [`Settings`] instance based on prompt selections.\n    #[must_use]\n    pub fn from_wizard(package_name: &str, prompt_selection: &wizard::Selections, os: OS) -> Self {\n        let features = if prompt_selection.db.enable() {\n            Features::default()\n        } else {\n            let mut features = Features::disable_features();\n            if matches!(prompt_selection.background, wizard::BackgroundOption::Queue) {\n                features.names.push(\"bg_redis\".to_string());\n            }\n            features\n        };\n\n        Self {\n            package_name: package_name.to_string(),\n            module_name: package_name.to_snake_case(),\n            auth: prompt_selection.db.enable(),\n            mailer: prompt_selection.db.enable(),\n            db: prompt_selection.db.clone().into(),\n            background: prompt_selection.background.clone().into(),\n            asset: prompt_selection.asset.clone().into(),\n            initializers: if prompt_selection.asset == AssetsOption::Serverside {\n                Some(Initializers { view_engine: true })\n            } else {\n                None\n            },\n            features,\n            loco_version_text: get_loco_version_text(),\n            os,\n        }\n    }\n}\nimpl Default for Settings {\n    fn default() -> Self {\n        #[allow(clippy::default_trait_access)]\n        Self {\n            package_name: Default::default(),\n            module_name: Default::default(),\n            db: Default::default(),\n            background: Default::default(),\n            asset: Default::default(),\n            auth: Default::default(),\n            mailer: Default::default(),\n            initializers: Default::default(),\n            features: Default::default(),\n            loco_version_text: get_loco_version_text(),\n            os: Default::default(),\n        }\n    }\n}\n\nfn get_loco_version_text() -> String {\n    env::var(\"LOCO_DEV_MODE_PATH\").map_or_else(\n        |_| format!(r#\"version = \"{LOCO_VERSION}\"\"#),\n        |path| {\n            let path = path.replace('\\\\', \"/\");\n            format!(r#\"version=\"*\", path=\"{path}\"\"#)\n        },\n    )\n}\n\n/// Database configuration settings.\n#[derive(Serialize, Deserialize, Clone, Debug, Default, CustomType)]\npub struct Db {\n    pub kind: DBOption,\n    pub endpoint: String,\n}\n\n/// Background processing configuration.\n#[derive(Serialize, Deserialize, Clone, Debug, Default, CustomType)]\npub struct Background {\n    pub kind: BackgroundOption,\n}\n\n/// Asset configuration settings.\n#[derive(Serialize, Deserialize, Clone, Debug, Default, CustomType)]\npub struct Asset {\n    pub kind: AssetsOption,\n}\n\n#[derive(Serialize, Deserialize, Clone, Debug, Default, CustomType)]\npub struct Initializers {\n    pub view_engine: bool,\n}\n\n/// Feature configuration, allowing toggling of optional features.\n#[derive(Serialize, Deserialize, Clone, Debug)]\npub struct Features {\n    pub default_features: bool,\n    pub names: Vec<String>,\n}\n\nimpl Default for Features {\n    fn default() -> Self {\n        Self {\n            default_features: true,\n            names: vec![],\n        }\n    }\n}\n\nimpl Features {\n    /// Disables default features.\n    #[must_use]\n    pub fn disable_features() -> Self {\n        Self {\n            default_features: false,\n            names: vec![\"cli\".to_string()],\n        }\n    }\n}\n"
  },
  {
    "path": "loco-new/src/wizard.rs",
    "content": "//! This module provides interactive utilities for setting up application\n//! configurations based on user input.\n\nuse clap::ValueEnum;\nuse colored::Colorize;\nuse dialoguer::{theme::ColorfulTheme, Confirm, Input, Select};\nuse serde::{Deserialize, Serialize};\nuse strum::{Display, EnumIter, IntoEnumIterator};\n\nuse crate::Error;\n\n#[derive(\n    Debug, Clone, Deserialize, Serialize, EnumIter, Display, Default, PartialEq, Eq, ValueEnum,\n)]\npub enum Template {\n    #[default]\n    #[strum(to_string = \"Saas App with server side rendering\")]\n    SaasServerSideRendering,\n    #[strum(to_string = \"Saas App with client side rendering\")]\n    SaasClientSideRendering,\n    #[strum(to_string = \"Rest API (with DB and user auth)\")]\n    RestApi,\n    #[strum(to_string = \"lightweight-service (minimal, only controllers and views)\")]\n    Lightweight,\n    #[strum(to_string = \"Advanced\")]\n    Advanced,\n}\n\n#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]\npub enum OptionsList {\n    #[serde(rename = \"db\")]\n    DB,\n    #[serde(rename = \"bg\")]\n    Background,\n    #[serde(rename = \"assets\")]\n    Assets,\n}\n\n#[derive(\n    Debug, Clone, Deserialize, Serialize, EnumIter, Display, Default, PartialEq, Eq, ValueEnum,\n)]\npub enum DBOption {\n    #[default]\n    #[serde(rename = \"sqlite\")]\n    Sqlite,\n    #[serde(rename = \"pg\")]\n    Postgres,\n    #[serde(rename = \"none\")]\n    None,\n}\n\nimpl DBOption {\n    #[must_use]\n    pub const fn enable(&self) -> bool {\n        !matches!(self, Self::None)\n    }\n\n    #[must_use]\n    pub fn user_message(&self) -> Option<String> {\n        match self {\n            Self::Postgres => Some(format!(\n                \"{}: You've selected `{}` as your DB provider (you should have a postgres \\\n                 instance to connect to)\",\n                \"database\".underline(),\n                \"postgres\".yellow()\n            )),\n            Self::Sqlite | Self::None => None,\n        }\n    }\n\n    #[must_use]\n    pub const fn endpoint_config(&self) -> &str {\n        match self {\n            Self::Sqlite => \"sqlite://NAME_ENV.sqlite?mode=rwc\",\n            Self::Postgres => \"postgres://loco:loco@localhost:5432/NAME_ENV\",\n            Self::None => \"\",\n        }\n    }\n}\n\n#[derive(\n    Debug, Clone, Deserialize, Serialize, EnumIter, Display, Default, PartialEq, Eq, ValueEnum,\n)]\npub enum BackgroundOption {\n    #[default]\n    #[strum(to_string = \"Async (in-process tokio async tasks)\")]\n    #[serde(rename = \"BackgroundAsync\")]\n    Async,\n    #[strum(to_string = \"Queue (standalone workers using Redis)\")]\n    #[serde(rename = \"BackgroundQueue\")]\n    Queue,\n    #[strum(to_string = \"Blocking (run tasks in foreground)\")]\n    #[serde(rename = \"ForegroundBlocking\")]\n    Blocking,\n}\n\nimpl BackgroundOption {\n    #[must_use]\n    pub fn user_message(&self) -> Option<String> {\n        match self {\n            Self::Queue => Some(format!(\n                \"{}: You've selected `{}` for your background worker configuration (you should \\\n                 have a Redis/valkey instance to connect to)\",\n                \"workers\".underline(),\n                \"queue\".yellow()\n            )),\n            Self::Blocking => Some(format!(\n                \"{}: You've selected `{}` for your background worker configuration. Your workers \\\n                 configuration will BLOCK REQUESTS until a task is done.\",\n                \"workers\".underline(),\n                \"blocking\".yellow()\n            )),\n            Self::Async => None,\n        }\n    }\n\n    #[must_use]\n    pub const fn prompt_view(&self) -> &str {\n        match self {\n            Self::Async => \"Async\",\n            Self::Queue => \"BackgroundQueue\",\n            Self::Blocking => \"ForegroundBlocking\",\n        }\n    }\n}\n\n#[derive(\n    Debug, Clone, Deserialize, Serialize, EnumIter, Display, Default, PartialEq, Eq, ValueEnum,\n)]\npub enum AssetsOption {\n    #[default]\n    #[strum(to_string = \"Server (configures server-rendered views)\")]\n    #[serde(rename = \"server\")]\n    Serverside,\n    #[strum(to_string = \"Client (configures assets for frontend serving)\")]\n    #[serde(rename = \"client\")]\n    Clientside,\n    #[strum(to_string = \"None\")]\n    #[serde(rename = \"none\")]\n    None,\n}\n\nimpl AssetsOption {\n    #[must_use]\n    pub const fn enable(&self) -> bool {\n        !matches!(self, Self::None)\n    }\n\n    #[must_use]\n    pub fn user_message(&self) -> Option<String> {\n        match self {\n            Self::Clientside => Some(format!(\n                \"{}: You've selected `{}` for your asset serving configuration.\\n\\nNext step, \\\n                 build your frontend:\\n  $ cd {}\\n  $ npm install && npm run build\\n\",\n                \"assets\".underline(),\n                \"clientside\".yellow(),\n                \"frontend/\".yellow()\n            )),\n            Self::Serverside | Self::None => None,\n        }\n    }\n}\n\n#[derive(Debug, Clone, Default)]\n/// Represents internal placeholders to be replaced.\npub struct ArgsPlaceholder {\n    pub db: Option<DBOption>,\n    pub bg: Option<BackgroundOption>,\n    pub assets: Option<AssetsOption>,\n}\n\n/// Holds the user's configuration selections.\npub struct Selections {\n    pub db: DBOption,\n    pub background: BackgroundOption,\n    pub asset: AssetsOption,\n}\n\nimpl Selections {\n    #[must_use]\n    pub fn message(&self) -> Vec<String> {\n        let mut res = Vec::new();\n        if let Some(m) = self.db.user_message() {\n            res.push(m);\n        }\n        if let Some(m) = self.background.user_message() {\n            res.push(m);\n        }\n        if let Some(m) = self.asset.user_message() {\n            res.push(m);\n        }\n        res\n    }\n}\n\n/// Prompts the user to enter an application name, with optional pre-set name\n/// input. Validates the name to ensure compliance with required naming rules.\n///\n/// # Errors\n/// when could not show user selection\npub fn app_name(name: Option<String>) -> crate::Result<String> {\n    if let Some(app_name) = name {\n        validate_app_name(app_name.as_str()).map_err(|err| Error::msg(err.to_string()))?;\n        Ok(app_name)\n    } else {\n        let res = Input::with_theme(&ColorfulTheme::default())\n            .with_prompt(\"❯ App name?\")\n            .default(\"myapp\".into())\n            .validate_with(|input: &String| {\n                if let Err(err) = validate_app_name(input) {\n                    Err(err.to_string())\n                } else {\n                    Ok(())\n                }\n            })\n            .interact_text()?;\n        Ok(res)\n    }\n}\n\n/// Warns the user if the current directory is inside a Git repository and\n/// prompts them to confirm whether they wish to proceed. If declined, an error\n/// is returned.\n///\n/// # Errors\n/// when could not show user selection or user chose not continue\npub fn warn_if_in_git_repo() -> crate::Result<()> {\n    let answer = Confirm::with_theme(&ColorfulTheme::default())\n        .with_prompt(\"❯ You are inside a git repository. Do you wish to continue?\")\n        .default(false)\n        .interact()?;\n\n    if answer {\n        Ok(())\n    } else {\n        Err(Error::msg(\n            \"Aborted: You've chose not to continue.\".to_string(),\n        ))\n    }\n}\n\n/// Validates the application name.\nfn validate_app_name(app_name: &str) -> Result<(), &str> {\n    if app_name.is_empty() {\n        return Err(\"app name could not be empty\");\n    }\n\n    let mut chars = app_name.chars();\n    if let Some(ch) = chars.next() {\n        if ch.is_ascii_digit() {\n            return Err(\"the name cannot start with a digit\");\n        }\n        if !(unicode_xid::UnicodeXID::is_xid_start(ch) || ch == '_') {\n            return Err(\n                \"the first character must be a Unicode XID start character (most letters or `_`)\",\n            );\n        }\n    }\n    for ch in chars {\n        if !(unicode_xid::UnicodeXID::is_xid_continue(ch) || ch == '-') {\n            return Err(\n                \"characters must be Unicode XID characters (numbers, `-`, `_`, or most letters)\",\n            );\n        }\n    }\n    Ok(())\n}\n\n/// Provides a selection menu to the user for choosing from a list of options.\n/// Returns the selected option or a default if selection fails.\nfn select_option<T>(text: &str, options: &[T]) -> crate::Result<T>\nwhere\n    T: Default + ToString + Clone,\n{\n    let selection = Select::with_theme(&ColorfulTheme::default())\n        .with_prompt(text)\n        .default(0)\n        .items(options)\n        .interact()?;\n    Ok(options.get(selection).cloned().unwrap_or_default())\n}\n\n/// start wizard\n///\n/// # Errors\n/// when could not show user selection or user chose not continue\npub fn start(args: &ArgsPlaceholder) -> crate::Result<Selections> {\n    // user provided everything via flags so no need to prompt, just return\n    if let (Some(db), Some(bg), Some(assets)) =\n        (args.db.clone(), args.bg.clone(), args.assets.clone())\n    {\n        return Ok(Selections {\n            db,\n            background: bg,\n            asset: assets,\n        });\n    }\n\n    let template = select_option(\n        \"❯ What would you like to build?\",\n        &Template::iter().collect::<Vec<_>>(),\n    )?;\n\n    match template {\n        Template::Lightweight => Ok(Selections {\n            db: DBOption::None,\n            background: BackgroundOption::Async,\n            asset: AssetsOption::None,\n        }),\n        Template::RestApi => Ok(Selections {\n            db: select_db(args)?,\n            background: select_background(args, None)?,\n            asset: AssetsOption::None,\n        }),\n        Template::SaasServerSideRendering => Ok(Selections {\n            db: select_db(args)?,\n            background: select_background(args, None)?,\n            asset: AssetsOption::Serverside,\n        }),\n        Template::SaasClientSideRendering => Ok(Selections {\n            db: select_db(args)?,\n            background: select_background(args, None)?,\n            asset: AssetsOption::Clientside,\n        }),\n        Template::Advanced => {\n            let db = select_db(args)?;\n            Ok(Selections {\n                db,\n                background: select_background(args, None)?,\n                asset: select_asset(args)?,\n            })\n        }\n    }\n}\n\n/// Prompts the user to select a database option if none is provided in the\n/// arguments.\nfn select_db(args: &ArgsPlaceholder) -> crate::Result<DBOption> {\n    let dboption = if let Some(dboption) = args.db.clone() {\n        dboption\n    } else {\n        select_option(\n            \"❯ Select a DB Provider\",\n            &DBOption::iter().collect::<Vec<_>>(),\n        )?\n    };\n    Ok(dboption)\n}\n\n/// Prompts the user to select a background worker option if none is provided in\n/// the arguments.\nfn select_background(\n    args: &ArgsPlaceholder,\n    filters: Option<&Vec<BackgroundOption>>,\n) -> crate::Result<BackgroundOption> {\n    let bgopt = if let Some(bgopt) = args.bg.clone() {\n        bgopt\n    } else {\n        let available_options = BackgroundOption::iter()\n            .filter(|opt| filters.as_ref().is_none_or(|f| !f.contains(opt)))\n            .collect::<Vec<_>>();\n\n        select_option(\"❯ Select your background worker type\", &available_options)?\n    };\n    Ok(bgopt)\n}\n\n/// Prompts the user to select an asset configuration if none is provided in the\n/// arguments.\nfn select_asset(args: &ArgsPlaceholder) -> crate::Result<AssetsOption> {\n    let assetopt = if let Some(assetopt) = args.assets.clone() {\n        assetopt\n    } else {\n        select_option(\n            \"❯ Select an asset serving configuration\",\n            &AssetsOption::iter().collect::<Vec<_>>(),\n        )?\n    };\n    Ok(assetopt)\n}\n"
  },
  {
    "path": "loco-new/tests/assertion/mod.rs",
    "content": "pub mod string;\npub mod toml;\npub mod yaml;\n"
  },
  {
    "path": "loco-new/tests/assertion/string.rs",
    "content": "#![allow(clippy::missing_panics_doc)]\nuse std::path::PathBuf;\n\nuse regex::Regex;\n\n#[must_use]\npub fn load(path: PathBuf) -> String {\n    std::fs::read_to_string(path).expect(\"could not read file\")\n}\n\npub fn assert_line_regex(content: &str, expected: &str) {\n    let re = Regex::new(expected).unwrap();\n\n    // Use assert! to check the regex match and panic if it fails\n    assert!(\n        // sanitize windows crlf\n        re.is_match(&content.replace('\\r', \"\")),\n        \"Assertion failed: The content did not match the expected string. Expected: '{expected}', \\\n         content:\\n{content}\"\n    );\n}\n\npub fn assert_str_not_exists(content: &str, expected: &str) {\n    // Use assert! to check the regex match and panic if it fails\n    assert!(\n        !content.contains(expected),\n        \"Assertion failed: The content matched the unexpected string. Expected string to not \\\n         exist: '{expected}', content in:\\n{content}\",\n    );\n}\n\npub fn assert_contains(content: &str, expected: &str) {\n    let content_sanitized = content.replace('\\r', \"\");\n    let expected_sanitized = expected.replace('\\r', \"\");\n\n    assert!(\n        content_sanitized.contains(&expected_sanitized),\n        \"Assertion failed: The content did not contain the expected string. \\\n         Expected: '{expected_sanitized}', content:\\n{content_sanitized}\"\n    );\n}\n\npub fn assert_not_contains(content: &str, unexpected: &str) {\n    let content_sanitized = content.replace('\\r', \"\");\n    let unexpected_sanitized = unexpected.replace('\\r', \"\");\n\n    assert!(\n        !content_sanitized.contains(&unexpected_sanitized),\n        \"Assertion failed: The content unexpectedly contained the string. \\\n         Unexpected: '{unexpected_sanitized}', content:\\n{content_sanitized}\"\n    );\n}\n"
  },
  {
    "path": "loco-new/tests/assertion/toml.rs",
    "content": "#![allow(clippy::missing_panics_doc)]\nuse std::path::PathBuf;\n\nuse toml::Value;\n\n#[must_use]\npub fn load(path: PathBuf) -> toml::Value {\n    let s = std::fs::read_to_string(path).expect(\"could not open file\");\n    toml::from_str(&s).expect(\"invalid toml content\")\n}\n\npub fn assert_path_value_eq_string(toml: &Value, path: &[&str], expected: &str) {\n    let expected_value = Value::String(expected.to_string());\n    assert_path_value_eq(toml, path, &expected_value);\n}\n\npub fn eq_path_value_eq_bool(toml: &Value, path: &[&str], expected: bool) {\n    let expected_value = Value::Boolean(expected);\n    assert_path_value_eq(toml, path, &expected_value);\n}\n\npub fn assert_path_is_empty_array(toml: &Value, path: &[&str]) {\n    let actual = get_value_at_path(toml, path);\n\n    assert!(\n        match actual {\n            Some(Value::Array(arr)) => arr.is_empty(),\n            None => true,\n            _ => false,\n        },\n        \"Assertion failed: Path {path:?} is not an empty array. Actual value: {actual:?}\"\n    );\n}\n\n/// Assert that the value at the specified path is an array and matches the\n/// expected array.\npub fn assert_path_value_eq_array(toml: &Value, path: &[&str], expected: &[Value]) {\n    let expected_value = Value::Array(expected.to_vec());\n    assert_path_value_eq(toml, path, &expected_value);\n}\n\n/// Assert that a TOML value contains a specific key path and that it matches\n/// the expected value.\npub fn assert_path_value_eq(toml: &Value, path: &[&str], expected: &Value) {\n    let actual = get_value_at_path(toml, path);\n    assert!(\n        actual == Some(expected),\n        \"Assertion failed: Path {path:?} does not match expected value. Expected: {expected:?}, \\\n         Actual: {actual:?}\"\n    );\n}\n\n/// Assert that a TOML value contains a specific path, and that the value is an\n/// object (table).\npub fn assert_path_is_object(toml: &Value, path: &[&str]) {\n    let actual = get_value_at_path(toml, path).unwrap();\n    assert!(\n        matches!(actual, Value::Table(_)),\n        \"Assertion failed: Path {path:?} is not an object. Actual value: {actual:?}\"\n    );\n}\n\n/// Helper function to concatenate keys of a nested table to form a string.\n#[must_use]\npub fn get_keys_concatenated_as_string(toml: &Value, path: &[&str]) -> Option<String> {\n    let value_at_path = get_value_at_path(toml, path)?;\n    if let Value::Table(table) = value_at_path {\n        let mut concatenated_string = String::new();\n        for key in table.keys() {\n            concatenated_string.push_str(key);\n        }\n        Some(concatenated_string)\n    } else {\n        None\n    }\n}\n\n/// Assert that the TOML value at the given path is empty (either an empty table\n/// or array).\npub fn assert_path_is_empty(toml: &Value, path: &[&str]) {\n    let actual = get_value_at_path(toml, path);\n\n    assert!(\n        match actual {\n            Some(Value::Table(table)) => table.is_empty(),\n            Some(Value::Array(arr)) => arr.is_empty(),\n            None => true,\n            _ => false,\n        },\n        \"Assertion failed: Path {path:?} is not empty. Actual value: {actual:?}\"\n    );\n}\n\npub fn assert_path_exists(toml: &Value, path: &[&str]) {\n    let actual = get_value_at_path(toml, path);\n\n    assert!(\n        actual.is_some(),\n        \"Assertion failed: Path {path:?} does not exist. Actual value: {actual:?}\"\n    );\n}\n\n/// Internal helper function to traverse a TOML structure and get the value at a\n/// specific path.\n#[must_use]\npub fn get_value_at_path<'a>(toml: &'a Value, path: &[&str]) -> Option<&'a Value> {\n    let mut current = toml;\n    for &key in path {\n        match current {\n            Value::Table(table) => {\n                current = table.get(key)?;\n            }\n            Value::Array(arr) => match key.parse::<usize>() {\n                Ok(index) => current = arr.get(index)?,\n                Err(_) => return None,\n            },\n            _ => return None,\n        }\n    }\n    Some(current)\n}\n"
  },
  {
    "path": "loco-new/tests/assertion/yaml.rs",
    "content": "#![allow(clippy::missing_panics_doc)]\nuse std::{fs::File, io::BufReader, path::PathBuf};\n\nuse serde_yaml::Value;\n\n#[must_use]\npub fn load(path: PathBuf) -> serde_yaml::Value {\n    let file = File::open(path).expect(\"could not open file\");\n    let reader = BufReader::new(file);\n    serde_yaml::from_reader(reader).expect(\"invalid yaml content\")\n}\n\npub fn assert_path_value_eq_string(yml: &Value, path: &[&str], expected: &str) {\n    let expected_value = Value::String(expected.to_string());\n    assert_path_value_eq(yml, path, &expected_value);\n}\n\n/// Asserts that the YAML value at the specified path is equal to the expected\n/// boolean value.\npub fn assert_path_value_eq_bool(yml: &Value, path: &[&str], expected: bool) {\n    let expected_value = Value::Bool(expected);\n    assert_path_value_eq(yml, path, &expected_value);\n}\n\n/// Asserts that the YAML value at the specified path is equal to the expected\n/// number value.\npub fn assert_path_value_eq_int(yml: &Value, path: &[&str], expected: i64) {\n    let expected_value = Value::Number(serde_yaml::Number::from(expected));\n    assert_path_value_eq(yml, path, &expected_value);\n}\n\npub fn assert_path_value_eq_float(yml: &Value, path: &[&str], expected: f64) {\n    let expected_value = Value::Number(serde_yaml::Number::from(expected));\n    assert_path_value_eq(yml, path, &expected_value);\n}\n\n/// Asserts that the YAML mapping at the specified path contains the expected\n/// number of keys.\npub fn assert_path_key_count(yml: &Value, path: &[&str], expected_count: usize) {\n    let actual = get_value_at_path(yml, path).expect(\"Path not found in YAML structure\");\n    assert!(\n        matches!(actual, Value::Mapping(map) if map.len() == expected_count),\n        \"Assertion failed: Path {:?} does not contain the expected number of keys. Expected: {}, \\\n         Actual: {}\",\n        path,\n        expected_count,\n        match actual {\n            Value::Mapping(map) => map.len(),\n            _ => 0,\n        }\n    );\n}\n\n/// Assert that a YAML value contains a specific key path and that it matches\n/// the expected value.\npub fn assert_path_value_eq(yml: &Value, path: &[&str], expected: &Value) {\n    let actual = get_value_at_path(yml, path);\n    assert!(\n        actual == Some(expected),\n        \"Assertion failed: Path {path:?} does not match expected value. Expected: {expected:?}, \\\n         Actual: {actual:?}\"\n    );\n}\n\n// pub fn assert_path_value_eq_mapping(yml: &Value, path: &[&str], expected:\n// &serde_yaml::Mapping) {     let actual = get_value_at_path(yml,\n// path).unwrap();     assert!(\n//         matches!(actual, Value::Mapping(map) if map == expected),\n//         \"Assertion failed: Path {path:?} does not match expected mapping.\n// Expected: {expected:?}, Actual: {actual:?}\"     );\n// }\n\n/// Assert that a YAML value contains a specific path, and that the value is an\n/// object.\npub fn assert_path_is_object(yml: &Value, path: &[&str]) {\n    let actual = get_value_at_path(yml, path).unwrap();\n    assert!(\n        matches!(actual, Value::Mapping(_)),\n        \"Assertion failed: Path {path:?} is not an object. Actual value: {actual:?}\"\n    );\n}\n\n/// Helper function to concatenate keys of a nested mapping to form a string.\n#[must_use]\npub fn get_keys_concatenated_as_string(yml: &Value, path: &[&str]) -> Option<String> {\n    let value_at_path = get_value_at_path(yml, path)?;\n    if let Value::Mapping(map) = value_at_path {\n        let mut concatenated_string = String::new();\n        for key in map.keys() {\n            if let Value::String(key_str) = key {\n                concatenated_string.push_str(key_str);\n            }\n        }\n        Some(concatenated_string)\n    } else {\n        None\n    }\n}\n\n/// Assert that the YAML value at the given path is empty (either an empty\n/// object or sequence).\npub fn assert_path_is_empty(yml: &Value, path: &[&str]) {\n    let actual = get_value_at_path(yml, path);\n\n    assert!(\n        match actual {\n            Some(Value::Mapping(map)) => map.is_empty(),\n            Some(Value::Sequence(seq)) => seq.is_empty(),\n            Some(Value::Null) | None => true,\n            _ => {\n                false\n            }\n        },\n        \"Assertion failed: Path {path:?} is not empty. Actual value: {actual:?}\"\n    );\n}\n\npub fn assert_path_value_eq_mapping(yml: &Value, path: &[&str], expected: &serde_yaml::Mapping) {\n    let actual = get_value_at_path(yml, path).expect(\"Path not found in YAML structure\");\n    assert!(\n        matches!(actual, Value::Mapping(map) if map == expected),\n        \"Assertion failed: Path {path:?} does not match expected mapping. Expected: \\\n         {expected:#?}, Actual: {actual:#?}\"\n    );\n}\n\n/// Internal helper function to traverse a YAML structure and get the value at a\n/// specific path.\n#[must_use]\npub fn get_value_at_path<'a>(yml: &'a Value, path: &[&str]) -> Option<&'a Value> {\n    let mut current = yml;\n    for &key in path {\n        match current {\n            Value::Mapping(map) => {\n                current = map.get(Value::String(key.to_string()))?;\n            }\n            Value::Sequence(seq) => match key.parse::<usize>() {\n                Ok(index) => current = seq.get(index)?,\n                Err(_) => return None,\n            },\n            _ => return None,\n        }\n    }\n    Some(current)\n}\n"
  },
  {
    "path": "loco-new/tests/mod.rs",
    "content": "mod templates;\nmod wizard;\n\npub mod assertion;\n"
  },
  {
    "path": "loco-new/tests/templates/asset.rs",
    "content": "use loco::{settings, wizard::AssetsOption};\nuse rstest::rstest;\n\nuse super::*;\nuse crate::assertion;\n\npub fn run_generator(asset: AssetsOption) -> TestGenerator {\n    let settings = settings::Settings {\n        asset: asset.into(),\n        ..Default::default()\n    };\n\n    TestGenerator::generate(settings)\n}\n\n#[rstest]\nfn test_config_file_middleware_when_asset_empty(\n    #[values(\"config/development.yaml\", \"config/test.yaml\")] config_file: &str,\n) {\n    let generator: TestGenerator = run_generator(AssetsOption::None);\n    let content = assertion::yaml::load(generator.path(config_file));\n\n    assertion::yaml::assert_path_is_empty(&content, &[\"server\", \"middlewares\"]);\n}\n\n#[rstest]\nfn test_config_file_middleware_asset_server(\n    #[values(\"config/development.yaml\", \"config/test.yaml\")] config_file: &str,\n) {\n    let generator: TestGenerator = run_generator(AssetsOption::Serverside);\n    let content = assertion::yaml::load(generator.path(config_file));\n\n    assertion::yaml::assert_path_is_object(&content, &[\"server\", \"middlewares\", \"static\"]);\n\n    let expected: serde_yaml::Value = serde_yaml::from_str(\n        r\"\nenable: true\nmust_exist: true\nprecompressed: false\nfolder:\n    uri: /static\n    path: assets/static\nfallback: assets/static/404.html\n\",\n    )\n    .unwrap();\n    assertion::yaml::assert_path_value_eq(\n        &content,\n        &[\"server\", \"middlewares\", \"static\"],\n        &expected,\n    );\n}\n\n#[rstest]\nfn test_config_file_middleware_asset_client(\n    #[values(\"config/development.yaml\", \"config/test.yaml\")] config_file: &str,\n) {\n    let generator: TestGenerator = run_generator(AssetsOption::Clientside);\n    let content = assertion::yaml::load(generator.path(config_file));\n\n    assertion::yaml::assert_path_is_object(&content, &[\"server\", \"middlewares\"]);\n\n    let expected: serde_yaml::Value = serde_yaml::from_str(\n        r\"\nfallback:\n    enable: false\nstatic:\n    enable: true\n    must_exist: true\n    precompressed: false\n    folder:\n        uri: /\n        path: frontend/dist\n    fallback: frontend/dist/index.html\n\",\n    )\n    .unwrap();\n    assertion::yaml::assert_path_value_eq(&content, &[\"server\", \"middlewares\"], &expected);\n}\n\n#[rstest]\nfn test_cargo_toml(\n    #[values(AssetsOption::None, AssetsOption::Serverside, AssetsOption::Clientside)]\n    asset: AssetsOption,\n) {\n    let generator = run_generator(asset.clone());\n    let content = assertion::toml::load(generator.path(\"Cargo.toml\"));\n\n    insta::assert_snapshot!(\n        format!(\"cargo_dependencies_{:?}\", asset),\n        content.get(\"dependencies\").unwrap()\n    );\n}\n\n#[rstest]\nfn test_github_ci_yaml(\n    #[values(AssetsOption::None, AssetsOption::Serverside, AssetsOption::Clientside)]\n    asset: AssetsOption,\n) {\n    let generator: TestGenerator = run_generator(asset.clone());\n    let content =\n        assertion::string::load(generator.path(\".github\").join(\"workflows\").join(\"ci.yaml\"));\n\n    let frontend_section = r\"      - name: Setup node\n        uses: actions/setup-node@v4\n        with:\n          node-version: ${{matrix.node-version}}\n      - name: Build frontend\n        run: npm install && npm run build\n        working-directory: ./frontend\n      - name: Setup Rust cache\n        uses: Swatinem/rust-cache@v2\";\n\n    match asset {\n        AssetsOption::Serverside | AssetsOption::None => {\n            assertion::string::assert_not_contains(&content, frontend_section);\n        }\n        AssetsOption::Clientside => {\n            assertion::string::assert_contains(&content, frontend_section);\n        }\n    }\n}\n"
  },
  {
    "path": "loco-new/tests/templates/auth.rs",
    "content": "use loco::{settings, wizard::DBOption};\nuse rstest::rstest;\n\nuse super::*;\nuse crate::assertion;\n\npub fn run_generator(enable_auth: bool, db: DBOption) -> TestGenerator {\n    let settings = settings::Settings {\n        package_name: \"loco-app-test\".to_string(),\n        module_name: \"loco_app_test\".to_string(),\n        auth: enable_auth,\n        db: db.into(),\n        ..Default::default()\n    };\n\n    TestGenerator::generate(settings)\n}\n\n#[rstest]\nfn test_config_file_without_auth(\n    #[values(\"config/development.yaml\", \"config/test.yaml\")] config_file: &str,\n) {\n    let generator = run_generator(false, DBOption::None);\n    let content = assertion::yaml::load(generator.path(config_file));\n    assertion::yaml::assert_path_is_empty(&content, &[\"auth\"]);\n}\n\n#[rstest]\nfn test_config_file_with_auth(\n    #[values(\"config/development.yaml\", \"config/test.yaml\")] config_file: &str,\n) {\n    let generator = run_generator(true, DBOption::None);\n    let content = assertion::yaml::load(generator.path(config_file));\n    assertion::yaml::assert_path_key_count(&content, &[\"auth\"], 1);\n\n    assertion::yaml::assert_path_key_count(&content, &[\"auth\", \"jwt\"], 2);\n}\n\n#[test]\nfn test_config_file_development_rand_secret() {\n    let generator = run_generator(true, DBOption::None);\n    let content = assertion::yaml::load(generator.path(\"config/development.yaml\"));\n    assertion::yaml::assert_path_value_eq_string(\n        &content,\n        &[\"auth\", \"jwt\", \"secret\"],\n        \"IhPi3oZCnaWvL2oIeA07\",\n    );\n}\n\n#[test]\nfn test_config_file_test_rand_secret() {\n    let generator = run_generator(true, DBOption::None);\n    let content = assertion::yaml::load(generator.path(\"config/test.yaml\"));\n    assertion::yaml::assert_path_value_eq_string(\n        &content,\n        &[\"auth\", \"jwt\", \"secret\"],\n        \"mg3ZtJzh0NoAKhdDqpQ2\",\n    );\n}\n\n#[rstest]\nfn test_app_rs(\n    #[values(true, false)] auth: bool,\n    #[values(DBOption::None, DBOption::Sqlite)] db: DBOption,\n) {\n    let generator = run_generator(auth, db.clone());\n    insta::assert_snapshot!(\n        format!(\"src_app_rs_auth_{:?}_{:?}\", auth, db),\n        std::fs::read_to_string(generator.path(\"src/app.rs\")).expect(\"could not open file\")\n    );\n}\n\n#[rstest]\nfn test_src_controllers_mod_rs(#[values(true, false)] auth: bool) {\n    let generator = run_generator(auth, DBOption::None);\n    let content = std::fs::read_to_string(generator.path(\"src/controllers/mod.rs\"))\n        .expect(\"could not open file\");\n\n    if auth {\n        assertion::string::assert_line_regex(&content, \"(?m)^pub mod auth;$\");\n    } else {\n        assertion::string::assert_line_regex(&content, \"(?m)^pub mod home;$\");\n    }\n}\n\n#[rstest]\nfn test_src_views_mod_rs(#[values(true, false)] auth: bool) {\n    let generator = run_generator(auth, DBOption::None);\n    let content =\n        std::fs::read_to_string(generator.path(\"src/views/mod.rs\")).expect(\"could not open file\");\n\n    if auth {\n        assertion::string::assert_line_regex(&content, \"(?m)^pub mod auth;$\");\n    } else {\n        assertion::string::assert_line_regex(&content, \"(?m)^pub mod home;$\");\n    }\n}\n\n#[rstest]\nfn test_tests_requests_mod_rs(#[values(true, false)] auth: bool) {\n    let generator = run_generator(auth, DBOption::None);\n    let content = std::fs::read_to_string(generator.path(\"tests/requests/mod.rs\"))\n        .expect(\"could not open file\");\n\n    if auth {\n        assertion::string::assert_line_regex(&content, \"(?m)^mod auth;$\");\n        assertion::string::assert_line_regex(&content, \"(?m)^mod prepare_data;$\");\n    } else {\n        assertion::string::assert_line_regex(&content, \"(?m)^mod home;$\");\n    }\n}\n\n#[rstest]\nfn test_migration_src_lib(#[values(true)] auth: bool) {\n    let generator = run_generator(auth, DBOption::Sqlite);\n    let content = std::fs::read_to_string(generator.path(\"migration/src/lib.rs\"))\n        .expect(\"could not open file\");\n\n    if auth {\n        assertion::string::assert_line_regex(&content, \"(?m)^mod m20220101_000001_users;$\");\n        assertion::string::assert_line_regex(\n            &content,\n            r\"(?m)Box::new\\(m20220101_000001_users::Migration\\),$\",\n        );\n    }\n}\n\n#[rstest]\nfn test_models_mod_rs(#[values(true)] auth: bool) {\n    let generator = run_generator(auth, DBOption::Sqlite);\n    let content =\n        std::fs::read_to_string(generator.path(\"src/models/mod.rs\")).expect(\"could not open file\");\n\n    if auth {\n        assertion::string::assert_line_regex(&content, \"(?m)^pub mod users;$\");\n    }\n}\n\n#[rstest]\nfn test_models_entities_mod_rs(#[values(true)] auth: bool) {\n    let generator = run_generator(auth, DBOption::Sqlite);\n    let content = std::fs::read_to_string(generator.path(\"src/models/_entities/mod.rs\"))\n        .expect(\"could not open file\");\n\n    if auth {\n        assertion::string::assert_line_regex(&content, \"(?m)^pub mod users;$\");\n    }\n}\n\n#[rstest]\nfn test_models_entities_prelude_rs(#[values(true)] auth: bool) {\n    let generator = run_generator(auth, DBOption::Sqlite);\n    let content = std::fs::read_to_string(generator.path(\"src/models/_entities/prelude.rs\"))\n        .expect(\"could not open file\");\n\n    if auth {\n        assertion::string::assert_line_regex(\n            &content,\n            \"(?m)^pub use super::users::Entity as Users;$\",\n        );\n    }\n}\n\n#[rstest]\nfn test_tests_models_mod_rs(#[values(true, false)] auth: bool) {\n    let generator = run_generator(auth, DBOption::Sqlite);\n    let content = std::fs::read_to_string(generator.path(\"tests/models/mod.rs\"))\n        .expect(\"could not open file\");\n\n    if auth {\n        assertion::string::assert_line_regex(&content, \"(?m)^mod users;$\");\n    } else {\n        assert!(content.is_empty());\n    }\n}\n"
  },
  {
    "path": "loco-new/tests/templates/background.rs",
    "content": "use loco::{settings, wizard::BackgroundOption};\nuse rstest::rstest;\n\nuse super::*;\nuse crate::assertion;\n\npub fn run_generator(background: BackgroundOption) -> TestGenerator {\n    let settings = settings::Settings {\n        background: background.into(),\n        ..Default::default()\n    };\n\n    TestGenerator::generate(settings)\n}\n\n#[rstest]\nfn test_config_file_queue(\n    #[values(\"config/development.yaml\", \"config/test.yaml\")] config_file: &str,\n    #[values(\n        BackgroundOption::Async,\n        BackgroundOption::Queue,\n        BackgroundOption::Blocking\n    )]\n    background: BackgroundOption,\n) {\n    let generator = run_generator(background.clone());\n    let content = assertion::yaml::load(generator.path(config_file));\n\n    if background == BackgroundOption::Queue {\n        assertion::yaml::assert_path_is_object(&content, &[\"queue\"]);\n        assertion::yaml::assert_path_key_count(&content, &[\"queue\"], 3);\n        assertion::yaml::assert_path_value_eq_string(&content, &[\"queue\", \"kind\"], \"Redis\");\n        assertion::yaml::assert_path_value_eq_bool(\n            &content,\n            &[\"queue\", \"dangerously_flush\"],\n            false,\n        );\n\n        let mut inner_uri = serde_yaml::Mapping::new();\n        inner_uri.insert(\n            serde_yaml::Value::String(\"get_env(name=\\\"REDIS_URL\\\"\".to_string()),\n            serde_yaml::Value::Null,\n        );\n        inner_uri.insert(\n            serde_yaml::Value::String(\"default=\\\"redis://127.0.0.1\\\")\".to_string()),\n            serde_yaml::Value::Null,\n        );\n        let mut uri = serde_yaml::Mapping::new();\n        uri.insert(\n            serde_yaml::Value::Mapping(inner_uri),\n            serde_yaml::Value::Null,\n        );\n\n        assertion::yaml::assert_path_value_eq_mapping(&content, &[\"queue\", \"uri\"], &uri);\n    } else {\n        assertion::yaml::assert_path_is_empty(&content, &[\"queue\"]);\n    }\n}\n\n#[rstest]\nfn test_config_file_workers(\n    #[values(\"config/development.yaml\")] config_file: &str,\n    #[values(\n        BackgroundOption::Async,\n        BackgroundOption::Queue,\n        BackgroundOption::Blocking\n    )]\n    background: BackgroundOption,\n) {\n    let generator = run_generator(background.clone());\n    let content = assertion::yaml::load(generator.path(config_file));\n\n    match background {\n        BackgroundOption::Async => {\n            assertion::yaml::assert_path_value_eq_string(\n                &content,\n                &[\"workers\", \"mode\"],\n                \"BackgroundAsync\",\n            );\n        }\n        BackgroundOption::Queue => {\n            assertion::yaml::assert_path_value_eq_string(\n                &content,\n                &[\"workers\", \"mode\"],\n                \"BackgroundQueue\",\n            );\n        }\n        BackgroundOption::Blocking => {\n            assertion::yaml::assert_path_value_eq_string(\n                &content,\n                &[\"workers\", \"mode\"],\n                \"ForegroundBlocking\",\n            );\n        }\n    };\n\n    assertion::yaml::assert_path_key_count(&content, &[\"workers\"], 1);\n}\n\n#[rstest]\nfn test_config_file_workers_tests(\n    #[values(\n        BackgroundOption::Async,\n        BackgroundOption::Queue,\n        BackgroundOption::Blocking\n    )]\n    background: BackgroundOption,\n) {\n    let generator = run_generator(background.clone());\n    let content = assertion::yaml::load(generator.path(\"config/test.yaml\"));\n\n    match background {\n        BackgroundOption::Async => {\n            assertion::yaml::assert_path_value_eq_string(\n                &content,\n                &[\"workers\", \"mode\"],\n                \"ForegroundBlocking\",\n            );\n        }\n        BackgroundOption::Queue => {\n            assertion::yaml::assert_path_value_eq_string(\n                &content,\n                &[\"workers\", \"mode\"],\n                \"ForegroundBlocking\",\n            );\n        }\n        BackgroundOption::Blocking => {\n            assertion::yaml::assert_path_value_eq_string(\n                &content,\n                &[\"workers\", \"mode\"],\n                \"ForegroundBlocking\",\n            );\n        }\n    };\n\n    assertion::yaml::assert_path_key_count(&content, &[\"workers\"], 1);\n}\n\n#[rstest]\nfn test_app_rs(\n    #[values(\n        BackgroundOption::Async,\n        BackgroundOption::Queue,\n        BackgroundOption::Blocking\n    )]\n    background: BackgroundOption,\n) {\n    let generator = run_generator(background.clone());\n    insta::assert_snapshot!(\n        format!(\"src_app_rs_{:?}\", background),\n        std::fs::read_to_string(generator.path(\"src/app.rs\")).expect(\"could not open file\")\n    );\n}\n\n#[rstest]\nfn test_src_lib_rs(\n    #[values(\n        BackgroundOption::Async,\n        BackgroundOption::Queue,\n        BackgroundOption::Blocking\n    )]\n    background: BackgroundOption,\n) {\n    let generator = run_generator(background.clone());\n\n    let content =\n        std::fs::read_to_string(generator.path(\"src/lib.rs\")).expect(\"could not open file\");\n\n    assertion::string::assert_line_regex(&content, \"(?m)^pub mod workers;$\");\n}\n\n#[rstest]\nfn test_tests_mod_rs(\n    #[values(\n        BackgroundOption::Async,\n        BackgroundOption::Queue,\n        BackgroundOption::Blocking\n    )]\n    background: BackgroundOption,\n) {\n    let generator = run_generator(background.clone());\n\n    let content =\n        std::fs::read_to_string(generator.path(\"tests/mod.rs\")).expect(\"could not open file\");\n\n    assertion::string::assert_line_regex(&content, \"(?m)^mod workers;$\");\n}\n"
  },
  {
    "path": "loco-new/tests/templates/db.rs",
    "content": "use loco::{settings, wizard::DBOption};\nuse rstest::rstest;\n\nuse super::*;\nuse crate::assertion;\n\npub fn run_generator(db: DBOption) -> TestGenerator {\n    let settings = settings::Settings {\n        package_name: \"loco-app-test\".to_string(),\n        module_name: \"loco_app_test\".to_string(),\n        db: db.into(),\n        ..Default::default()\n    };\n\n    TestGenerator::generate(settings)\n}\n\n#[rstest]\nfn test_config_file_no_db(\n    #[values(\"config/development.yaml\", \"config/test.yaml\")] config_file: &str,\n) {\n    let generator = run_generator(DBOption::None);\n    let content = assertion::yaml::load(generator.path(config_file));\n    assertion::yaml::assert_path_is_empty(&content, &[\"database\"]);\n}\n\n#[rstest]\nfn test_config_with_sqlite(\n    #[values(DBOption::Sqlite, DBOption::Postgres)] db: DBOption,\n    #[values(\"config/development.yaml\", \"config/test.yaml\")] config_file: &str,\n) {\n    let generator = run_generator(db.clone());\n    let content = assertion::yaml::load(generator.path(config_file));\n\n    insta::assert_snapshot!(\n        format!(\n            \"{}_config_database_{:?}\",\n            config_file.replace(['/', '.'], \"_\"),\n            db\n        ),\n        format!(\n            \"{:#?}\",\n            assertion::yaml::get_value_at_path(&content, &[\"database\"]).unwrap()\n        )\n    );\n}\n\n#[rstest]\nfn test_cargo_toml(#[values(DBOption::None, DBOption::Sqlite, DBOption::Postgres)] db: DBOption) {\n    let generator = run_generator(db.clone());\n    let content = assertion::toml::load(generator.path(\"Cargo.toml\"));\n\n    insta::assert_snapshot!(\n        format!(\"cargo_dependencies_{:?}\", db),\n        content.get(\"dependencies\").unwrap()\n    );\n}\n\n#[rstest]\nfn test_app_rs(#[values(DBOption::None, DBOption::Sqlite, DBOption::Postgres)] db: DBOption) {\n    let generator = run_generator(db.clone());\n    insta::assert_snapshot!(\n        format!(\"src_app_rs_{:?}\", db),\n        std::fs::read_to_string(generator.path(\"src/app.rs\")).expect(\"could not open file\")\n    );\n}\n\n#[rstest]\nfn test_src_lib_rs(#[values(DBOption::None, DBOption::Sqlite, DBOption::Postgres)] db: DBOption) {\n    let generator = run_generator(db.clone());\n\n    let content =\n        std::fs::read_to_string(generator.path(\"src/lib.rs\")).expect(\"could not open file\");\n\n    if db.enable() {\n        assertion::string::assert_line_regex(&content, \"(?m)^pub mod models;$\");\n    } else {\n        assertion::string::assert_str_not_exists(&content, \"pub mod models;\");\n    }\n}\n\n#[rstest]\nfn test_src_bin_main_rs(\n    #[values(DBOption::None, DBOption::Sqlite, DBOption::Postgres)] db: DBOption,\n) {\n    let generator = run_generator(db.clone());\n\n    let content =\n        std::fs::read_to_string(generator.path(\"src/bin/main.rs\")).expect(\"could not open file\");\n\n    if db.enable() {\n        assertion::string::assert_line_regex(&content, \"(?m)^use migration::Migrator;$\");\n        assertion::string::assert_line_regex(\n            &content,\n            r\"(?m)^    cli::main::<App, Migrator>\\(\\).await$\",\n        );\n    } else {\n        assertion::string::assert_str_not_exists(&content, \"(?m)^use migration::Migrator;$\");\n        assertion::string::assert_line_regex(&content, r\"(?m)^    cli::main::<App>\\(\\).await\");\n    }\n}\n\n#[rstest]\n#[cfg(windows)]\nfn test_src_bin_tool_rs(\n    #[values(DBOption::None, DBOption::Sqlite, DBOption::Postgres)] db: DBOption,\n) {\n    let generator = run_generator(db.clone());\n\n    let content =\n        std::fs::read_to_string(generator.path(\"src/bin/tool.rs\")).expect(\"could not open file\");\n\n    if db.enable() {\n        assertion::string::assert_line_regex(&content, \"(?m)^use migration::Migrator;$\");\n        assertion::string::assert_line_regex(\n            &content,\n            r\"(?m)^    cli::main::<App, Migrator>\\(\\).await$\",\n        );\n    } else {\n        assertion::string::assert_str_not_exists(&content, \"(?m)^use migration::Migrator;$\");\n        assertion::string::assert_line_regex(&content, r\"(?m)^    cli::main::<App>\\(\\).await\");\n    }\n}\n\n#[rstest]\nfn test_tests_mod_rs(#[values(DBOption::None, DBOption::Sqlite, DBOption::Postgres)] db: DBOption) {\n    let generator = run_generator(db.clone());\n\n    let content =\n        std::fs::read_to_string(generator.path(\"tests/mod.rs\")).expect(\"could not open file\");\n\n    if db.enable() {\n        assertion::string::assert_line_regex(&content, \"(?m)^mod models;$\");\n    } else {\n        assertion::string::assert_str_not_exists(&content, \"mod models;\");\n    }\n}\n"
  },
  {
    "path": "loco-new/tests/templates/features.rs",
    "content": "use loco::settings;\n\nuse super::*;\nuse crate::assertion;\n\npub fn run_generator(default_features: bool, names: &[&str]) -> TestGenerator {\n    let settings = settings::Settings {\n        features: settings::Features {\n            default_features,\n            names: names.iter().map(std::string::ToString::to_string).collect(),\n        },\n        ..Default::default()\n    };\n\n    TestGenerator::generate(settings)\n}\n\n#[test]\nfn test_cargo_toml_with_default_features_and_empty_names() {\n    let generator = run_generator(true, &[]);\n    let content = assertion::toml::load(generator.path(\"Cargo.toml\"));\n    assertion::toml::assert_path_exists(&content, &[\"workspace\", \"dependencies\", \"loco-rs\"]);\n    assertion::toml::assert_path_is_empty(\n        &content,\n        &[\"workspace\", \"dependencies\", \"loco-rs\", \"default-features\"],\n    );\n}\n\n#[test]\nfn test_cargo_toml_without_default_features_and_empty_names() {\n    let generator = run_generator(false, &[]);\n    let content = assertion::toml::load(generator.path(\"Cargo.toml\"));\n    assertion::toml::eq_path_value_eq_bool(\n        &content,\n        &[\"workspace\", \"dependencies\", \"loco-rs\", \"default-features\"],\n        false,\n    );\n}\n\n#[test]\nfn test_cargo_toml_with_features() {\n    let generator = run_generator(false, &[\"foo\", \"bar\"]);\n    let content = assertion::toml::load(generator.path(\"Cargo.toml\"));\n    assertion::toml::assert_path_value_eq_array(\n        &content,\n        &[\"dependencies\", \"loco-rs\", \"features\"],\n        &[\n            toml::Value::String(\"foo\".to_string()),\n            toml::Value::String(\"bar\".to_string()),\n        ],\n    );\n}\n\n#[test]\nfn test_cargo_toml_without_features() {\n    let generator = run_generator(false, &[]);\n    let content = assertion::toml::load(generator.path(\"Cargo.toml\"));\n    assertion::toml::assert_path_is_empty(&content, &[\"dependencies\", \"loco-rs\", \"features\"]);\n}\n"
  },
  {
    "path": "loco-new/tests/templates/initializers.rs",
    "content": "use loco::settings;\nuse rstest::rstest;\n\nuse super::*;\nuse crate::assertion;\n\npub fn run_generator(initializers: Option<settings::Initializers>) -> TestGenerator {\n    let settings = settings::Settings {\n        initializers,\n        ..Default::default()\n    };\n\n    TestGenerator::generate(settings)\n}\n\n#[test]\nfn test_app_rs_with_initializers() {\n    let generator = run_generator(Some(settings::Initializers { view_engine: true }));\n    insta::assert_snapshot!(\n        \"src_app_rs_without_initializers\",\n        std::fs::read_to_string(generator.path(\"src/app.rs\")).expect(\"could not open file\")\n    );\n}\n\n#[test]\nfn test_app_rs_without_view_engine() {\n    let generator = run_generator(None);\n    insta::assert_snapshot!(\n        \"src_app_rs_with_initializers\",\n        std::fs::read_to_string(generator.path(\"src/app.rs\")).expect(\"could not open file\")\n    );\n}\n\n#[rstest]\nfn test_src_initializers_mod_rs_view_engine(#[values(true, false)] view_engine: bool) {\n    let generator = run_generator(Some(settings::Initializers { view_engine }));\n\n    let content = std::fs::read_to_string(generator.path(\"src/initializers/mod.rs\"))\n        .expect(\"could not open file\");\n    if view_engine {\n        assertion::string::assert_line_regex(&content, \"(?m)^pub mod view_engine;$\");\n    } else {\n        assertion::string::assert_str_not_exists(&content, \"pub mod view_engine\");\n    }\n}\n"
  },
  {
    "path": "loco-new/tests/templates/mailer.rs",
    "content": "use loco::settings;\nuse rstest::rstest;\n\nuse super::*;\nuse crate::assertion;\n\npub fn run_generator(enable_mailer: bool) -> TestGenerator {\n    let settings = settings::Settings {\n        mailer: enable_mailer,\n        ..Default::default()\n    };\n\n    TestGenerator::generate(settings)\n}\n\n#[rstest]\nfn test_config_file_without_mailer(\n    #[values(\"config/development.yaml\", \"config/test.yaml\")] config_file: &str,\n) {\n    let generator = run_generator(false);\n    let content = assertion::yaml::load(generator.path(config_file));\n    assertion::yaml::assert_path_is_empty(&content, &[\"mailer\"]);\n}\n\n#[rstest]\nfn test_config_file_with_mailer(\n    #[values(\"config/development.yaml\", \"config/test.yaml\")] config_file: &str,\n) {\n    let generator = run_generator(true);\n    let content = assertion::yaml::load(generator.path(config_file));\n    if config_file == \"config/test.yaml\" {\n        assertion::yaml::assert_path_key_count(&content, &[\"mailer\"], 2);\n        assertion::yaml::assert_path_value_eq_bool(&content, &[\"mailer\", \"stub\"], true);\n    } else {\n        assertion::yaml::assert_path_key_count(&content, &[\"mailer\"], 1);\n    }\n\n    assertion::yaml::assert_path_key_count(&content, &[\"mailer\", \"smtp\"], 4);\n    assertion::yaml::assert_path_value_eq_bool(&content, &[\"mailer\", \"smtp\", \"enable\"], true);\n    assertion::yaml::assert_path_value_eq_int(&content, &[\"mailer\", \"smtp\", \"port\"], 1025);\n    assertion::yaml::assert_path_value_eq_bool(&content, &[\"mailer\", \"smtp\", \"secure\"], false);\n    assertion::yaml::assert_path_value_eq_string(\n        &content,\n        &[\"mailer\", \"smtp\", \"host\"],\n        \"localhost\",\n    );\n}\n\n#[rstest]\nfn test_cargo_toml(#[values(true, false)] mailer: bool) {\n    let generator = run_generator(mailer);\n    let content = assertion::toml::load(generator.path(\"Cargo.toml\"));\n\n    insta::assert_snapshot!(\n        format!(\"cargo_dependencies_mailer_{:?}\", mailer),\n        content.get(\"dependencies\").unwrap()\n    );\n}\n\n#[rstest]\nfn test_src_lib_rs(#[values(true, false)] mailer: bool) {\n    let generator = run_generator(mailer);\n\n    let content =\n        std::fs::read_to_string(generator.path(\"src/lib.rs\")).expect(\"could not open file\");\n\n    if mailer {\n        assertion::string::assert_line_regex(&content, \"(?m)^pub mod mailers;$\");\n    } else {\n        assertion::string::assert_str_not_exists(&content, \"pub mod mailers;;\");\n    }\n}\n"
  },
  {
    "path": "loco-new/tests/templates/mod.rs",
    "content": "use std::{\n    path::{Path, PathBuf},\n    sync::Arc,\n};\n\nuse loco::{\n    generator::{self, executer::FileSystem, template},\n    settings,\n};\nuse rand::{rngs::StdRng, SeedableRng};\n\nmod asset;\nmod auth;\nmod background;\nmod db;\nmod features;\nmod initializers;\nmod mailer;\nmod module_name;\n\npub struct TestGenerator {\n    tree: tree_fs::Tree,\n}\n\nimpl TestGenerator {\n    pub fn generate(settings: settings::Settings) -> Self {\n        let tree = tree_fs::TreeBuilder::default()\n            .drop(true)\n            .create()\n            .expect(\"create tree fs\");\n\n        let template_engine = template::Template::new(StdRng::seed_from_u64(42));\n\n        let fs: FileSystem = FileSystem::with_template_engine(\n            Path::new(\"base_template\"),\n            tree.root.as_path(),\n            template_engine,\n        );\n\n        generator::Generator::new(Arc::new(fs), settings)\n            .run()\n            .expect(\"run generate\");\n\n        Self { tree }\n    }\n\n    pub fn path(&self, path: &str) -> PathBuf {\n        self.tree.root.join(path)\n    }\n}\n"
  },
  {
    "path": "loco-new/tests/templates/module_name.rs",
    "content": "use loco::{settings, wizard::DBOption};\nuse rstest::rstest;\n\nuse super::*;\nuse crate::assertion;\n\npub fn run_generator() -> TestGenerator {\n    let settings = settings::Settings {\n        package_name: \"loco-app-test\".to_string(),\n        module_name: \"loco_app_test\".to_string(),\n        ..Default::default()\n    };\n\n    TestGenerator::generate(settings)\n}\n\n#[test]\nfn test_cargo_toml() {\n    let generator = run_generator();\n\n    let content = assertion::toml::load(generator.path(\"Cargo.toml\"));\n\n    assertion::toml::assert_path_value_eq_string(&content, &[\"package\", \"name\"], \"loco-app-test\");\n    assertion::toml::assert_path_value_eq_string(\n        &content,\n        &[\"package\", \"default-run\"],\n        \"loco_app_test-cli\",\n    );\n\n    let bin = content\n        .get(\"bin\")\n        .expect(\"bin\")\n        .get(0)\n        .expect(\"get first bin\");\n    assertion::toml::assert_path_value_eq_string(bin, &[\"name\"], \"loco_app_test-cli\");\n}\n\n#[rstest]\nfn test_use_name(#[values(\"src/bin/main.rs\", \"tests/requests/home.rs\")] file: &str) {\n    let generator = run_generator();\n\n    let content = std::fs::read_to_string(generator.path(file)).expect(\"could not open file\");\n\n    assertion::string::assert_line_regex(&content, \"(?m)^use loco_app_test::\");\n}\n\n#[rstest]\nfn test_use_name_with_db(\n    #[values(\"tests/models/users.rs\", \"tests/requests/prepare_data.rs\")] file: &str,\n) {\n    let settings = settings::Settings {\n        package_name: \"loco-app-test\".to_string(),\n        module_name: \"loco_app_test\".to_string(),\n        db: DBOption::Sqlite.into(),\n        auth: true,\n        ..Default::default()\n    };\n\n    let generator = TestGenerator::generate(settings);\n    println!(\"{:#?}\", generator.tree);\n\n    let content = std::fs::read_to_string(generator.path(file)).expect(\"could not open file\");\n\n    assertion::string::assert_line_regex(&content, \"(?m)^use loco_app_test::\");\n}\n\n#[rstest]\nfn test_use_name_with_auth(#[values(\"tests/requests/auth.rs\")] file: &str) {\n    let generator = super::auth::run_generator(true, DBOption::None);\n\n    let content = std::fs::read_to_string(generator.path(file)).expect(\"could not open file\");\n\n    assertion::string::assert_line_regex(&content, \"(?m)^use loco_app_test::\");\n}\n"
  },
  {
    "path": "loco-new/tests/templates/snapshots/r#mod__templates__asset__cargo_dependencies_Clientside.snap",
    "content": "---\nsource: tests/templates/asset.rs\nexpression: \"content.get(\\\"dependencies\\\").unwrap()\"\n---\n{ async-trait = { version = \"0.1\" }, axum = { version = \"0.8\" }, axum-extra = { features = [\"form\"], version = \"0.10\" }, loco-rs = { workspace = true }, regex = { version = \"1.11\" }, serde = { features = [\"derive\"], version = \"1\" }, serde_json = { version = \"1\" }, tokio = { default-features = false, features = [\"rt-multi-thread\"], version = \"1.45\" }, tracing = { version = \"0.1\" }, tracing-subscriber = { features = [\"env-filter\", \"json\"], version = \"0.3\" } }\n"
  },
  {
    "path": "loco-new/tests/templates/snapshots/r#mod__templates__asset__cargo_dependencies_None.snap",
    "content": "---\nsource: tests/templates/asset.rs\nexpression: \"content.get(\\\"dependencies\\\").unwrap()\"\n---\n{ async-trait = { version = \"0.1\" }, axum = { version = \"0.8\" }, loco-rs = { workspace = true }, regex = { version = \"1.11\" }, serde = { features = [\"derive\"], version = \"1\" }, serde_json = { version = \"1\" }, tokio = { default-features = false, features = [\"rt-multi-thread\"], version = \"1.45\" }, tracing = { version = \"0.1\" }, tracing-subscriber = { features = [\"env-filter\", \"json\"], version = \"0.3\" } }\n"
  },
  {
    "path": "loco-new/tests/templates/snapshots/r#mod__templates__asset__cargo_dependencies_Serverside.snap",
    "content": "---\nsource: tests/templates/asset.rs\nexpression: \"content.get(\\\"dependencies\\\").unwrap()\"\n---\n{ async-trait = { version = \"0.1\" }, axum = { version = \"0.8\" }, axum-extra = { features = [\"form\"], version = \"0.10\" }, fluent-templates = { features = [\"tera\"], version = \"0.13\" }, loco-rs = { workspace = true }, regex = { version = \"1.11\" }, serde = { features = [\"derive\"], version = \"1\" }, serde_json = { version = \"1\" }, tokio = { default-features = false, features = [\"rt-multi-thread\"], version = \"1.45\" }, tracing = { version = \"0.1\" }, tracing-subscriber = { features = [\"env-filter\", \"json\"], version = \"0.3\" }, unic-langid = { version = \"0.9\" } }\n"
  },
  {
    "path": "loco-new/tests/templates/snapshots/r#mod__templates__auth__src_app_rs_auth_false_None.snap",
    "content": "---\nsource: tests/templates/auth.rs\nexpression: \"std::fs::read_to_string(generator.path(\\\"src/app.rs\\\")).expect(\\\"could not open file\\\")\"\n---\nuse async_trait::async_trait;\nuse loco_rs::{\n    app::{AppContext, Hooks, Initializer},\n    bgworker::{\n        Queue},\n    boot::{create_app, BootResult, StartMode},\n    config::Config,\n    controller::AppRoutes,\n    environment::Environment,\n    task::Tasks,\n    Result,\n};\n\n#[allow(unused_imports)]\nuse crate::{\n    controllers ,tasks\n};\n\npub struct App;\n#[async_trait]\nimpl Hooks for App {\n    fn app_name() -> &'static str {\n        env!(\"CARGO_CRATE_NAME\")\n    }\n\n    fn app_version() -> String {\n        format!(\n            \"{} ({})\",\n            env!(\"CARGO_PKG_VERSION\"),\n            option_env!(\"BUILD_SHA\")\n                .or(option_env!(\"GITHUB_SHA\"))\n                .unwrap_or(\"dev\")\n        )\n    }\n\n    async fn boot(mode: StartMode, environment: &Environment, config: Config) -> Result<BootResult> {\n        create_app::<Self>(mode, environment, config).await\n    }\n\n    async fn initializers(_ctx: &AppContext) -> Result<Vec<Box<dyn Initializer>>> {\n        Ok(vec![])\n    }\n\n    fn routes(_ctx: &AppContext) -> AppRoutes {\n        AppRoutes::with_default_routes() // controller routes below\n            .add_route(controllers::home::routes())\n    }\n    async fn connect_workers(_ctx: &AppContext, _queue: &Queue) -> Result<()> {\n        Ok(())\n    }\n\n    #[allow(unused_variables)]\n    fn register_tasks(tasks: &mut Tasks) {\n        // tasks-inject (do not remove) \n    }\n}\n"
  },
  {
    "path": "loco-new/tests/templates/snapshots/r#mod__templates__auth__src_app_rs_auth_false_Sqlite.snap",
    "content": "---\nsource: tests/templates/auth.rs\nexpression: \"std::fs::read_to_string(generator.path(\\\"src/app.rs\\\")).expect(\\\"could not open file\\\")\"\n---\nuse std::path::Path;\nuse async_trait::async_trait;\nuse loco_rs::{\n    app::{AppContext, Hooks, Initializer},\n    bgworker::{\n        Queue},\n    boot::{create_app, BootResult, StartMode},\n    config::Config,\n    controller::AppRoutes,\n    environment::Environment,\n    task::Tasks,\n    Result,\n};\nuse migration::Migrator;\n\n#[allow(unused_imports)]\nuse crate::{\n    controllers ,tasks\n};\n\npub struct App;\n#[async_trait]\nimpl Hooks for App {\n    fn app_name() -> &'static str {\n        env!(\"CARGO_CRATE_NAME\")\n    }\n\n    fn app_version() -> String {\n        format!(\n            \"{} ({})\",\n            env!(\"CARGO_PKG_VERSION\"),\n            option_env!(\"BUILD_SHA\")\n                .or(option_env!(\"GITHUB_SHA\"))\n                .unwrap_or(\"dev\")\n        )\n    }\n\n    async fn boot(mode: StartMode, environment: &Environment, config: Config) -> Result<BootResult> {\n        create_app::<Self, Migrator>(mode, environment, config).await\n        \n    }\n\n    async fn initializers(_ctx: &AppContext) -> Result<Vec<Box<dyn Initializer>>> {\n        Ok(vec![])\n    }\n\n    fn routes(_ctx: &AppContext) -> AppRoutes {\n        AppRoutes::with_default_routes() // controller routes below\n            .add_route(controllers::home::routes())\n    }\n    async fn connect_workers(_ctx: &AppContext, _queue: &Queue) -> Result<()> {\n        Ok(())\n    }\n\n    #[allow(unused_variables)]\n    fn register_tasks(tasks: &mut Tasks) {\n        // tasks-inject (do not remove) \n    }\n    async fn truncate(_ctx: &AppContext) -> Result<()> {\n        Ok(())\n    } \n    async fn seed(_ctx: &AppContext, _base: &Path) -> Result<()> {\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "loco-new/tests/templates/snapshots/r#mod__templates__auth__src_app_rs_auth_true_None.snap",
    "content": "---\nsource: tests/templates/auth.rs\nexpression: \"std::fs::read_to_string(generator.path(\\\"src/app.rs\\\")).expect(\\\"could not open file\\\")\"\n---\nuse async_trait::async_trait;\nuse loco_rs::{\n    app::{AppContext, Hooks, Initializer},\n    bgworker::{\n        Queue},\n    boot::{create_app, BootResult, StartMode},\n    config::Config,\n    controller::AppRoutes,\n    db::{self, truncate_table},\n    environment::Environment,\n    task::Tasks,\n    Result,\n};\n\n#[allow(unused_imports)]\nuse crate::{\n    controllers ,tasks\n    , models::_entities::users\n};\n\npub struct App;\n#[async_trait]\nimpl Hooks for App {\n    fn app_name() -> &'static str {\n        env!(\"CARGO_CRATE_NAME\")\n    }\n\n    fn app_version() -> String {\n        format!(\n            \"{} ({})\",\n            env!(\"CARGO_PKG_VERSION\"),\n            option_env!(\"BUILD_SHA\")\n                .or(option_env!(\"GITHUB_SHA\"))\n                .unwrap_or(\"dev\")\n        )\n    }\n\n    async fn boot(mode: StartMode, environment: &Environment, config: Config) -> Result<BootResult> {\n        create_app::<Self>(mode, environment, config).await\n    }\n\n    async fn initializers(_ctx: &AppContext) -> Result<Vec<Box<dyn Initializer>>> {\n        Ok(vec![])\n    }\n\n    fn routes(_ctx: &AppContext) -> AppRoutes {\n        AppRoutes::with_default_routes() // controller routes below\n            .add_route(controllers::auth::routes())\n    }\n    async fn connect_workers(_ctx: &AppContext, _queue: &Queue) -> Result<()> {\n        Ok(())\n    }\n\n    #[allow(unused_variables)]\n    fn register_tasks(tasks: &mut Tasks) {\n        // tasks-inject (do not remove)\n        tasks.register(tasks::user_create::UserCreate); \n    }\n}\n"
  },
  {
    "path": "loco-new/tests/templates/snapshots/r#mod__templates__auth__src_app_rs_auth_true_Sqlite.snap",
    "content": "---\nsource: tests/templates/auth.rs\nexpression: \"std::fs::read_to_string(generator.path(\\\"src/app.rs\\\")).expect(\\\"could not open file\\\")\"\n---\nuse std::path::Path;\nuse async_trait::async_trait;\nuse loco_rs::{\n    app::{AppContext, Hooks, Initializer},\n    bgworker::{\n        Queue},\n    boot::{create_app, BootResult, StartMode},\n    config::Config,\n    controller::AppRoutes,\n    db::{self, truncate_table},\n    environment::Environment,\n    task::Tasks,\n    Result,\n};\nuse migration::Migrator;\n\n#[allow(unused_imports)]\nuse crate::{\n    controllers ,tasks\n    , models::_entities::users\n};\n\npub struct App;\n#[async_trait]\nimpl Hooks for App {\n    fn app_name() -> &'static str {\n        env!(\"CARGO_CRATE_NAME\")\n    }\n\n    fn app_version() -> String {\n        format!(\n            \"{} ({})\",\n            env!(\"CARGO_PKG_VERSION\"),\n            option_env!(\"BUILD_SHA\")\n                .or(option_env!(\"GITHUB_SHA\"))\n                .unwrap_or(\"dev\")\n        )\n    }\n\n    async fn boot(mode: StartMode, environment: &Environment, config: Config) -> Result<BootResult> {\n        create_app::<Self, Migrator>(mode, environment, config).await\n        \n    }\n\n    async fn initializers(_ctx: &AppContext) -> Result<Vec<Box<dyn Initializer>>> {\n        Ok(vec![])\n    }\n\n    fn routes(_ctx: &AppContext) -> AppRoutes {\n        AppRoutes::with_default_routes() // controller routes below\n            .add_route(controllers::auth::routes())\n    }\n    async fn connect_workers(_ctx: &AppContext, _queue: &Queue) -> Result<()> {\n        Ok(())\n    }\n\n    #[allow(unused_variables)]\n    fn register_tasks(tasks: &mut Tasks) {\n        // tasks-inject (do not remove)\n        tasks.register(tasks::user_create::UserCreate); \n    }\n    async fn truncate(ctx: &AppContext) -> Result<()> {\n        truncate_table(&ctx.db, users::Entity).await?;\n        Ok(())\n    }\n    async fn seed(ctx: &AppContext, base: &Path) -> Result<()> {\n        db::seed::<users::ActiveModel>(&ctx.db, &base.join(\"users.yaml\").display().to_string()).await?;\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "loco-new/tests/templates/snapshots/r#mod__templates__background__src_app_rs_Async.snap",
    "content": "---\nsource: tests/templates/background.rs\nexpression: \"std::fs::read_to_string(generator.path(\\\"src/app.rs\\\")).expect(\\\"could not open file\\\")\"\n---\nuse async_trait::async_trait;\nuse loco_rs::{\n    app::{AppContext, Hooks, Initializer},\n    bgworker::{\n        BackgroundWorker,\n        Queue},\n    boot::{create_app, BootResult, StartMode},\n    config::Config,\n    controller::AppRoutes,\n    environment::Environment,\n    task::Tasks,\n    Result,\n};\n\n#[allow(unused_imports)]\nuse crate::{\n    controllers ,tasks\n    , workers::downloader::DownloadWorker\n};\n\npub struct App;\n#[async_trait]\nimpl Hooks for App {\n    fn app_name() -> &'static str {\n        env!(\"CARGO_CRATE_NAME\")\n    }\n\n    fn app_version() -> String {\n        format!(\n            \"{} ({})\",\n            env!(\"CARGO_PKG_VERSION\"),\n            option_env!(\"BUILD_SHA\")\n                .or(option_env!(\"GITHUB_SHA\"))\n                .unwrap_or(\"dev\")\n        )\n    }\n\n    async fn boot(mode: StartMode, environment: &Environment, config: Config) -> Result<BootResult> {\n        create_app::<Self>(mode, environment, config).await\n    }\n\n    async fn initializers(_ctx: &AppContext) -> Result<Vec<Box<dyn Initializer>>> {\n        Ok(vec![])\n    }\n\n    fn routes(_ctx: &AppContext) -> AppRoutes {\n        AppRoutes::with_default_routes() // controller routes below\n            .add_route(controllers::home::routes())\n    }\n    async fn connect_workers(ctx: &AppContext, queue: &Queue) -> Result<()> {\n        queue.register(DownloadWorker::build(ctx)).await?;\n        Ok(())\n    }\n\n    #[allow(unused_variables)]\n    fn register_tasks(tasks: &mut Tasks) {\n        // tasks-inject (do not remove) \n    }\n}\n"
  },
  {
    "path": "loco-new/tests/templates/snapshots/r#mod__templates__background__src_app_rs_Blocking.snap",
    "content": "---\nsource: tests/templates/background.rs\nexpression: \"std::fs::read_to_string(generator.path(\\\"src/app.rs\\\")).expect(\\\"could not open file\\\")\"\n---\nuse async_trait::async_trait;\nuse loco_rs::{\n    app::{AppContext, Hooks, Initializer},\n    bgworker::{\n        BackgroundWorker,\n        Queue},\n    boot::{create_app, BootResult, StartMode},\n    config::Config,\n    controller::AppRoutes,\n    environment::Environment,\n    task::Tasks,\n    Result,\n};\n\n#[allow(unused_imports)]\nuse crate::{\n    controllers ,tasks\n    , workers::downloader::DownloadWorker\n};\n\npub struct App;\n#[async_trait]\nimpl Hooks for App {\n    fn app_name() -> &'static str {\n        env!(\"CARGO_CRATE_NAME\")\n    }\n\n    fn app_version() -> String {\n        format!(\n            \"{} ({})\",\n            env!(\"CARGO_PKG_VERSION\"),\n            option_env!(\"BUILD_SHA\")\n                .or(option_env!(\"GITHUB_SHA\"))\n                .unwrap_or(\"dev\")\n        )\n    }\n\n    async fn boot(mode: StartMode, environment: &Environment, config: Config) -> Result<BootResult> {\n        create_app::<Self>(mode, environment, config).await\n    }\n\n    async fn initializers(_ctx: &AppContext) -> Result<Vec<Box<dyn Initializer>>> {\n        Ok(vec![])\n    }\n\n    fn routes(_ctx: &AppContext) -> AppRoutes {\n        AppRoutes::with_default_routes() // controller routes below\n            .add_route(controllers::home::routes())\n    }\n    async fn connect_workers(ctx: &AppContext, queue: &Queue) -> Result<()> {\n        queue.register(DownloadWorker::build(ctx)).await?;\n        Ok(())\n    }\n\n    #[allow(unused_variables)]\n    fn register_tasks(tasks: &mut Tasks) {\n        // tasks-inject (do not remove) \n    }\n}\n"
  },
  {
    "path": "loco-new/tests/templates/snapshots/r#mod__templates__background__src_app_rs_None.snap",
    "content": "---\nsource: tests/templates/background.rs\nexpression: \"std::fs::read_to_string(generator.path(\\\"src/app.rs\\\")).expect(\\\"could not open file\\\")\"\nsnapshot_kind: text\n---\nuse async_trait::async_trait;\nuse loco_rs::{\n    app::{AppContext, Hooks, Initializer},\n    bgworker::{\n        Queue},\n    boot::{create_app, BootResult, StartMode},\n    config::Config,\n    controller::AppRoutes,\n    environment::Environment,\n    task::Tasks,\n    Result,\n};\n\n#[allow(unused_imports)]\nuse crate::{\n    controllers ,tasks\n};\n\npub struct App;\n#[async_trait]\nimpl Hooks for App {\n    fn app_name() -> &'static str {\n        env!(\"CARGO_CRATE_NAME\")\n    }\n\n    fn app_version() -> String {\n        format!(\n            \"{} ({})\",\n            env!(\"CARGO_PKG_VERSION\"),\n            option_env!(\"BUILD_SHA\")\n                .or(option_env!(\"GITHUB_SHA\"))\n                .unwrap_or(\"dev\")\n        )\n    }\n\n    async fn boot(mode: StartMode, environment: &Environment, config: Config) -> Result<BootResult> {\n        create_app::<Self>(mode, environment, config).await\n    }\n\n    async fn initializers(_ctx: &AppContext) -> Result<Vec<Box<dyn Initializer>>> {\n        Ok(vec![])\n    }\n\n    fn routes(_ctx: &AppContext) -> AppRoutes {\n        AppRoutes::with_default_routes() // controller routes below\n            .add_route(controllers::home::routes())\n    }\n    async fn connect_workers(_ctx: &AppContext, _queue: &Queue) -> Result<()> {\n        Ok(())\n    }\n\n    #[allow(unused_variables)]\n    fn register_tasks(tasks: &mut Tasks) {\n        // tasks-inject (do not remove) \n    }\n}\n"
  },
  {
    "path": "loco-new/tests/templates/snapshots/r#mod__templates__background__src_app_rs_Queue.snap",
    "content": "---\nsource: tests/templates/background.rs\nexpression: \"std::fs::read_to_string(generator.path(\\\"src/app.rs\\\")).expect(\\\"could not open file\\\")\"\n---\nuse async_trait::async_trait;\nuse loco_rs::{\n    app::{AppContext, Hooks, Initializer},\n    bgworker::{\n        BackgroundWorker,\n        Queue},\n    boot::{create_app, BootResult, StartMode},\n    config::Config,\n    controller::AppRoutes,\n    environment::Environment,\n    task::Tasks,\n    Result,\n};\n\n#[allow(unused_imports)]\nuse crate::{\n    controllers ,tasks\n    , workers::downloader::DownloadWorker\n};\n\npub struct App;\n#[async_trait]\nimpl Hooks for App {\n    fn app_name() -> &'static str {\n        env!(\"CARGO_CRATE_NAME\")\n    }\n\n    fn app_version() -> String {\n        format!(\n            \"{} ({})\",\n            env!(\"CARGO_PKG_VERSION\"),\n            option_env!(\"BUILD_SHA\")\n                .or(option_env!(\"GITHUB_SHA\"))\n                .unwrap_or(\"dev\")\n        )\n    }\n\n    async fn boot(mode: StartMode, environment: &Environment, config: Config) -> Result<BootResult> {\n        create_app::<Self>(mode, environment, config).await\n    }\n\n    async fn initializers(_ctx: &AppContext) -> Result<Vec<Box<dyn Initializer>>> {\n        Ok(vec![])\n    }\n\n    fn routes(_ctx: &AppContext) -> AppRoutes {\n        AppRoutes::with_default_routes() // controller routes below\n            .add_route(controllers::home::routes())\n    }\n    async fn connect_workers(ctx: &AppContext, queue: &Queue) -> Result<()> {\n        queue.register(DownloadWorker::build(ctx)).await?;\n        Ok(())\n    }\n\n    #[allow(unused_variables)]\n    fn register_tasks(tasks: &mut Tasks) {\n        // tasks-inject (do not remove) \n    }\n}\n"
  },
  {
    "path": "loco-new/tests/templates/snapshots/r#mod__templates__db__cargo_dependencies_None.snap",
    "content": "---\nsource: tests/templates/db.rs\nexpression: \"content.get(\\\"dependencies\\\").unwrap()\"\n---\n{ async-trait = { version = \"0.1\" }, axum = { version = \"0.8\" }, loco-rs = { workspace = true }, regex = { version = \"1.11\" }, serde = { features = [\"derive\"], version = \"1\" }, serde_json = { version = \"1\" }, tokio = { default-features = false, features = [\"rt-multi-thread\"], version = \"1.45\" }, tracing = { version = \"0.1\" }, tracing-subscriber = { features = [\"env-filter\", \"json\"], version = \"0.3\" } }\n"
  },
  {
    "path": "loco-new/tests/templates/snapshots/r#mod__templates__db__cargo_dependencies_Postgres.snap",
    "content": "---\nsource: tests/templates/db.rs\nexpression: \"content.get(\\\"dependencies\\\").unwrap()\"\n---\n{ async-trait = { version = \"0.1\" }, axum = { version = \"0.8\" }, chrono = { version = \"0.4\" }, loco-rs = { workspace = true }, migration = { path = \"migration\" }, regex = { version = \"1.11\" }, sea-orm = { features = [\"sqlx-sqlite\", \"sqlx-postgres\", \"runtime-tokio-rustls\", \"macros\"], version = \"1.1\" }, serde = { features = [\"derive\"], version = \"1\" }, serde_json = { version = \"1\" }, tokio = { default-features = false, features = [\"rt-multi-thread\"], version = \"1.45\" }, tracing = { version = \"0.1\" }, tracing-subscriber = { features = [\"env-filter\", \"json\"], version = \"0.3\" }, uuid = { features = [\"v4\"], version = \"1.6\" }, validator = { version = \"0.20\" } }\n"
  },
  {
    "path": "loco-new/tests/templates/snapshots/r#mod__templates__db__cargo_dependencies_Sqlite.snap",
    "content": "---\nsource: tests/templates/db.rs\nexpression: \"content.get(\\\"dependencies\\\").unwrap()\"\n---\n{ async-trait = { version = \"0.1\" }, axum = { version = \"0.8\" }, chrono = { version = \"0.4\" }, loco-rs = { workspace = true }, migration = { path = \"migration\" }, regex = { version = \"1.11\" }, sea-orm = { features = [\"sqlx-sqlite\", \"sqlx-postgres\", \"runtime-tokio-rustls\", \"macros\"], version = \"1.1\" }, serde = { features = [\"derive\"], version = \"1\" }, serde_json = { version = \"1\" }, tokio = { default-features = false, features = [\"rt-multi-thread\"], version = \"1.45\" }, tracing = { version = \"0.1\" }, tracing-subscriber = { features = [\"env-filter\", \"json\"], version = \"0.3\" }, uuid = { features = [\"v4\"], version = \"1.6\" }, validator = { version = \"0.20\" } }\n"
  },
  {
    "path": "loco-new/tests/templates/snapshots/r#mod__templates__db__config_development_yaml_config_database_Postgres.snap",
    "content": "---\nsource: tests/templates/db.rs\nexpression: \"format!(\\\"{:#?}\\\",\\nassertion::yaml::get_value_at_path(&content, &[\\\"database\\\"]).unwrap())\"\n---\nMapping {\n    \"uri\": Mapping {\n        Mapping {\n            \"get_env(name=\\\"DATABASE_URL\\\"\": Null,\n            \"default=\\\"postgres://loco:loco@localhost:5432/loco-app-test_development\\\")\": Null,\n        }: Null,\n    },\n    \"enable_logging\": Bool(false),\n    \"connect_timeout\": Mapping {\n        Mapping {\n            \"get_env(name=\\\"DB_CONNECT_TIMEOUT\\\"\": Null,\n            \"default=\\\"500\\\")\": Null,\n        }: Null,\n    },\n    \"idle_timeout\": Mapping {\n        Mapping {\n            \"get_env(name=\\\"DB_IDLE_TIMEOUT\\\"\": Null,\n            \"default=\\\"500\\\")\": Null,\n        }: Null,\n    },\n    \"min_connections\": Mapping {\n        Mapping {\n            \"get_env(name=\\\"DB_MIN_CONNECTIONS\\\"\": Null,\n            \"default=\\\"1\\\")\": Null,\n        }: Null,\n    },\n    \"max_connections\": Mapping {\n        Mapping {\n            \"get_env(name=\\\"DB_MAX_CONNECTIONS\\\"\": Null,\n            \"default=\\\"1\\\")\": Null,\n        }: Null,\n    },\n    \"auto_migrate\": Bool(true),\n    \"dangerously_truncate\": Bool(false),\n    \"dangerously_recreate\": Bool(false),\n}\n"
  },
  {
    "path": "loco-new/tests/templates/snapshots/r#mod__templates__db__config_development_yaml_config_database_Sqlite.snap",
    "content": "---\nsource: tests/templates/db.rs\nexpression: \"format!(\\\"{:#?}\\\",\\nassertion::yaml::get_value_at_path(&content, &[\\\"database\\\"]).unwrap())\"\n---\nMapping {\n    \"uri\": Mapping {\n        Mapping {\n            \"get_env(name=\\\"DATABASE_URL\\\"\": Null,\n            \"default=\\\"sqlite://loco-app-test_development.sqlite?mode=rwc\\\")\": Null,\n        }: Null,\n    },\n    \"enable_logging\": Bool(false),\n    \"connect_timeout\": Mapping {\n        Mapping {\n            \"get_env(name=\\\"DB_CONNECT_TIMEOUT\\\"\": Null,\n            \"default=\\\"500\\\")\": Null,\n        }: Null,\n    },\n    \"idle_timeout\": Mapping {\n        Mapping {\n            \"get_env(name=\\\"DB_IDLE_TIMEOUT\\\"\": Null,\n            \"default=\\\"500\\\")\": Null,\n        }: Null,\n    },\n    \"min_connections\": Mapping {\n        Mapping {\n            \"get_env(name=\\\"DB_MIN_CONNECTIONS\\\"\": Null,\n            \"default=\\\"1\\\")\": Null,\n        }: Null,\n    },\n    \"max_connections\": Mapping {\n        Mapping {\n            \"get_env(name=\\\"DB_MAX_CONNECTIONS\\\"\": Null,\n            \"default=\\\"1\\\")\": Null,\n        }: Null,\n    },\n    \"auto_migrate\": Bool(true),\n    \"dangerously_truncate\": Bool(false),\n    \"dangerously_recreate\": Bool(false),\n}\n"
  },
  {
    "path": "loco-new/tests/templates/snapshots/r#mod__templates__db__config_test_yaml_config_database_Postgres.snap",
    "content": "---\nsource: tests/templates/db.rs\nexpression: \"format!(\\\"{:#?}\\\",\\nassertion::yaml::get_value_at_path(&content, &[\\\"database\\\"]).unwrap())\"\n---\nMapping {\n    \"uri\": Mapping {\n        Mapping {\n            \"get_env(name=\\\"DATABASE_URL\\\"\": Null,\n            \"default=\\\"postgres://loco:loco@localhost:5432/loco-app-test_test\\\")\": Null,\n        }: Null,\n    },\n    \"enable_logging\": Bool(false),\n    \"connect_timeout\": Mapping {\n        Mapping {\n            \"get_env(name=\\\"DB_CONNECT_TIMEOUT\\\"\": Null,\n            \"default=\\\"500\\\")\": Null,\n        }: Null,\n    },\n    \"idle_timeout\": Mapping {\n        Mapping {\n            \"get_env(name=\\\"DB_IDLE_TIMEOUT\\\"\": Null,\n            \"default=\\\"500\\\")\": Null,\n        }: Null,\n    },\n    \"min_connections\": Mapping {\n        Mapping {\n            \"get_env(name=\\\"DB_MIN_CONNECTIONS\\\"\": Null,\n            \"default=\\\"1\\\")\": Null,\n        }: Null,\n    },\n    \"max_connections\": Mapping {\n        Mapping {\n            \"get_env(name=\\\"DB_MAX_CONNECTIONS\\\"\": Null,\n            \"default=\\\"1\\\")\": Null,\n        }: Null,\n    },\n    \"auto_migrate\": Bool(true),\n    \"dangerously_truncate\": Bool(true),\n    \"dangerously_recreate\": Bool(true),\n}\n"
  },
  {
    "path": "loco-new/tests/templates/snapshots/r#mod__templates__db__config_test_yaml_config_database_Sqlite.snap",
    "content": "---\nsource: tests/templates/db.rs\nexpression: \"format!(\\\"{:#?}\\\",\\nassertion::yaml::get_value_at_path(&content, &[\\\"database\\\"]).unwrap())\"\n---\nMapping {\n    \"uri\": Mapping {\n        Mapping {\n            \"get_env(name=\\\"DATABASE_URL\\\"\": Null,\n            \"default=\\\"sqlite://loco-app-test_test.sqlite?mode=rwc\\\")\": Null,\n        }: Null,\n    },\n    \"enable_logging\": Bool(false),\n    \"connect_timeout\": Mapping {\n        Mapping {\n            \"get_env(name=\\\"DB_CONNECT_TIMEOUT\\\"\": Null,\n            \"default=\\\"500\\\")\": Null,\n        }: Null,\n    },\n    \"idle_timeout\": Mapping {\n        Mapping {\n            \"get_env(name=\\\"DB_IDLE_TIMEOUT\\\"\": Null,\n            \"default=\\\"500\\\")\": Null,\n        }: Null,\n    },\n    \"min_connections\": Mapping {\n        Mapping {\n            \"get_env(name=\\\"DB_MIN_CONNECTIONS\\\"\": Null,\n            \"default=\\\"1\\\")\": Null,\n        }: Null,\n    },\n    \"max_connections\": Mapping {\n        Mapping {\n            \"get_env(name=\\\"DB_MAX_CONNECTIONS\\\"\": Null,\n            \"default=\\\"1\\\")\": Null,\n        }: Null,\n    },\n    \"auto_migrate\": Bool(true),\n    \"dangerously_truncate\": Bool(true),\n    \"dangerously_recreate\": Bool(true),\n}\n"
  },
  {
    "path": "loco-new/tests/templates/snapshots/r#mod__templates__db__src_app_rs_None.snap",
    "content": "---\nsource: tests/templates/db.rs\nexpression: \"std::fs::read_to_string(generator.path(\\\"src/app.rs\\\")).expect(\\\"could not open file\\\")\"\n---\nuse async_trait::async_trait;\nuse loco_rs::{\n    app::{AppContext, Hooks, Initializer},\n    bgworker::{\n        Queue},\n    boot::{create_app, BootResult, StartMode},\n    config::Config,\n    controller::AppRoutes,\n    environment::Environment,\n    task::Tasks,\n    Result,\n};\n\n#[allow(unused_imports)]\nuse crate::{\n    controllers ,tasks\n};\n\npub struct App;\n#[async_trait]\nimpl Hooks for App {\n    fn app_name() -> &'static str {\n        env!(\"CARGO_CRATE_NAME\")\n    }\n\n    fn app_version() -> String {\n        format!(\n            \"{} ({})\",\n            env!(\"CARGO_PKG_VERSION\"),\n            option_env!(\"BUILD_SHA\")\n                .or(option_env!(\"GITHUB_SHA\"))\n                .unwrap_or(\"dev\")\n        )\n    }\n\n    async fn boot(mode: StartMode, environment: &Environment, config: Config) -> Result<BootResult> {\n        create_app::<Self>(mode, environment, config).await\n    }\n\n    async fn initializers(_ctx: &AppContext) -> Result<Vec<Box<dyn Initializer>>> {\n        Ok(vec![])\n    }\n\n    fn routes(_ctx: &AppContext) -> AppRoutes {\n        AppRoutes::with_default_routes() // controller routes below\n            .add_route(controllers::home::routes())\n    }\n    async fn connect_workers(_ctx: &AppContext, _queue: &Queue) -> Result<()> {\n        Ok(())\n    }\n\n    #[allow(unused_variables)]\n    fn register_tasks(tasks: &mut Tasks) {\n        // tasks-inject (do not remove) \n    }\n}\n"
  },
  {
    "path": "loco-new/tests/templates/snapshots/r#mod__templates__db__src_app_rs_Postgres.snap",
    "content": "---\nsource: tests/templates/db.rs\nexpression: \"std::fs::read_to_string(generator.path(\\\"src/app.rs\\\")).expect(\\\"could not open file\\\")\"\n---\nuse std::path::Path;\nuse async_trait::async_trait;\nuse loco_rs::{\n    app::{AppContext, Hooks, Initializer},\n    bgworker::{\n        Queue},\n    boot::{create_app, BootResult, StartMode},\n    config::Config,\n    controller::AppRoutes,\n    environment::Environment,\n    task::Tasks,\n    Result,\n};\nuse migration::Migrator;\n\n#[allow(unused_imports)]\nuse crate::{\n    controllers ,tasks\n};\n\npub struct App;\n#[async_trait]\nimpl Hooks for App {\n    fn app_name() -> &'static str {\n        env!(\"CARGO_CRATE_NAME\")\n    }\n\n    fn app_version() -> String {\n        format!(\n            \"{} ({})\",\n            env!(\"CARGO_PKG_VERSION\"),\n            option_env!(\"BUILD_SHA\")\n                .or(option_env!(\"GITHUB_SHA\"))\n                .unwrap_or(\"dev\")\n        )\n    }\n\n    async fn boot(mode: StartMode, environment: &Environment, config: Config) -> Result<BootResult> {\n        create_app::<Self, Migrator>(mode, environment, config).await\n        \n    }\n\n    async fn initializers(_ctx: &AppContext) -> Result<Vec<Box<dyn Initializer>>> {\n        Ok(vec![])\n    }\n\n    fn routes(_ctx: &AppContext) -> AppRoutes {\n        AppRoutes::with_default_routes() // controller routes below\n            .add_route(controllers::home::routes())\n    }\n    async fn connect_workers(_ctx: &AppContext, _queue: &Queue) -> Result<()> {\n        Ok(())\n    }\n\n    #[allow(unused_variables)]\n    fn register_tasks(tasks: &mut Tasks) {\n        // tasks-inject (do not remove) \n    }\n    async fn truncate(_ctx: &AppContext) -> Result<()> {\n        Ok(())\n    } \n    async fn seed(_ctx: &AppContext, _base: &Path) -> Result<()> {\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "loco-new/tests/templates/snapshots/r#mod__templates__db__src_app_rs_Sqlite.snap",
    "content": "---\nsource: tests/templates/db.rs\nexpression: \"std::fs::read_to_string(generator.path(\\\"src/app.rs\\\")).expect(\\\"could not open file\\\")\"\n---\nuse std::path::Path;\nuse async_trait::async_trait;\nuse loco_rs::{\n    app::{AppContext, Hooks, Initializer},\n    bgworker::{\n        Queue},\n    boot::{create_app, BootResult, StartMode},\n    config::Config,\n    controller::AppRoutes,\n    environment::Environment,\n    task::Tasks,\n    Result,\n};\nuse migration::Migrator;\n\n#[allow(unused_imports)]\nuse crate::{\n    controllers ,tasks\n};\n\npub struct App;\n#[async_trait]\nimpl Hooks for App {\n    fn app_name() -> &'static str {\n        env!(\"CARGO_CRATE_NAME\")\n    }\n\n    fn app_version() -> String {\n        format!(\n            \"{} ({})\",\n            env!(\"CARGO_PKG_VERSION\"),\n            option_env!(\"BUILD_SHA\")\n                .or(option_env!(\"GITHUB_SHA\"))\n                .unwrap_or(\"dev\")\n        )\n    }\n\n    async fn boot(mode: StartMode, environment: &Environment, config: Config) -> Result<BootResult> {\n        create_app::<Self, Migrator>(mode, environment, config).await\n        \n    }\n\n    async fn initializers(_ctx: &AppContext) -> Result<Vec<Box<dyn Initializer>>> {\n        Ok(vec![])\n    }\n\n    fn routes(_ctx: &AppContext) -> AppRoutes {\n        AppRoutes::with_default_routes() // controller routes below\n            .add_route(controllers::home::routes())\n    }\n    async fn connect_workers(_ctx: &AppContext, _queue: &Queue) -> Result<()> {\n        Ok(())\n    }\n\n    #[allow(unused_variables)]\n    fn register_tasks(tasks: &mut Tasks) {\n        // tasks-inject (do not remove) \n    }\n    async fn truncate(_ctx: &AppContext) -> Result<()> {\n        Ok(())\n    } \n    async fn seed(_ctx: &AppContext, _base: &Path) -> Result<()> {\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "loco-new/tests/templates/snapshots/r#mod__templates__initializers__src_app_rs_with_initializers.snap",
    "content": "---\nsource: tests/templates/initializers.rs\nexpression: \"std::fs::read_to_string(generator.path(\\\"src/app.rs\\\")).expect(\\\"could not open file\\\")\"\n---\nuse async_trait::async_trait;\nuse loco_rs::{\n    app::{AppContext, Hooks, Initializer},\n    bgworker::{\n        Queue},\n    boot::{create_app, BootResult, StartMode},\n    config::Config,\n    controller::AppRoutes,\n    environment::Environment,\n    task::Tasks,\n    Result,\n};\n\n#[allow(unused_imports)]\nuse crate::{\n    controllers ,tasks\n};\n\npub struct App;\n#[async_trait]\nimpl Hooks for App {\n    fn app_name() -> &'static str {\n        env!(\"CARGO_CRATE_NAME\")\n    }\n\n    fn app_version() -> String {\n        format!(\n            \"{} ({})\",\n            env!(\"CARGO_PKG_VERSION\"),\n            option_env!(\"BUILD_SHA\")\n                .or(option_env!(\"GITHUB_SHA\"))\n                .unwrap_or(\"dev\")\n        )\n    }\n\n    async fn boot(mode: StartMode, environment: &Environment, config: Config) -> Result<BootResult> {\n        create_app::<Self>(mode, environment, config).await\n    }\n\n    async fn initializers(_ctx: &AppContext) -> Result<Vec<Box<dyn Initializer>>> {\n        Ok(vec![])\n    }\n\n    fn routes(_ctx: &AppContext) -> AppRoutes {\n        AppRoutes::with_default_routes() // controller routes below\n            .add_route(controllers::home::routes())\n    }\n    async fn connect_workers(_ctx: &AppContext, _queue: &Queue) -> Result<()> {\n        Ok(())\n    }\n\n    #[allow(unused_variables)]\n    fn register_tasks(tasks: &mut Tasks) {\n        // tasks-inject (do not remove) \n    }\n}\n"
  },
  {
    "path": "loco-new/tests/templates/snapshots/r#mod__templates__initializers__src_app_rs_without_initializers.snap",
    "content": "---\nsource: tests/templates/initializers.rs\nexpression: \"std::fs::read_to_string(generator.path(\\\"src/app.rs\\\")).expect(\\\"could not open file\\\")\"\n---\nuse async_trait::async_trait;\nuse loco_rs::{\n    app::{AppContext, Hooks, Initializer},\n    bgworker::{\n        Queue},\n    boot::{create_app, BootResult, StartMode},\n    config::Config,\n    controller::AppRoutes,\n    environment::Environment,\n    task::Tasks,\n    Result,\n};\n\n#[allow(unused_imports)]\nuse crate::{\n    controllers ,tasks, initializers\n};\n\npub struct App;\n#[async_trait]\nimpl Hooks for App {\n    fn app_name() -> &'static str {\n        env!(\"CARGO_CRATE_NAME\")\n    }\n\n    fn app_version() -> String {\n        format!(\n            \"{} ({})\",\n            env!(\"CARGO_PKG_VERSION\"),\n            option_env!(\"BUILD_SHA\")\n                .or(option_env!(\"GITHUB_SHA\"))\n                .unwrap_or(\"dev\")\n        )\n    }\n\n    async fn boot(mode: StartMode, environment: &Environment, config: Config) -> Result<BootResult> {\n        create_app::<Self>(mode, environment, config).await\n    }\n\n    async fn initializers(_ctx: &AppContext) -> Result<Vec<Box<dyn Initializer>>> {\n        Ok(vec![Box::new(initializers::view_engine::ViewEngineInitializer)])\n    }\n\n    fn routes(_ctx: &AppContext) -> AppRoutes {\n        AppRoutes::with_default_routes() // controller routes below\n            .add_route(controllers::home::routes())\n    }\n    async fn connect_workers(_ctx: &AppContext, _queue: &Queue) -> Result<()> {\n        Ok(())\n    }\n\n    #[allow(unused_variables)]\n    fn register_tasks(tasks: &mut Tasks) {\n        // tasks-inject (do not remove) \n    }\n}\n"
  },
  {
    "path": "loco-new/tests/templates/snapshots/r#mod__templates__mailer__cargo_dependencies_mailer_false.snap",
    "content": "---\nsource: tests/templates/mailer.rs\nexpression: \"content.get(\\\"dependencies\\\").unwrap()\"\n---\n{ async-trait = { version = \"0.1\" }, axum = { version = \"0.8\" }, loco-rs = { workspace = true }, regex = { version = \"1.11\" }, serde = { features = [\"derive\"], version = \"1\" }, serde_json = { version = \"1\" }, tokio = { default-features = false, features = [\"rt-multi-thread\"], version = \"1.45\" }, tracing = { version = \"0.1\" }, tracing-subscriber = { features = [\"env-filter\", \"json\"], version = \"0.3\" } }\n"
  },
  {
    "path": "loco-new/tests/templates/snapshots/r#mod__templates__mailer__cargo_dependencies_mailer_true.snap",
    "content": "---\nsource: tests/templates/mailer.rs\nexpression: \"content.get(\\\"dependencies\\\").unwrap()\"\n---\n{ async-trait = { version = \"0.1\" }, axum = { version = \"0.8\" }, include_dir = { version = \"0.7\" }, loco-rs = { workspace = true }, regex = { version = \"1.11\" }, serde = { features = [\"derive\"], version = \"1\" }, serde_json = { version = \"1\" }, tokio = { default-features = false, features = [\"rt-multi-thread\"], version = \"1.45\" }, tracing = { version = \"0.1\" }, tracing-subscriber = { features = [\"env-filter\", \"json\"], version = \"0.3\" } }\n"
  },
  {
    "path": "loco-new/tests/wizard/mod.rs",
    "content": "mod new;\n"
  },
  {
    "path": "loco-new/tests/wizard/new.rs",
    "content": "use std::{collections::HashMap, path::PathBuf, process::Output, sync::Arc};\n\nuse duct::cmd;\nuse loco::{\n    generator::{executer::FileSystem, Generator},\n    settings,\n    wizard::{self, AssetsOption, BackgroundOption, DBOption},\n    OS,\n};\n\n// #[cfg(feature = \"test-wizard\")]\n// #[rstest::rstest]\n// fn test_all_combinations(\n//     #[values(DBOption::None, DBOption::Sqlite)] db: DBOption,\n//     #[values(\n//         BackgroundOption::Async,\n//         BackgroundOption::Queue,\n//         BackgroundOption::Blocking,\n//         BackgroundOption::None\n//     )]\n//     background: BackgroundOption,\n//     #[values(AssetsOption::Serverside, AssetsOption::Clientside,\n// AssetsOption::None)]     asset: AssetsOption,\n// ) {\n//     test_combination(db, background, asset, true);\n// }\n\n// when running locally set LOCO_DEV_MODE_PATH=<to local loco path>\n#[rstest::rstest]\n// lightweight service\n#[case(DBOption::None, AssetsOption::None)]\n// REST API\n#[case(DBOption::Sqlite, AssetsOption::None)]\n// SaaS, serverside\n#[case(DBOption::None, AssetsOption::Serverside)]\n// SaaS, clientside\n#[case(DBOption::None, AssetsOption::Clientside)]\n// test only DB\n#[case(DBOption::Sqlite, AssetsOption::None)]\nfn test_starter_combinations(#[case] db: DBOption, #[case] asset: AssetsOption) {\n    test_combination(db, asset, true);\n}\n\nfn test_combination(db: DBOption, asset: AssetsOption, test_generator: bool) {\n    let test_dir = tree_fs::TreeBuilder::default().drop(true);\n\n    let executor = FileSystem::new(&PathBuf::from(\"base_template\"), &test_dir.root);\n\n    let wizard_selection = wizard::Selections {\n        db: db.clone(),\n        background: BackgroundOption::Async,\n        asset: asset.clone(),\n    };\n    let settings =\n        settings::Settings::from_wizard(\"test-loco-template\", &wizard_selection, OS::default());\n\n    let res = Generator::new(Arc::new(executor), settings.clone()).run();\n    assert!(res.is_ok());\n\n    let mut env_map: HashMap<_, _> = std::env::vars().collect();\n    env_map.insert(\"RUSTFLAGS\".into(), \"-D warnings\".into());\n    env_map.insert(\"DB_CONNECT_TIMEOUT\".into(), \"2000\".into());\n    env_map.insert(\"DB_IDLE_TIMEOUT\".into(), \"2000\".into());\n\n    let tester = Tester {\n        dir: test_dir.root,\n        env_map,\n    };\n\n    tester\n        .run_clippy()\n        .expect(\"run clippy after create new project\");\n\n    tester\n        .run_test()\n        .expect(\"run test after create new project\");\n\n    if test_generator {\n        // Generate API controller\n        tester.run_generate(&vec![\n            \"controller\",\n            \"notes_api\",\n            \"--api\",\n            \"create_note\",\n            \"get_note\",\n        ]);\n\n        if asset.enable() {\n            // Generate HTMX controller\n            tester.run_generate(&vec![\n                \"controller\",\n                \"notes_htmx\",\n                \"--htmx\",\n                \"create_note\",\n                \"get_note\",\n            ]);\n\n            // Generate HTML controller\n            tester.run_generate(&vec![\n                \"controller\",\n                \"notes_html\",\n                \"--html\",\n                \"create_note\",\n                \"get_note\",\n            ]);\n        }\n\n        // Generate Task\n        tester.run_generate(&vec![\"task\", \"list_users\"]);\n\n        // Generate Scheduler\n        tester.run_generate(&vec![\"scheduler\"]);\n\n        // Generate Worker (background workers are always enabled)\n        tester.run_generate(&vec![\"worker\", \"cleanup\"]);\n\n        if settings.mailer {\n            // Generate Mailer\n            tester.run_generate(&vec![\"mailer\", \"user_mailer\"]);\n        }\n\n        // Generate deployment nginx\n        tester.run_generate(&vec![\"deployment\", \"nginx\"]);\n\n        // Generate deployment docker\n        tester.run_generate(&vec![\"deployment\", \"docker\"]);\n\n        // Generate data\n        tester.run_generate(&vec![\"data\", \"stocks\"]);\n\n        if db.enable() {\n            // Generate Model\n            if !settings.auth {\n                tester.run_generate(&vec![\"model\", \"users\", \"name:string\", \"email:string\"]);\n            }\n            tester.run_generate(&vec![\"model\", \"movies\", \"title:string\", \"user:references\"]);\n\n            if asset.enable() {\n                // Generate HTMX Scaffold\n                tester.run_generate(&vec![\n                    \"scaffold\",\n                    \"movies_htmx\",\n                    \"title:string\",\n                    \"user:references\",\n                    \"--htmx\",\n                ]);\n\n                // Generate HTML Scaffold\n                tester.run_generate(&vec![\n                    \"scaffold\",\n                    \"movies_html\",\n                    \"title:string\",\n                    \"user:references\",\n                    \"--html\",\n                ]);\n            }\n\n            // Generate API Scaffold\n            tester.run_generate(&vec![\n                \"scaffold\",\n                \"movies_api\",\n                \"title:string\",\n                \"user:references\",\n                \"--api\",\n            ]);\n\n            // Generate CreatePosts migration\n            tester.run_generate_migration(&vec![\n                \"CreatePosts\",\n                \"title:string\",\n                \"movies:references\",\n            ]);\n\n            // Generate AddNameAndAgeToUsers migration\n            tester.run_generate_migration(&vec![\n                \"AddNameAndAgeToUsers\",\n                \"first_name:string\",\n                \"age:int\",\n            ]);\n\n            // Generate AddNameAndAgeToUsers migration\n            tester.run_generate_migration(&vec![\n                \"RemoveNameAndAgeFromUsers\",\n                \"first_name:string\",\n                \"age:int\",\n            ]);\n\n            // Generate AddUserRefToPosts migration\n            tester.run_generate_migration(&vec![\"AddUserRefToPosts\", \"users:references\"]);\n\n            // Generate CreateJoinTableUsersAndGroups migration\n            tester.run_generate_migration(&vec![\"CreateJoinTableUsersAndGroups\", \"count:int\"]);\n        }\n    }\n}\n\nstruct Tester {\n    dir: PathBuf,\n    env_map: HashMap<String, String>,\n}\n\nimpl Tester {\n    fn run_clippy(&self) -> Result<Output, std::io::Error> {\n        cmd!(\n            \"cargo\",\n            \"clippy\",\n            // \"--quiet\",\n            \"--\",\n            \"-W\",\n            \"clippy::pedantic\",\n            \"-W\",\n            \"clippy::nursery\",\n            \"-W\",\n            \"rust-2018-idioms\",\n            \"-A\",\n            \"clippy::result_large_err\"\n        )\n        .full_env(&self.env_map)\n        // .stdout_null()\n        // .stderr_null()\n        .dir(&self.dir)\n        .run()\n    }\n\n    fn run_test(&self) -> Result<Output, std::io::Error> {\n        cmd!(\"cargo\", \"test\")\n            // .stdout_null()\n            // .stderr_null()\n            .full_env(&self.env_map)\n            .dir(&self.dir)\n            .run()\n    }\n\n    fn run_migrate(&self) -> Result<Output, std::io::Error> {\n        cmd!(\"cargo\", \"loco\", \"db\", \"migrate\")\n            // .stdout_null()\n            // .stderr_null()\n            .full_env(&self.env_map)\n            .dir(&self.dir)\n            .run()\n    }\n\n    fn run_generate(&self, command: &Vec<&str>) {\n        let base_command = vec![\"loco\", \"generate\"];\n\n        // Concatenate base_command with the command vector\n        let mut args = base_command.clone();\n        args.extend(command);\n\n        duct::cmd(\"cargo\", &args)\n            // .stdout_null()\n            // .stderr_null()\n            .full_env(&self.env_map)\n            .dir(&self.dir)\n            .run()\n            .unwrap_or_else(|_| panic!(\"generate `{}`\", command.join(\" \")));\n\n        self.run_clippy()\n            .unwrap_or_else(|_| panic!(\"Run clippy after generate `{}`\", command.join(\" \")));\n\n        self.run_test()\n            .unwrap_or_else(|_| panic!(\"Run Test after generate `{}`\", command.join(\" \")));\n    }\n\n    fn run_generate_migration(&self, command: &Vec<&str>) {\n        let base_command = vec![\"loco\", \"generate\", \"migration\"];\n\n        // Concatenate base_command with the command vector\n        let mut args = base_command.clone();\n        args.extend(command);\n\n        duct::cmd(\"cargo\", &args)\n            // .stdout_null()\n            // .stderr_null()\n            .full_env(&self.env_map)\n            .dir(&self.dir)\n            .run()\n            .unwrap_or_else(|_| panic!(\"generate `{}`\", command.join(\" \")));\n\n        self.run_migrate().unwrap_or_else(|_| {\n            panic!(\n                \"Run migrate after creating the migration `{}`\",\n                command.join(\" \")\n            )\n        });\n\n        self.run_clippy().unwrap_or_else(|_| {\n            panic!(\n                \"Run clippy after generate migration `{}`\",\n                command.join(\" \")\n            )\n        });\n\n        self.run_test().unwrap_or_else(|_| {\n            panic!(\"Run Test after generate migration `{}`\", command.join(\" \"))\n        });\n    }\n}\n"
  },
  {
    "path": "snipdoc.yml",
    "content": "snippets:\n  description:\n    content: 🚂 Loco is Rust on Rails.\n    path: ./snipdoc.yml\n  help-command:\n    content: cargo loco --help\n    path: ./snipdoc.yml\n  exec-help-command:\n    content: cd ./examples/demo && cargo loco --help\n    path: ./snipdoc.yml\n  build-command:\n    content: cargo build --release\n    path: ./snipdoc.yml\n  quick-installation-command:\n    content: |-\n      cargo install loco\n      cargo install sea-orm-cli # Only when DB is needed\n    path: ./snipdoc.yml\n  loco-cli-new-from-template:\n    content: |-\n      ❯ loco new\n      ✔ ❯ App name? · myapp\n      ✔ ❯ What would you like to build? · Saas App with client side rendering\n      ✔ ❯ Select a DB Provider · Sqlite\n      ✔ ❯ Select your background worker type · Async (in-process tokio async tasks)\n\n      🚂 Loco app generated successfully in:\n      myapp/\n\n      - assets: You've selected `clientside` for your asset serving configuration.\n\n      Next step, build your frontend:\n        $ cd frontend/\n        $ npm install && npm run build\n\n    path: ./snipdoc.yml\n  postgres-run-docker-command:\n    content: |-\n      docker run -d -p 5432:5432 \\\n        -e POSTGRES_USER=loco \\\n        -e POSTGRES_DB=myapp_development \\\n        -e POSTGRES_PASSWORD=\"loco\" \\\n        postgres:15.3-alpine\n    path: ./snipdoc.yml\n  redis-run-docker-command:\n    content: docker run -p 6379:6379 -d redis redis-server\n    path: ./snipdoc.yml\n  starting-the-server-command:\n    content: cargo loco start\n    path: ./snipdoc.yml\n  starting-the-server-command-with-environment-env-var:\n    content: LOCO_ENV=qa cargo loco start\n    path: ./snipdoc.yml\n  starting-the-server-command-with-output:\n    content: |-\n      $ cargo loco start\n\n                            ▄     ▀\n                                      ▀  ▄\n                        ▄       ▀     ▄  ▄ ▄▀\n                                          ▄ ▀▄▄\n                              ▄     ▀    ▀  ▀▄▀█▄\n                                                ▀█▄\n      ▄▄▄▄▄▄▄  ▄▄▄▄▄▄▄▄▄   ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄ ▀▀█\n      ██████  █████   ███ █████   ███ █████   ███ ▀█\n      ██████  █████   ███ █████   ▀▀▀ █████   ███ ▄█▄\n      ██████  █████   ███ █████       █████   ███ ████▄\n      ██████  █████   ███ █████   ▄▄▄ █████   ███ █████\n      ██████  █████   ███  ████   ███ █████   ███ ████▀\n        ▀▀▀██▄ ▀▀▀▀▀▀▀▀▀▀  ▀▀▀▀▀▀▀▀▀▀  ▀▀▀▀▀▀▀▀▀▀ ██▀\n            ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀\n                      https://loco.rs\n\n      listening on port 5150\n    path: ./snipdoc.yml\n  doctor-command:\n    content: |-\n      $ cargo loco doctor\n          Finished dev [unoptimized + debuginfo] target(s) in 0.32s\n          Running `target/debug/myapp-cli doctor`\n      ✅ SeaORM CLI is installed\n      ✅ DB connection: success\n      ✅ Redis connection: success\n    path: ./snipdoc.yml\n  generate-deployment-command:\n    content: |-\n      cargo loco generate deployment docker     \n\n      added: \"Dockerfile\"\n      added: \".dockerignore\"\n      * Dockerfile generated successfully.\n      * Dockerignore generated successfully\n    path: ./snipdoc.yml\n  scaffold-help-command:\n    content: cd ./examples/demo && cargo loco generate scaffold --help\n    path: ./snipdoc.yml\n  scaffold-post-command:\n    content: cargo loco generate scaffold posts\n      name:string title:string content:text --api\n    path: ./snipdoc.yml\n  generate-task-help-command:\n    content: cd ./examples/demo && cargo loco generate task --help\n    path: ./snipdoc.yml\n  run-task-command:\n    content: cargo loco task <TASK_NAME>\n    path: ./snipdoc.yml\n  list-tasks-command:\n    content: cargo loco task\n    path: ./snipdoc.yml\n  migrate-down-command:\n    content: cargo loco db down\n    path: ./snipdoc.yml\n  migrate-down-n-command:\n    content: cargo loco db down 2\n    path: ./snipdoc.yml\n  scheduler-generate-command:\n    content: cargo loco generate scheduler\n    path: ./snipdoc.yml\n  scheduler-list-command:\n    content: cargo loco scheduler --list\n    path: ./snipdoc.yml\n  scheduler-list-from-file-command:\n    content: cargo loco scheduler --config config/scheduler.yaml --list\n    path: ./snipdoc.yml\n  scheduler-list-from-env-setting-command:\n    content: LOCO_ENV=production cargo loco scheduler --list\n    path: ./snipdoc.yml\n  scheduler-run-job-by-name-command:\n    content: LOCO_ENV=production cargo loco scheduler --name 'JOB_NAME'\n    path: ./snipdoc.yml\n  scheduler-run-job-by-tag-command:\n    content: LOCO_ENV=production cargo loco scheduler --tag 'maintenance'\n    path: ./snipdoc.yml\n  cli-middleware-list:\n    content: cargo loco middleware --config\n    path: ./snipdoc.yml\n  jobs-help-command:\n    content: cd ./examples/demo && cargo loco jobs --help\n    path: ./snipdoc.yml\n  seed-help-command:\n    content: cd ./examples/demo && cargo loco db seed --help\n    path: ./snipdoc.yml\n"
  },
  {
    "path": "src/app.rs",
    "content": "//! This module contains the core components and traits for building a web\n//! server application.\n#[cfg(feature = \"with-db\")]\nuse {sea_orm::DatabaseConnection, std::path::Path};\n\nuse std::{\n    any::{Any, TypeId},\n    net::SocketAddr,\n    sync::Arc,\n};\n\nuse async_trait::async_trait;\nuse axum::extract::FromRef;\nuse axum::Router as AxumRouter;\nuse dashmap::DashMap;\n\nuse crate::{\n    bgworker::{self, Queue},\n    boot::{shutdown_signal, BootResult, ServeParams, StartMode},\n    cache::{self},\n    config::Config,\n    controller::{\n        middleware::{self, MiddlewareLayer},\n        AppRoutes,\n    },\n    environment::Environment,\n    mailer::EmailSender,\n    storage::Storage,\n    task::Tasks,\n    Result,\n};\n\n/// Type-safe heterogeneous storage for arbitrary application data\n#[derive(Default, Debug)]\npub struct SharedStore {\n    // Use DashMap for concurrent access with fine-grained locking\n    storage: DashMap<TypeId, Box<dyn Any + Send + Sync>>,\n}\n\nimpl SharedStore {\n    /// Insert a value of type T into the shared store\n    ///\n    /// # Example\n    /// ```\n    /// # use loco_rs::app::SharedStore;\n    /// let shared_store = SharedStore::default();\n    ///\n    /// #[derive(Debug)]\n    /// struct TestService {\n    ///     name: String,\n    ///     value: i32,\n    /// }\n    ///\n    /// let service = TestService {\n    ///     name: \"test\".to_string(),\n    ///     value: 100,\n    /// };\n    ///\n    /// shared_store.insert(service);\n    /// assert!(shared_store.contains::<TestService>());\n    /// ```\n    pub fn insert<T: 'static + Send + Sync>(&self, val: T) {\n        self.storage.insert(TypeId::of::<T>(), Box::new(val));\n    }\n\n    /// Remove a value of type T from the shared store\n    ///\n    /// Returns `Some(T)` if the value was present and removed, `None` otherwise.\n    ///\n    /// # Example\n    /// ```\n    /// # use loco_rs::app::SharedStore;\n    /// let shared_store = SharedStore::default();\n    ///\n    /// struct TestService {\n    ///     name: String,\n    ///     value: i32,\n    /// }\n    ///\n    /// let service = TestService {\n    ///     name: \"test\".to_string(),\n    ///     value: 100,\n    /// };\n    ///\n    /// shared_store.insert(service);\n    /// assert!(shared_store.contains::<TestService>());\n    ///\n    /// // Remove and get the value\n    /// let removed_service_opt = shared_store.remove::<TestService>();\n    /// assert!(removed_service_opt.is_some(), \"Service should be present\");\n    /// // Assert fields individually instead of comparing the whole struct\n    /// if let Some(removed_service) = removed_service_opt {\n    ///      assert_eq!(removed_service.name, \"test\");\n    ///      assert_eq!(removed_service.value, 100);\n    /// }\n    /// // Ensure it's gone\n    /// assert!(!shared_store.contains::<TestService>());\n    ///\n    /// // Trying to remove again returns None\n    /// assert!(shared_store.remove::<TestService>().is_none());\n    /// ```\n    #[must_use]\n    pub fn remove<T: 'static + Send + Sync>(&self) -> Option<T> {\n        self.storage\n            .remove(&TypeId::of::<T>())\n            .map(|(_, v)| v) // Extract the Box<dyn Any>\n            .and_then(|any| any.downcast::<T>().ok()) // Downcast to Box<T>\n            .map(|boxed| *boxed) // Dereference the Box<T> to get T\n    }\n\n    /// Get a reference to a value of type T from the shared store.\n    ///\n    /// Returns `None` if the value doesn't exist.\n    /// The reference is valid for as long as the returned `RefGuard` is held.\n    /// If you need to clone the value, you can do so directly from the\n    /// returned reference, or use the `get` method instead.\n    ///\n    /// # Example\n    /// ```\n    /// # use loco_rs::app::SharedStore;\n    /// let shared_store = SharedStore::default();\n    ///\n    /// #[derive(Clone)]\n    /// struct TestService {\n    ///     name: String,\n    ///     value: i32,\n    /// }\n    ///\n    /// let service = TestService {\n    ///     name: \"test\".to_string(),\n    ///     value: 100,\n    /// };\n    ///\n    /// shared_store.insert(service);\n    ///\n    /// // Get a reference to the service\n    /// let service_ref = shared_store.get_ref::<TestService>().expect(\"Service not found\");\n    /// // Access fields directly\n    /// assert_eq!(service_ref.name, \"test\");\n    /// assert_eq!(service_ref.value, 100);\n    ///\n    /// // Clone if needed (the field itself)\n    /// let name_clone = service_ref.name.clone();\n    /// assert_eq!(name_clone, \"test\");\n    ///\n    /// // Compute values from the reference\n    /// let name_len = service_ref.name.len();\n    /// assert_eq!(name_len, 4);\n    /// ```\n    #[must_use]\n    pub fn get_ref<T: 'static + Send + Sync>(&self) -> Option<RefGuard<'_, T>> {\n        let type_id = TypeId::of::<T>();\n        self.storage.get(&type_id).map(|r| RefGuard::<T> {\n            inner: r,\n            _phantom: std::marker::PhantomData,\n        })\n    }\n\n    /// Get a clone of a value of type T from the shared store.\n    /// Requires T to implement Clone.\n    ///\n    /// Returns `None` if the value doesn't exist.\n    /// This method clones the stored value.\n    /// If cloning is not desired or T does not implement Clone,\n    /// use `get_ref` instead.\n    ///\n    /// # Example\n    /// ```\n    /// # use loco_rs::app::SharedStore;\n    /// let shared_store = SharedStore::default();\n    ///\n    /// #[derive(Clone)]\n    /// struct TestService {\n    ///     name: String,\n    ///     value: i32,\n    /// }\n    ///\n    /// let service = TestService {\n    ///     name: \"test\".to_string(),\n    ///     value: 100,\n    /// };\n    ///\n    /// shared_store.insert(service);\n    ///\n    /// // Get a clone of the service\n    /// let service_clone_opt = shared_store.get::<TestService>();\n    /// assert!(service_clone_opt.is_some(), \"Service not found\");\n    /// // Assert fields individually\n    /// if let Some(ref service_clone) = service_clone_opt {\n    ///     assert_eq!(service_clone.name, \"test\");\n    ///     assert_eq!(service_clone.value, 100);\n    /// }\n    /// ```\n    #[must_use]\n    pub fn get<T: 'static + Send + Sync + Clone>(&self) -> Option<T> {\n        self.get_ref::<T>().map(|guard| (*guard).clone())\n    }\n\n    /// Check if the shared store contains a value of type T\n    ///\n    /// # Example\n    /// ```\n    /// # use loco_rs::app::SharedStore;\n    /// let shared_store = SharedStore::default();\n    ///\n    /// struct TestService {\n    ///     name: String,\n    ///     value: i32,\n    /// }\n    ///\n    /// let service = TestService {\n    ///     name: \"test\".to_string(),\n    ///     value: 100,\n    /// };\n    ///\n    /// shared_store.insert(service);\n    /// assert!(shared_store.contains::<TestService>());\n    /// assert!(!shared_store.contains::<String>());\n    /// ```\n    #[must_use]\n    pub fn contains<T: 'static + Send + Sync>(&self) -> bool {\n        self.storage.contains_key(&TypeId::of::<T>())\n    }\n}\n\n// A wrapper around DashMap's Ref type that erases the exact type\n// but provides deref to the target type\npub struct RefGuard<'a, T: 'static + Send + Sync> {\n    inner: dashmap::mapref::one::Ref<'a, TypeId, Box<dyn Any + Send + Sync>>,\n    _phantom: std::marker::PhantomData<&'a T>,\n}\n\nimpl<T: 'static + Send + Sync> std::ops::Deref for RefGuard<'_, T> {\n    type Target = T;\n\n    fn deref(&self) -> &Self::Target {\n        // This is safe because we only create a RefGuard for a specific type\n        // after looking it up by its TypeId\n        #[allow(clippy::coerce_container_to_any)]\n        self.inner\n            .value()\n            .downcast_ref::<T>()\n            .expect(\"Type mismatch in RefGuard\")\n    }\n}\n\n/// Represents the application context for a web server.\n///\n/// This struct encapsulates various components and configurations required by\n/// the web server to operate. It is typically used to store and manage shared\n/// resources and settings that are accessible throughout the application's\n/// lifetime.\n#[derive(Clone, FromRef)]\n#[allow(clippy::module_name_repetitions)]\npub struct AppContext {\n    /// The environment in which the application is running.\n    pub environment: Environment,\n    #[cfg(feature = \"with-db\")]\n    /// A database connection used by the application.\n    pub db: DatabaseConnection,\n    /// Queue provider\n    pub queue_provider: Option<Arc<bgworker::Queue>>,\n    /// Configuration settings for the application\n    pub config: Config,\n    /// An optional email sender component that can be used to send email.\n    pub mailer: Option<EmailSender>,\n    // An optional storage instance for the application\n    pub storage: Arc<Storage>,\n    // Cache instance for the application\n    pub cache: Arc<cache::Cache>,\n    /// Shared store for arbitrary application data\n    pub shared_store: Arc<SharedStore>,\n}\n\n/// A trait that defines hooks for customizing and extending the behavior of a\n/// web server application.\n///\n/// Users of the web server application should implement this trait to customize\n/// the application's routing, worker connections, task registration, and\n/// database actions according to their specific requirements and use cases.\n#[async_trait]\npub trait Hooks: Send {\n    /// Defines the composite app version\n    #[must_use]\n    fn app_version() -> String {\n        \"dev\".to_string()\n    }\n    /// Defines the crate name\n    ///\n    /// Example\n    /// ```rust\n    /// fn app_name() -> &'static str {\n    ///     env!(\"CARGO_CRATE_NAME\")\n    /// }\n    /// ```\n    fn app_name() -> &'static str;\n\n    /// Initializes and boots the application based on the specified mode and\n    /// environment.\n    ///\n    /// The boot initialization process may vary depending on whether a DB\n    /// migrator is used or not.\n    ///\n    /// # Examples\n    ///\n    /// With DB:\n    /// ```rust,ignore\n    /// async fn boot(mode: StartMode, environment: &str, config: Config) -> Result<BootResult> {\n    ///     create_app::<Self, Migrator>(mode, environment, config).await\n    /// }\n    /// ````\n    ///\n    /// Without DB:\n    /// ```rust,ignore\n    /// async fn boot(mode: StartMode, environment: &str, config: Config) -> Result<BootResult> {\n    ///     create_app::<Self>(mode, environment, config).await\n    /// }\n    /// ````\n    ///\n    ///\n    /// # Errors\n    /// Could not boot the application\n    async fn boot(mode: StartMode, environment: &Environment, config: Config)\n        -> Result<BootResult>;\n\n    /// Start serving the Axum web application on the specified address and\n    /// port.\n    ///\n    /// # Returns\n    /// A Result indicating success () or an error if the server fails to start.\n    async fn serve(app: AxumRouter, ctx: &AppContext, serve_params: &ServeParams) -> Result<()> {\n        let listener = tokio::net::TcpListener::bind(&format!(\n            \"{}:{}\",\n            serve_params.binding, serve_params.port\n        ))\n        .await?;\n\n        let cloned_ctx = ctx.clone();\n        axum::serve(\n            listener,\n            app.into_make_service_with_connect_info::<SocketAddr>(),\n        )\n        .with_graceful_shutdown(async move {\n            shutdown_signal().await;\n            tracing::info!(\"shutting down...\");\n            Self::on_shutdown(&cloned_ctx).await;\n        })\n        .await?;\n\n        Ok(())\n    }\n\n    /// Override and return `Ok(true)` to provide an alternative logging and\n    /// tracing stack of your own.\n    /// When returning `Ok(true)`, Loco will *not* initialize its own logger,\n    /// so you should set up a complete tracing and logging stack.\n    ///\n    /// # Errors\n    /// If fails returns an error\n    fn init_logger(_ctx: &AppContext) -> Result<bool> {\n        Ok(false)\n    }\n\n    /// Loads the configuration settings for the application based on the given environment.\n    ///\n    /// This function is responsible for retrieving the configuration for the application\n    /// based on the current environment.\n    async fn load_config(env: &Environment) -> Result<Config> {\n        env.load()\n    }\n\n    /// Returns the initial Axum router for the application, allowing the user\n    /// to control the construction of the Axum router. This is where a fallback\n    /// handler can be installed before middleware or other routes are added.\n    ///\n    /// # Errors\n    /// Return an [`Result`] when the router could not be created\n    async fn before_routes(_ctx: &AppContext) -> Result<AxumRouter<AppContext>> {\n        Ok(AxumRouter::new())\n    }\n\n    /// Invoke this function after the Loco routers have been constructed. This\n    /// function enables you to configure custom Axum logics, such as layers,\n    /// that are compatible with Axum.\n    ///\n    /// # Errors\n    /// Axum router error\n    async fn after_routes(router: AxumRouter, _ctx: &AppContext) -> Result<AxumRouter> {\n        Ok(router)\n    }\n\n    /// Provide a list of initializers\n    /// An initializer can be used to seamlessly add functionality to your app\n    /// or to initialize some aspects of it.\n    async fn initializers(_ctx: &AppContext) -> Result<Vec<Box<dyn Initializer>>> {\n        Ok(vec![])\n    }\n\n    /// Provide a list of middlewares\n    #[must_use]\n    fn middlewares(ctx: &AppContext) -> Vec<Box<dyn MiddlewareLayer>> {\n        middleware::default_middleware_stack(ctx)\n    }\n\n    /// Calling the function before run the app\n    /// You can now code some custom loading of resources or other things before\n    /// the app runs\n    async fn before_run(_app_context: &AppContext) -> Result<()> {\n        Ok(())\n    }\n\n    /// Defines the application's routing configuration.\n    fn routes(_ctx: &AppContext) -> AppRoutes;\n\n    // Provides the options to change Loco [`AppContext`] after initialization.\n    async fn after_context(ctx: AppContext) -> Result<AppContext> {\n        Ok(ctx)\n    }\n\n    /// Connects custom workers to the application using the provided\n    /// [`Processor`] and [`AppContext`].\n    async fn connect_workers(ctx: &AppContext, queue: &Queue) -> Result<()>;\n\n    /// Registers custom tasks with the provided [`Tasks`] object.\n    fn register_tasks(tasks: &mut Tasks);\n\n    /// Truncates the database as required. Users should implement this\n    /// function. The truncate controlled from the [`crate::config::Database`]\n    /// by changing `dangerously_truncate` to true (default false).\n    /// Truncate can be useful when you want to truncate the database before any\n    /// test.\n    #[cfg(feature = \"with-db\")]\n    async fn truncate(_ctx: &AppContext) -> Result<()>;\n\n    /// Seeds the database with initial data.\n    #[cfg(feature = \"with-db\")]\n    async fn seed(_ctx: &AppContext, path: &Path) -> Result<()>;\n\n    /// Called when the application is shutting down.\n    /// This function allows users to perform any necessary cleanup or final\n    /// actions before the application stops completely.\n    async fn on_shutdown(_ctx: &AppContext) {}\n}\n\n/// An initializer.\n/// Initializers should be kept in `src/initializers/`\n///\n/// Initializers can provide health checks by implementing the `check` method.\n/// These checks will be run during the `cargo loco doctor` command to validate\n/// the initializer's configuration and test its connections.\n#[async_trait]\n// <snip id=\"initializers-trait\">\npub trait Initializer: Sync + Send {\n    /// The initializer name or identifier\n    fn name(&self) -> String;\n\n    /// Occurs after the app's `before_run`.\n    /// Use this to for one-time initializations, load caches, perform web\n    /// hooks, etc.\n    async fn before_run(&self, _app_context: &AppContext) -> Result<()> {\n        Ok(())\n    }\n\n    /// Occurs after the app's `after_routes`.\n    /// Use this to compose additional functionality and wire it into an Axum\n    /// Router\n    async fn after_routes(&self, router: AxumRouter, _ctx: &AppContext) -> Result<AxumRouter> {\n        Ok(router)\n    }\n\n    /// Perform health checks for this initializer.\n    /// This method is called during the doctor command to validate the initializer's configuration.\n    /// Return `None` if no check is needed, or `Some(Check)` if a check should be performed.\n    async fn check(&self, _app_context: &AppContext) -> Result<Option<crate::doctor::Check>> {\n        Ok(None)\n    }\n}\n// </snip>\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use crate::tests_cfg::app::get_app_context;\n\n    struct TestService {\n        name: String,\n        value: i32,\n    }\n\n    #[derive(Clone)]\n    struct CloneableTestService {\n        name: String,\n        value: i32,\n    }\n\n    #[test]\n    fn test_extensions_insert_and_get() {\n        // Setup\n        let shared_store = SharedStore::default();\n\n        shared_store.insert(42i32);\n        assert_eq!(shared_store.get::<i32>().expect(\"Value should exist\"), 42);\n\n        let service = TestService {\n            name: \"test\".to_string(),\n            value: 100,\n        };\n\n        shared_store.insert(service);\n\n        let service_ref_opt = shared_store.get_ref::<TestService>();\n        assert!(service_ref_opt.is_some(), \"Service ref should exist\");\n        if let Some(service_ref) = service_ref_opt {\n            assert_eq!(service_ref.name, \"test\");\n            assert_eq!(service_ref.value, 100);\n            let name_clone = service_ref.name.clone();\n            assert_eq!(name_clone, \"test\");\n        } else {\n            panic!(\"Should have gotten Some(service_ref)\");\n        }\n    }\n\n    #[test]\n    fn test_extensions_get_without_clone() {\n        let shared_store = SharedStore::default();\n\n        let service = TestService {\n            name: \"test_direct\".to_string(),\n            value: 100,\n        };\n        shared_store.insert(service);\n\n        let service_ref_opt = shared_store.get_ref::<TestService>();\n        assert!(service_ref_opt.is_some(), \"Service ref should exist\");\n        if let Some(service_ref) = service_ref_opt {\n            assert_eq!(service_ref.name, \"test_direct\");\n            assert_eq!(service_ref.value, 100);\n        } else {\n            panic!(\"Should have gotten Some(service_ref)\");\n        }\n\n        let name_len_opt = shared_store.get_ref::<TestService>().map(|r| r.name.len());\n        assert!(\n            name_len_opt.is_some(),\n            \"Service ref should exist for len check\"\n        );\n        assert_eq!(name_len_opt.unwrap(), 11);\n\n        let value_opt = shared_store.get_ref::<TestService>().map(|r| r.value);\n        assert!(\n            value_opt.is_some(),\n            \"Service ref should exist for value check\"\n        );\n        assert_eq!(value_opt.unwrap(), 100);\n    }\n\n    #[test]\n    fn test_extensions_remove() {\n        let shared_store = SharedStore::default();\n\n        shared_store.insert(42i32);\n        assert!(shared_store.contains::<i32>());\n        assert_eq!(shared_store.remove::<i32>(), Some(42));\n        assert!(!shared_store.contains::<i32>());\n        assert_eq!(shared_store.remove::<i32>(), None);\n\n        let service = TestService {\n            name: \"rem\".to_string(),\n            value: 50,\n        };\n        shared_store.insert(service);\n        assert!(shared_store.contains::<TestService>());\n        let removed_opt = shared_store.remove::<TestService>();\n        assert!(removed_opt.is_some());\n        if let Some(removed) = removed_opt {\n            assert_eq!(removed.name, \"rem\");\n            assert_eq!(removed.value, 50);\n        } else {\n            panic!(\"Removed option should be Some\");\n        }\n        assert!(!shared_store.contains::<TestService>());\n        assert!(shared_store.remove::<TestService>().is_none());\n    }\n\n    #[test]\n    fn test_extensions_contains() {\n        let shared_store = SharedStore::default();\n\n        shared_store.insert(42i32);\n        shared_store.insert(TestService {\n            name: \"contains\".to_string(),\n            value: 1,\n        });\n\n        assert!(shared_store.contains::<i32>());\n        assert!(shared_store.contains::<TestService>());\n        assert!(!shared_store.contains::<String>());\n        assert!(!shared_store.contains::<CloneableTestService>());\n    }\n\n    #[test]\n    fn test_extensions_get_cloned() {\n        let shared_store = SharedStore::default();\n\n        shared_store.insert(42i32);\n        assert_eq!(shared_store.get::<i32>(), Some(42));\n        assert!(shared_store.contains::<i32>());\n\n        let service = CloneableTestService {\n            name: \"cloned_test\".to_string(),\n            value: 200,\n        };\n        shared_store.insert(service.clone());\n\n        let service_clone_opt = shared_store.get::<CloneableTestService>();\n        assert!(service_clone_opt.is_some(), \"Cloned service should exist\");\n        if let Some(ref service_clone) = service_clone_opt {\n            assert_eq!(service_clone.name, \"cloned_test\");\n            assert_eq!(service_clone.value, 200);\n        } else {\n            panic!(\"Should have gotten Some(service_clone)\");\n        }\n\n        assert!(shared_store.contains::<CloneableTestService>());\n        let original_ref_opt = shared_store.get_ref::<CloneableTestService>();\n        assert!(original_ref_opt.is_some(), \"Original ref should exist\");\n        if let Some(original_ref) = original_ref_opt {\n            assert_eq!(original_ref.name, \"cloned_test\");\n            assert_eq!(original_ref.value, 200);\n        } else {\n            panic!(\"Should have gotten Some(original_ref)\");\n        }\n\n        assert_eq!(shared_store.get::<String>(), None);\n        assert!(shared_store.get::<CloneableTestService>().is_some());\n        // The following line correctly fails to compile because TestService doesn't impl Clone,\n        // which is required by the `get` method.\n        // let non_existent_clone = shared_store.get::<TestService>();\n    }\n\n    #[tokio::test]\n    async fn test_app_context_extensions() {\n        let ctx = get_app_context().await;\n\n        let service_cloneable = CloneableTestService {\n            name: \"app_context_test_cloneable\".to_string(),\n            value: 42,\n        };\n        ctx.shared_store.insert(service_cloneable.clone());\n\n        let ref_opt = ctx.shared_store.get_ref::<CloneableTestService>();\n        assert!(ref_opt.is_some(), \"Cloneable service ref should exist\");\n        if let Some(service_ref) = ref_opt {\n            assert_eq!(service_ref.name, \"app_context_test_cloneable\");\n            assert_eq!(service_ref.value, 42);\n        } else {\n            panic!(\"Should have gotten Some(service_ref)\");\n        }\n\n        let clone_opt = ctx.shared_store.get::<CloneableTestService>();\n        assert!(clone_opt.is_some(), \"Should get cloned service\");\n        if let Some(service_clone) = clone_opt {\n            assert_eq!(service_clone.name, \"app_context_test_cloneable\");\n            assert_eq!(service_clone.value, 42);\n        } else {\n            panic!(\"Should have gotten Some(service_clone)\");\n        }\n\n        assert!(ctx.shared_store.contains::<CloneableTestService>());\n        assert!(!ctx.shared_store.contains::<String>());\n\n        let removed_cloneable_opt = ctx.shared_store.remove::<CloneableTestService>();\n        assert!(removed_cloneable_opt.is_some());\n        if let Some(removed) = removed_cloneable_opt {\n            assert_eq!(removed.name, \"app_context_test_cloneable\");\n            assert_eq!(removed.value, 42);\n        } else {\n            panic!(\"Removed cloneable option should be Some\");\n        }\n        assert!(!ctx.shared_store.contains::<CloneableTestService>());\n\n        let service_non_cloneable = TestService {\n            name: \"app_context_test_non_cloneable\".to_string(),\n            value: 99,\n        };\n        ctx.shared_store.insert(service_non_cloneable);\n\n        let non_clone_ref_opt = ctx.shared_store.get_ref::<TestService>();\n        assert!(\n            non_clone_ref_opt.is_some(),\n            \"Non-cloneable service ref should exist\"\n        );\n        if let Some(service_ref) = non_clone_ref_opt {\n            assert_eq!(service_ref.name, \"app_context_test_non_cloneable\");\n            assert_eq!(service_ref.value, 99);\n        } else {\n            panic!(\"Should have gotten Some(service_ref)\");\n        }\n\n        assert!(ctx.shared_store.contains::<TestService>());\n\n        let removed_non_cloneable_opt = ctx.shared_store.remove::<TestService>();\n        assert!(removed_non_cloneable_opt.is_some());\n        if let Some(removed) = removed_non_cloneable_opt {\n            assert_eq!(removed.name, \"app_context_test_non_cloneable\");\n            assert_eq!(removed.value, 99);\n        } else {\n            panic!(\"Removed non-cloneable option should be Some\");\n        }\n        assert!(!ctx.shared_store.contains::<TestService>());\n    }\n}\n"
  },
  {
    "path": "src/auth/jwt.rs",
    "content": "//! # JSON Web Token (JWT) and Password Hashing\n//!\n//! This module provides functionality for working with JSON Web Tokens (JWTs)\n//! and password hashing.\nuse jsonwebtoken::{\n    decode, encode, errors::Result as JWTResult, get_current_timestamp, Algorithm, DecodingKey,\n    EncodingKey, Header, TokenData, Validation,\n};\nuse serde::{Deserialize, Serialize};\nuse serde_json::{Map, Value};\n\n/// Represents the default JWT algorithm used by the [`JWT`] struct.\nconst JWT_ALGORITHM: Algorithm = Algorithm::HS512;\n\n/// Represents the claims associated with a user JWT.\n#[cfg_attr(test, derive(Eq, PartialEq))]\n#[derive(Debug, Serialize, Deserialize)]\npub struct UserClaims {\n    pub pid: String,\n    exp: u64,\n    #[serde(default, flatten)]\n    pub claims: Map<String, Value>,\n}\n\n/// Represents the JWT configuration and operations.\n///\n/// # Example\n/// ```rust\n/// use loco_rs::auth;\n///\n/// auth::jwt::JWT::new(\"PqRwLF2rhHe8J22oBeHy\");\n/// ```\n#[derive(Debug)]\npub struct JWT {\n    secret: String,\n    algorithm: Algorithm,\n}\n\nimpl JWT {\n    /// Creates a new [`JWT`] instance with the specified secret key.\n    #[must_use]\n    pub fn new(secret: &str) -> Self {\n        Self {\n            secret: secret.to_string(),\n            algorithm: JWT_ALGORITHM,\n        }\n    }\n\n    /// Override the default  JWT algorithm to be used.\n    #[must_use]\n    pub fn algorithm(mut self, algorithm: Algorithm) -> Self {\n        self.algorithm = algorithm;\n        self\n    }\n\n    /// Generates a new JWT with specified claims and an expiration time.\n    ///\n    /// # Errors\n    ///\n    /// returns [`JWTResult`] error when could not generate JWT token. can be an\n    /// invalid secret.\n    ///\n    /// # Example\n    /// ```rust\n    /// use serde_json::Map;\n    /// use loco_rs::auth;\n    ///\n    /// auth::jwt::JWT::new(\"PqRwLF2rhHe8J22oBeHy\").generate_token(604800, \"PID\".to_string(), Map::new());\n    /// ```\n    pub fn generate_token(\n        &self,\n        expiration: u64,\n        pid: String,\n        claims: Map<String, Value>,\n    ) -> JWTResult<String> {\n        let exp = get_current_timestamp().saturating_add(expiration);\n\n        let claims = UserClaims { pid, exp, claims };\n\n        let token = encode(\n            &Header::new(self.algorithm),\n            &claims,\n            &EncodingKey::from_base64_secret(&self.secret)?,\n        )?;\n\n        Ok(token)\n    }\n\n    /// Validates the authenticity and expiration of a given JWT.\n    /// If Token is valid, decode the Token Claims.\n    ///\n    /// # Errors\n    ///\n    /// returns [`JWTResult`] error when could not convert the given token to\n    /// [`UserClaims`], if the `secret` is invalid or token is expired.\n    ///\n    /// # Example\n    /// ```rust\n    /// use loco_rs::auth;\n    /// auth::jwt::JWT::new(\"PqRwLF2rhHe8J22oBeHy\").validate(\"JWT-TOKEN\");\n    /// ```\n    pub fn validate(&self, token: &str) -> JWTResult<TokenData<UserClaims>> {\n        let mut validate = Validation::new(self.algorithm);\n        validate.leeway = 0;\n\n        decode::<UserClaims>(\n            token,\n            &DecodingKey::from_base64_secret(&self.secret)?,\n            &validate,\n        )\n    }\n}\n\n#[cfg(test)]\nmod tests {\n\n    use insta::{assert_debug_snapshot, with_settings};\n    use rstest::rstest;\n    use serde_json::json;\n\n    use super::*;\n\n    #[rstest]\n    #[case(\"valid token\", 60, json!({}))]\n    #[case(\"token expired\", 1, json!({}))]\n    #[case(\"valid token and custom string claims\", 60, json!({ \"custom\": \"claim\",}))]\n    #[case(\"valid token and custom boolean claims\",60, json!({ \"custom\": true,}))]\n    #[case(\"valid token and custom number claims\",60, json!({ \"custom\": 123,}))]\n    #[case(\"valid token and custom nested claims\",60, json!({ \"level1\": { \"level2\": { \"level3\": \"claim\" } } }))]\n    #[case(\"valid token and custom array claims\",60, json!({ \"array\": [1, 2, 3] }))]\n    #[case(\"valid token and custom nested array claims\",60, json!({ \"level1\": { \"level2\": { \"level3\": [1, 2, 3] } } }))]\n    fn can_generate_token(\n        #[case] test_name: &str,\n        #[case] expiration: u64,\n        #[case] json_claims: Value,\n    ) {\n        let claims = json_claims\n            .as_object()\n            .expect(\"case input claims must be an object\")\n            .clone();\n        let jwt = JWT::new(\"PqRwLF2rhHe8J22oBeHy\");\n\n        let token = jwt\n            .generate_token(expiration, \"pid\".to_string(), claims)\n            .unwrap();\n\n        std::thread::sleep(std::time::Duration::from_secs(3));\n        with_settings!({filters => vec![\n            (r\"exp: (\\d+),\", \"exp: EXP,\")\n        ]}, {\n            assert_debug_snapshot!(test_name, jwt.validate(&token));\n        });\n    }\n\n    #[rstest]\n    #[case::without_custom_claims(json!({}))]\n    #[case::with_custom_string_claims(json!({ \"custom\": \"claim\",}))]\n    #[case::with_custom_boolean_claims(json!({ \"custom\": true,}))]\n    #[case::with_custom_number_claims(json!({ \"custom\": 123,}))]\n    #[case::with_custom_nested_claims(json!({ \"level1\": { \"level2\": { \"level3\": \"claim\" } } }))]\n    #[case::with_custom_array_claims(json!({ \"array\": [1, 2, 3] }))]\n    #[case::with_custom_nested_array_claims(json!({ \"level1\": { \"level2\": { \"level3\": [1, 2, 3] } } }))]\n    // we use `Value` to reduce code duplicity in the case inputs\n    fn serialize_user_claims(#[case] json_claims: Value) {\n        let claims = json_claims\n            .as_object()\n            .expect(\"case input claims must be an object\")\n            .clone();\n        let input_user_claims = UserClaims {\n            pid: \"pid\".to_string(),\n            exp: 60,\n            claims: claims.clone(),\n        };\n\n        let mut expected_claim = Map::new();\n        expected_claim.insert(\"pid\".to_string(), \"pid\".into());\n        expected_claim.insert(\"exp\".to_string(), 60.into());\n        // we add the claims in a flattened way\n        expected_claim.extend(claims);\n        let expected_value = Value::from(expected_claim);\n\n        // We check between `Value` instead of `String` to avoid key ordering issues when serializing.\n        // It is because `expected_value` has all the keys in alphabetical order, as the `Value` serialization ensures that.\n        // But when serializing `input_user_claims`, first the `pid` and `exp` fields are serialized (in that order),\n        // and then the claims are serialized in alfabetic order. So, the resulting JSON string from the `input_user_claims` serialization\n        // may have the `pid` and `exp` fields unordered which differs from the `Value` serialization.\n        assert_eq!(\n            expected_value,\n            serde_json::to_value(&input_user_claims).unwrap()\n        );\n    }\n\n    #[rstest]\n    #[case::without_custom_claims(json!({}))]\n    #[case::with_custom_string_claims(json!({ \"custom\": \"claim\",}))]\n    #[case::with_custom_boolean_claims(json!({ \"custom\": true,}))]\n    #[case::with_custom_number_claims(json!({ \"custom\": 123,}))]\n    #[case::with_custom_nested_claims(json!({ \"level1\": { \"level2\": { \"level3\": \"claim\" } } }))]\n    #[case::with_custom_array_claims(json!({ \"array\": [1, 2, 3] }))]\n    #[case::with_custom_nested_array_claims(json!({ \"level1\": { \"level2\": { \"level3\": [1, 2, 3] } } }))]\n    // we use `Value` to reduce code duplicity in the case inputs\n    fn deserialize_user_claims(#[case] json_claims: Value) {\n        let claims = json_claims\n            .as_object()\n            .expect(\"case input claims must be an object\")\n            .clone();\n\n        let mut input_claims = Map::new();\n        input_claims.insert(\"pid\".to_string(), \"pid\".into());\n        input_claims.insert(\"exp\".to_string(), 60.into());\n        // we add the claims in a flattened way\n        input_claims.extend(claims.clone());\n        let input_json = Value::from(input_claims).to_string();\n\n        let expected_user_claims = UserClaims {\n            pid: \"pid\".to_string(),\n            exp: 60,\n            claims,\n        };\n\n        assert_eq!(\n            expected_user_claims,\n            serde_json::from_str(&input_json).unwrap()\n        );\n    }\n}\n"
  },
  {
    "path": "src/auth/mod.rs",
    "content": "#[cfg(feature = \"auth_jwt\")]\npub mod jwt;\n"
  },
  {
    "path": "src/auth/snapshots/loco_rs__auth__jwt__tests__token expired.snap",
    "content": "---\nsource: src/auth/jwt.rs\nexpression: jwt.validate(&token)\n---\nErr(\n    Error(\n        ExpiredSignature,\n    ),\n)\n"
  },
  {
    "path": "src/auth/snapshots/loco_rs__auth__jwt__tests__valid token and custom array claims.snap",
    "content": "---\nsource: src/auth/jwt.rs\nexpression: jwt.validate(&token)\n---\nOk(\n    TokenData {\n        header: Header {\n            typ: Some(\n                \"JWT\",\n            ),\n            alg: HS512,\n            cty: None,\n            jku: None,\n            jwk: None,\n            kid: None,\n            x5u: None,\n            x5c: None,\n            x5t: None,\n            x5t_s256: None,\n        },\n        claims: UserClaims {\n            pid: \"pid\",\n            exp: EXP,\n            claims: {\n                \"array\": Array [\n                    Number(1),\n                    Number(2),\n                    Number(3),\n                ],\n            },\n        },\n    },\n)\n"
  },
  {
    "path": "src/auth/snapshots/loco_rs__auth__jwt__tests__valid token and custom boolean claims.snap",
    "content": "---\nsource: src/auth/jwt.rs\nexpression: jwt.validate(&token)\n---\nOk(\n    TokenData {\n        header: Header {\n            typ: Some(\n                \"JWT\",\n            ),\n            alg: HS512,\n            cty: None,\n            jku: None,\n            jwk: None,\n            kid: None,\n            x5u: None,\n            x5c: None,\n            x5t: None,\n            x5t_s256: None,\n        },\n        claims: UserClaims {\n            pid: \"pid\",\n            exp: EXP,\n            claims: {\n                \"custom\": Bool(true),\n            },\n        },\n    },\n)\n"
  },
  {
    "path": "src/auth/snapshots/loco_rs__auth__jwt__tests__valid token and custom nested array claims.snap",
    "content": "---\nsource: src/auth/jwt.rs\nexpression: jwt.validate(&token)\n---\nOk(\n    TokenData {\n        header: Header {\n            typ: Some(\n                \"JWT\",\n            ),\n            alg: HS512,\n            cty: None,\n            jku: None,\n            jwk: None,\n            kid: None,\n            x5u: None,\n            x5c: None,\n            x5t: None,\n            x5t_s256: None,\n        },\n        claims: UserClaims {\n            pid: \"pid\",\n            exp: EXP,\n            claims: {\n                \"level1\": Object {\n                    \"level2\": Object {\n                        \"level3\": Array [\n                            Number(1),\n                            Number(2),\n                            Number(3),\n                        ],\n                    },\n                },\n            },\n        },\n    },\n)\n"
  },
  {
    "path": "src/auth/snapshots/loco_rs__auth__jwt__tests__valid token and custom nested claims.snap",
    "content": "---\nsource: src/auth/jwt.rs\nexpression: jwt.validate(&token)\n---\nOk(\n    TokenData {\n        header: Header {\n            typ: Some(\n                \"JWT\",\n            ),\n            alg: HS512,\n            cty: None,\n            jku: None,\n            jwk: None,\n            kid: None,\n            x5u: None,\n            x5c: None,\n            x5t: None,\n            x5t_s256: None,\n        },\n        claims: UserClaims {\n            pid: \"pid\",\n            exp: EXP,\n            claims: {\n                \"level1\": Object {\n                    \"level2\": Object {\n                        \"level3\": String(\"claim\"),\n                    },\n                },\n            },\n        },\n    },\n)\n"
  },
  {
    "path": "src/auth/snapshots/loco_rs__auth__jwt__tests__valid token and custom number claims.snap",
    "content": "---\nsource: src/auth/jwt.rs\nexpression: jwt.validate(&token)\n---\nOk(\n    TokenData {\n        header: Header {\n            typ: Some(\n                \"JWT\",\n            ),\n            alg: HS512,\n            cty: None,\n            jku: None,\n            jwk: None,\n            kid: None,\n            x5u: None,\n            x5c: None,\n            x5t: None,\n            x5t_s256: None,\n        },\n        claims: UserClaims {\n            pid: \"pid\",\n            exp: EXP,\n            claims: {\n                \"custom\": Number(123),\n            },\n        },\n    },\n)\n"
  },
  {
    "path": "src/auth/snapshots/loco_rs__auth__jwt__tests__valid token and custom string claims.snap",
    "content": "---\nsource: src/auth/jwt.rs\nexpression: jwt.validate(&token)\n---\nOk(\n    TokenData {\n        header: Header {\n            typ: Some(\n                \"JWT\",\n            ),\n            alg: HS512,\n            cty: None,\n            jku: None,\n            jwk: None,\n            kid: None,\n            x5u: None,\n            x5c: None,\n            x5t: None,\n            x5t_s256: None,\n        },\n        claims: UserClaims {\n            pid: \"pid\",\n            exp: EXP,\n            claims: {\n                \"custom\": String(\"claim\"),\n            },\n        },\n    },\n)\n"
  },
  {
    "path": "src/auth/snapshots/loco_rs__auth__jwt__tests__valid token.snap",
    "content": "---\nsource: src/auth/jwt.rs\nexpression: jwt.validate(&token)\n---\nOk(\n    TokenData {\n        header: Header {\n            typ: Some(\n                \"JWT\",\n            ),\n            alg: HS512,\n            cty: None,\n            jku: None,\n            jwk: None,\n            kid: None,\n            x5u: None,\n            x5c: None,\n            x5t: None,\n            x5t_s256: None,\n        },\n        claims: UserClaims {\n            pid: \"pid\",\n            exp: EXP,\n            claims: {},\n        },\n    },\n)\n"
  },
  {
    "path": "src/banner.rs",
    "content": "use colored::Colorize;\n\nuse crate::boot::{BootResult, ServeParams};\n\npub const BANNER: &str = r\"\n                      ▄     ▀                     \n                                 ▀  ▄             \n                  ▄       ▀     ▄  ▄ ▄▀           \n                                    ▄ ▀▄▄         \n                        ▄     ▀    ▀  ▀▄▀█▄       \n                                          ▀█▄     \n▄▄▄▄▄▄▄  ▄▄▄▄▄▄▄▄▄   ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄ ▀▀█    \n ██████  █████   ███ █████   ███ █████   ███ ▀█   \n ██████  █████   ███ █████   ▀▀▀ █████   ███ ▄█▄  \n ██████  █████   ███ █████       █████   ███ ████▄\n ██████  █████   ███ █████   ▄▄▄ █████   ███ █████\n ██████  █████   ███  ████   ███ █████   ███ ████▀\n   ▀▀▀██▄ ▀▀▀▀▀▀▀▀▀▀  ▀▀▀▀▀▀▀▀▀▀  ▀▀▀▀▀▀▀▀▀▀ ██▀  \n       ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀    \n                https://loco.rs\n\";\n\npub fn print_banner(boot_result: &BootResult, server_config: &ServeParams) {\n    let ctx = &boot_result.app_context;\n    println!(\"{BANNER}\");\n    let config = &ctx.config;\n\n    println!(\"environment: {}\", ctx.environment.to_string().green());\n\n    #[cfg(feature = \"with-db\")]\n    {\n        let mut database = Vec::new();\n        if config.database.enable_logging {\n            database.push(\"logging\".green());\n        }\n        if config.database.auto_migrate {\n            database.push(\"automigrate\".yellow());\n        }\n        if config.database.dangerously_recreate {\n            database.push(\"recreate\".bright_red());\n        }\n        if config.database.dangerously_truncate {\n            database.push(\"truncate\".bright_red());\n        }\n\n        if !database.is_empty() {\n            println!(\n                \"   database: {}\",\n                database\n                    .iter()\n                    .map(ToString::to_string)\n                    .collect::<Vec<_>>()\n                    .join(\", \")\n            );\n        }\n    }\n    if config.logger.enable {\n        println!(\"     logger: {}\", config.logger.level.to_string().green());\n    } else {\n        println!(\"     logger: {}\", \"disabled\".bright_red());\n    }\n    if cfg!(debug_assertions) {\n        println!(\"compilation: {}\", \"debug\".bright_red());\n    } else {\n        println!(\"compilation: {}\", \"release\".green());\n    }\n\n    let mut modes = Vec::new();\n    let mut servingline = Vec::new();\n    if boot_result.router.is_some() {\n        modes.push(\"server\".green());\n        servingline.push(format!(\n            \"listening on http://{}:{}\",\n            server_config.binding.clone().green(),\n            server_config.port.to_string().green()\n        ));\n    }\n    if let Some(tags) = &boot_result.worker {\n        modes.push(\"worker\".green());\n        let status = format!(\"worker is {}\", \"online\".green());\n        if tags.is_empty() {\n            servingline.push(status);\n        } else {\n            servingline.push(format!(\"{status} with tags: {}\", tags.join(\",\")));\n        }\n    }\n    if boot_result.run_scheduler {\n        modes.push(\"scheduler\".green());\n        servingline.push(format!(\"scheduler is {}\", \"running\".green()));\n    }\n    if !modes.is_empty() {\n        println!(\n            \"      modes: {}\",\n            modes\n                .iter()\n                .map(ToString::to_string)\n                .collect::<Vec<_>>()\n                .join(\", \")\n        );\n    }\n\n    println!();\n    println!(\"{}\", servingline.join(\"\\n\"));\n}\n"
  },
  {
    "path": "src/bgworker/mod.rs",
    "content": "use std::{\n    fs::File,\n    io::Write,\n    path::{Path, PathBuf},\n    sync::Arc,\n};\n\nuse async_trait::async_trait;\n#[cfg(feature = \"cli\")]\nuse clap::ValueEnum;\nuse serde::{Deserialize, Serialize};\nuse serde_variant::to_variant_name;\n#[cfg(feature = \"bg_pg\")]\npub mod pg;\n#[cfg(feature = \"bg_redis\")]\npub mod redis;\n#[cfg(feature = \"bg_sqlt\")]\npub mod sqlt;\n\nuse crate::{\n    app::AppContext,\n    config::{\n        self, Config, PostgresQueueConfig, QueueConfig, RedisQueueConfig, SqliteQueueConfig,\n        WorkerMode,\n    },\n    Error, Result,\n};\n\n#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]\n#[cfg_attr(feature = \"cli\", derive(ValueEnum))]\npub enum JobStatus {\n    #[serde(rename = \"queued\")]\n    Queued,\n    #[serde(rename = \"processing\")]\n    Processing,\n    #[serde(rename = \"completed\")]\n    Completed,\n    #[serde(rename = \"failed\")]\n    Failed,\n    #[serde(rename = \"cancelled\")]\n    Cancelled,\n}\n\nimpl std::str::FromStr for JobStatus {\n    type Err = String;\n\n    fn from_str(s: &str) -> Result<Self, Self::Err> {\n        match s {\n            \"queued\" => Ok(Self::Queued),\n            \"processing\" => Ok(Self::Processing),\n            \"completed\" => Ok(Self::Completed),\n            \"failed\" => Ok(Self::Failed),\n            \"cancelled\" => Ok(Self::Cancelled),\n            _ => Err(format!(\"Invalid status: {s}\")),\n        }\n    }\n}\n\nimpl std::fmt::Display for JobStatus {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        to_variant_name(self).expect(\"only enum supported\").fmt(f)\n    }\n}\n\n// Queue struct now holds both a QueueProvider and QueueRegistrar\npub enum Queue {\n    #[cfg(feature = \"bg_redis\")]\n    Redis(\n        redis::RedisPool,\n        Arc<tokio::sync::Mutex<redis::JobRegistry>>,\n        redis::RunOpts,\n        tokio_util::sync::CancellationToken,\n    ),\n    #[cfg(feature = \"bg_pg\")]\n    Postgres(\n        pg::PgPool,\n        std::sync::Arc<tokio::sync::Mutex<pg::JobRegistry>>,\n        pg::RunOpts,\n        tokio_util::sync::CancellationToken,\n    ),\n    #[cfg(feature = \"bg_sqlt\")]\n    Sqlite(\n        sqlt::SqlitePool,\n        std::sync::Arc<tokio::sync::Mutex<sqlt::JobRegistry>>,\n        sqlt::RunOpts,\n        tokio_util::sync::CancellationToken,\n    ),\n    None,\n}\n\nimpl Queue {\n    /// Add a job to the queue\n    ///\n    /// # Errors\n    ///\n    /// This function will return an error if fails\n    #[allow(unused_variables)]\n    pub async fn enqueue<A: Serialize + Send + Sync>(\n        &self,\n        class: String,\n        queue: Option<String>,\n        args: A,\n        tags: Option<Vec<String>>,\n    ) -> Result<()> {\n        tracing::debug!(worker = class, queue = ?queue, tags = ?tags, \"Enqueuing background job\");\n        match self {\n            #[cfg(feature = \"bg_redis\")]\n            Self::Redis(pool, _, _, _) => {\n                redis::enqueue(pool, class, queue, args, tags).await?;\n            }\n            #[cfg(feature = \"bg_pg\")]\n            Self::Postgres(pool, _, _, _) => {\n                pg::enqueue(\n                    pool,\n                    &class,\n                    serde_json::to_value(args)?,\n                    chrono::Utc::now(),\n                    None,\n                    tags,\n                )\n                .await\n                .map_err(Box::from)?;\n            }\n            #[cfg(feature = \"bg_sqlt\")]\n            Self::Sqlite(pool, _, _, _) => {\n                sqlt::enqueue(\n                    pool,\n                    &class,\n                    serde_json::to_value(args)?,\n                    chrono::Utc::now(),\n                    None,\n                    tags,\n                )\n                .await\n                .map_err(Box::from)?;\n            }\n            _ => {}\n        }\n        Ok(())\n    }\n\n    /// Register a worker\n    ///\n    /// # Errors\n    ///\n    /// This function will return an error if fails\n    #[allow(unused_variables)]\n    pub async fn register<\n        A: Serialize + Send + Sync + 'static + for<'de> serde::Deserialize<'de>,\n        W: BackgroundWorker<A> + 'static,\n    >(\n        &self,\n        worker: W,\n    ) -> Result<()> {\n        tracing::info!(worker = W::class_name(), \"Registering background worker\");\n        match self {\n            #[cfg(feature = \"bg_redis\")]\n            Self::Redis(_, p, _, _) => {\n                let mut p = p.lock().await;\n                p.register_worker(W::class_name(), worker)?;\n            }\n            #[cfg(feature = \"bg_pg\")]\n            Self::Postgres(_, registry, _, _) => {\n                let mut r = registry.lock().await;\n                r.register_worker(W::class_name(), worker)?;\n            }\n            #[cfg(feature = \"bg_sqlt\")]\n            Self::Sqlite(_, registry, _, _) => {\n                let mut r = registry.lock().await;\n                r.register_worker(W::class_name(), worker)?;\n            }\n            _ => {}\n        }\n        Ok(())\n    }\n\n    /// Runs the worker loop for this [`Queue`].\n    ///\n    /// # Errors\n    ///\n    /// This function will return an error if fails\n    #[allow(unused_variables)]\n    pub async fn run(&self, tags: Vec<String>) -> Result<()> {\n        tracing::info!(\"Starting background job processing\");\n        match self {\n            #[cfg(feature = \"bg_redis\")]\n            Self::Redis(pool, registry, run_opts, token) => {\n                let handles = registry\n                    .lock()\n                    .await\n                    .run(pool, run_opts, &token.clone(), &tags);\n                Self::process_worker_handles(handles).await?;\n            }\n            #[cfg(feature = \"bg_pg\")]\n            Self::Postgres(pool, registry, run_opts, token) => {\n                let handles = registry\n                    .lock()\n                    .await\n                    .run(pool, run_opts, &token.clone(), &tags);\n                Self::process_worker_handles(handles).await?;\n            }\n            #[cfg(feature = \"bg_sqlt\")]\n            Self::Sqlite(pool, registry, run_opts, token) => {\n                let handles = registry\n                    .lock()\n                    .await\n                    .run(pool, run_opts, &token.clone(), &tags);\n                Self::process_worker_handles(handles).await?;\n            }\n            _ => {\n                tracing::error!(\n                    \"No queue provider is configured: compile with at least one queue provider feature\"\n                );\n            }\n        }\n        Ok(())\n    }\n\n    /// Process worker task handles and handle any errors\n    ///\n    /// # Errors\n    /// This function will return an error if a worker task fails to join\n    #[allow(dead_code)]\n    async fn process_worker_handles(handles: Vec<tokio::task::JoinHandle<()>>) -> Result<()> {\n        let handle_count = handles.len();\n        tracing::debug!(worker_count = handle_count, \"Processing worker handles\");\n\n        for (index, handle) in handles.into_iter().enumerate() {\n            if let Err(e) = handle.await {\n                if e.is_cancelled() {\n                    tracing::debug!(\n                        worker_index = index,\n                        \"Worker task cancelled during shutdown\"\n                    );\n                } else if e.is_panic() {\n                    tracing::error!(worker_index = index, \"Worker task panicked\");\n                    std::panic::resume_unwind(e.into_panic());\n                } else {\n                    tracing::error!(worker_index = index, error = ?e, \"Worker task failed to join\");\n                    return Err(crate::Error::Worker(format!(\"Worker join error: {e}\")));\n                }\n            }\n        }\n        tracing::info!(\n            worker_count = handle_count,\n            \"All worker tasks finished successfully\"\n        );\n        Ok(())\n    }\n\n    /// Runs the setup of this [`Queue`].\n    ///\n    /// # Errors\n    ///\n    /// This function will return an error if fails\n    pub async fn setup(&self) -> Result<()> {\n        match self {\n            #[cfg(feature = \"bg_redis\")]\n            Self::Redis(_, _, _, _) => {}\n            #[cfg(feature = \"bg_pg\")]\n            Self::Postgres(pool, _, _, _) => {\n                pg::initialize_database(pool).await.map_err(Box::from)?;\n            }\n            #[cfg(feature = \"bg_sqlt\")]\n            Self::Sqlite(pool, _, _, _) => {\n                sqlt::initialize_database(pool).await.map_err(Box::from)?;\n            }\n            _ => {}\n        }\n        Ok(())\n    }\n\n    /// Performs clear on this [`Queue`].\n    ///\n    /// # Errors\n    ///\n    /// This function will return an error if fails\n    pub async fn clear(&self) -> Result<()> {\n        tracing::info!(\"Clearing all jobs from queue\");\n        match self {\n            #[cfg(feature = \"bg_redis\")]\n            Self::Redis(pool, _, _, _) => {\n                redis::clear(pool).await?;\n            }\n            #[cfg(feature = \"bg_pg\")]\n            Self::Postgres(pool, _, _, _) => {\n                pg::clear(pool).await.map_err(Box::from)?;\n            }\n            #[cfg(feature = \"bg_sqlt\")]\n            Self::Sqlite(pool, _, _, _) => {\n                sqlt::clear(pool).await.map_err(Box::from)?;\n            }\n            _ => {}\n        }\n        Ok(())\n    }\n\n    /// Returns a ping of this [`Queue`].\n    ///\n    /// # Errors\n    ///\n    /// This function will return an error if fails\n    pub async fn ping(&self) -> Result<()> {\n        tracing::trace!(\"Pinging job queue\");\n        match self {\n            #[cfg(feature = \"bg_redis\")]\n            Self::Redis(pool, _, _, _) => {\n                redis::ping(pool).await?;\n            }\n            #[cfg(feature = \"bg_pg\")]\n            Self::Postgres(pool, _, _, _) => {\n                pg::ping(pool).await.map_err(Box::from)?;\n            }\n            #[cfg(feature = \"bg_sqlt\")]\n            Self::Sqlite(pool, _, _, _) => {\n                sqlt::ping(pool).await.map_err(Box::from)?;\n            }\n            _ => {}\n        }\n        Ok(())\n    }\n\n    #[must_use]\n    pub fn describe(&self) -> String {\n        match self {\n            #[cfg(feature = \"bg_redis\")]\n            Self::Redis(_, _, _, _) => \"redis queue\".to_string(),\n            #[cfg(feature = \"bg_pg\")]\n            Self::Postgres(_, _, _, _) => \"postgres queue\".to_string(),\n            #[cfg(feature = \"bg_sqlt\")]\n            Self::Sqlite(_, _, _, _) => \"sqlite queue\".to_string(),\n            _ => \"no queue\".to_string(),\n        }\n    }\n\n    /// # Errors\n    ///\n    /// Does not currently return an error, but the postgres or other future\n    /// queue implementations might, so using Result here as return type.\n    pub fn shutdown(&self) -> Result<()> {\n        tracing::info!(\"Shutting down background job processing\");\n        match self {\n            #[cfg(feature = \"bg_redis\")]\n            Self::Redis(_, _, _, cancellation_token) => cancellation_token.cancel(),\n            #[cfg(feature = \"bg_pg\")]\n            Self::Postgres(_, _, _, cancellation_token) => cancellation_token.cancel(),\n            #[cfg(feature = \"bg_sqlt\")]\n            Self::Sqlite(_, _, _, cancellation_token) => cancellation_token.cancel(),\n            _ => {}\n        }\n\n        Ok(())\n    }\n\n    async fn get_jobs(\n        &self,\n        status: Option<&Vec<JobStatus>>,\n        age_days: Option<i64>,\n    ) -> Result<serde_json::Value> {\n        tracing::info!(status = ?status, age_days = ?age_days, \"Retrieving jobs\");\n        match self {\n            #[cfg(feature = \"bg_pg\")]\n            Self::Postgres(pool, _, _, _) => {\n                let jobs = pg::get_jobs(pool, status, age_days)\n                    .await\n                    .map_err(Box::from)?;\n                Ok(serde_json::to_value(jobs)?)\n            }\n            #[cfg(feature = \"bg_sqlt\")]\n            Self::Sqlite(pool, _, _, _) => {\n                let jobs = sqlt::get_jobs(pool, status, age_days)\n                    .await\n                    .map_err(Box::from)?;\n\n                Ok(serde_json::to_value(jobs)?)\n            }\n            #[cfg(feature = \"bg_redis\")]\n            Self::Redis(pool, _, _, _) => {\n                let jobs = redis::get_jobs(pool, status, age_days).await?;\n                Ok(serde_json::to_value(jobs)?)\n            }\n            Self::None => {\n                tracing::error!(\n                    \"No queue provider is configured: compile with at least one queue provider feature\"\n                );\n                Err(Error::string(\"provider not configured\"))\n            }\n        }\n    }\n\n    /// Cancels jobs based on the given job name for the configured queue provider.\n    ///\n    /// # Errors\n    /// - If no queue provider is configured, it will return an error indicating the lack of configuration.\n    /// - If the Redis provider is selected, it will return an error stating that cancellation is not supported.\n    /// - Any error in the underlying provider's cancellation logic will propagate from the respective function.\n    ///\n    pub async fn cancel_jobs(&self, job_name: &str) -> Result<()> {\n        tracing::info!(job_name = job_name, \"Cancelling jobs by name\");\n\n        match self {\n            #[cfg(feature = \"bg_pg\")]\n            Self::Postgres(pool, _, _, _) => pg::cancel_jobs_by_name(pool, job_name).await,\n            #[cfg(feature = \"bg_sqlt\")]\n            Self::Sqlite(pool, _, _, _) => sqlt::cancel_jobs_by_name(pool, job_name).await,\n            #[cfg(feature = \"bg_redis\")]\n            Self::Redis(pool, _, _, _) => redis::cancel_jobs_by_name(pool, job_name).await,\n            Self::None => {\n                tracing::error!(\n                    \"No queue provider is configured: compile with at least one queue provider feature\"\n                );\n                Err(Error::string(\"provider not configured\"))\n            }\n        }\n    }\n\n    /// Clears jobs older than a specified number of days for the configured queue provider.\n    ///\n    /// # Errors\n    /// - If no queue provider is configured, it will return an error indicating the lack of configuration.\n    /// - If the Redis provider is selected, it will return an error stating that clearing jobs is not supported.\n    /// - Any error in the underlying provider's job clearing logic will propagate from the respective function.\n    ///\n    pub async fn clear_jobs_older_than(\n        &self,\n        age_days: i64,\n        status: &Vec<JobStatus>,\n    ) -> Result<()> {\n        tracing::info!(age_days = age_days, status = ?status, \"Clearing older jobs\");\n\n        match self {\n            #[cfg(feature = \"bg_pg\")]\n            Self::Postgres(pool, _, _, _) => {\n                pg::clear_jobs_older_than(pool, age_days, Some(status)).await\n            }\n            #[cfg(feature = \"bg_sqlt\")]\n            Self::Sqlite(pool, _, _, _) => {\n                sqlt::clear_jobs_older_than(pool, age_days, Some(status)).await\n            }\n            #[cfg(feature = \"bg_redis\")]\n            Self::Redis(pool, _, _, _) => {\n                redis::clear_jobs_older_than(pool, age_days, Some(status)).await\n            }\n            Self::None => {\n                tracing::error!(\n                    \"No queue provider is configured: compile with at least one queue provider feature\"\n                );\n                Err(Error::string(\"provider not configured\"))\n            }\n        }\n    }\n\n    /// Clears jobs based on their status for the configured queue provider.\n    ///\n    /// # Errors\n    /// - If no queue provider is configured, it will return an error indicating the lack of configuration.\n    /// - If the Redis provider is selected, it will return an error stating that clearing jobs is not supported.\n    /// - Any error in the underlying provider's job clearing logic will propagate from the respective function.\n    pub async fn clear_by_status(&self, status: Vec<JobStatus>) -> Result<()> {\n        tracing::info!(status = ?status, \"Clearing jobs by status\");\n        match self {\n            #[cfg(feature = \"bg_pg\")]\n            Self::Postgres(pool, _, _, _) => pg::clear_by_status(pool, status).await,\n            #[cfg(feature = \"bg_sqlt\")]\n            Self::Sqlite(pool, _, _, _) => sqlt::clear_by_status(pool, status).await,\n            #[cfg(feature = \"bg_redis\")]\n            Self::Redis(pool, _, _, _) => redis::clear_by_status(pool, status).await,\n            Self::None => {\n                tracing::error!(\n                    \"No queue provider is configured: compile with at least one queue provider feature\"\n                );\n                Err(Error::string(\"provider not configured\"))\n            }\n        }\n    }\n\n    /// Requeued job with the given minutes ages.\n    ///\n    /// # Errors\n    /// - If no queue provider is configured, it will return an error indicating the lack of configuration.\n    /// - If the Redis provider is selected, it will return an error stating that clearing jobs is not supported.\n    /// - Any error in the underlying provider's job clearing logic will propagate from the respective function.\n    pub async fn requeue(&self, age_minutes: &i64) -> Result<()> {\n        tracing::info!(age_minutes = age_minutes, \"Requeuing stale jobs\");\n        match self {\n            #[cfg(feature = \"bg_pg\")]\n            Self::Postgres(pool, _, _, _) => pg::requeue(pool, age_minutes).await,\n            #[cfg(feature = \"bg_sqlt\")]\n            Self::Sqlite(pool, _, _, _) => sqlt::requeue(pool, age_minutes).await,\n            #[cfg(feature = \"bg_redis\")]\n            Self::Redis(pool, _, _, _) => redis::requeue(pool, age_minutes).await,\n            Self::None => {\n                tracing::error!(\n                    \"No queue provider is configured: compile with at least one queue provider feature\"\n                );\n                Err(Error::string(\"provider not configured\"))\n            }\n        }\n    }\n\n    /// Dumps the list of jobs to a YAML file at the specified path.\n    ///\n    /// This function retrieves jobs from the queue, optionally filtered by their status, and\n    /// writes the job data to a YAML file.\n    ///\n    /// # Errors\n    /// - If the specified path cannot be created, an error will be returned.\n    /// - If the job retrieval or YAML serialization fails, an error will be returned.\n    /// - If there is an issue creating the dump file, an error will be returned\n    pub async fn dump(\n        &self,\n        path: &Path,\n        status: Option<&Vec<JobStatus>>,\n        age_days: Option<i64>,\n    ) -> Result<PathBuf> {\n        tracing::info!(path = %path.display(), status = ?status, age_days = ?age_days, \"Dumping jobs to file\");\n\n        if !path.exists() {\n            tracing::debug!(path = %path.display(), \"Directory does not exist, creating...\");\n            std::fs::create_dir_all(path)?;\n        }\n\n        let dump_file = path.join(format!(\n            \"loco-dump-jobs-{}.yaml\",\n            chrono::Utc::now().format(\"%Y-%m-%d-%H-%M-%S\")\n        ));\n\n        let jobs = self.get_jobs(status, age_days).await?;\n\n        let data = serde_yaml::to_string(&jobs)?;\n        let mut file = File::create(&dump_file)?;\n        file.write_all(data.as_bytes())?;\n\n        tracing::info!(file = %dump_file.display(), \"Jobs successfully dumped to file\");\n        Ok(dump_file)\n    }\n\n    /// Imports jobs from a YAML file into the configured queue provider.\n    ///\n    /// This function reads job data from a YAML file located at the specified `path` and imports\n    /// the jobs into the queue.\n    ///\n    /// # Errors\n    /// - If there is an issue opening or reading the YAML file, an error will be returned.\n    /// - If the queue provider is Redis or none, an error will be returned indicating the lack of support.\n    /// - If any issues occur while enqueuing the jobs, the function will return an error.\n    ///\n    pub async fn import(&self, path: &Path) -> Result<()> {\n        tracing::info!(path = %path.display(), \"Importing jobs from file\");\n\n        match &self {\n            #[cfg(feature = \"bg_pg\")]\n            Self::Postgres(_, _, _, _) => {\n                let jobs: Vec<pg::Job> = serde_yaml::from_reader(File::open(path)?)?;\n                for job in jobs {\n                    self.enqueue(job.name.clone(), None, job.data, None).await?;\n                }\n\n                Ok(())\n            }\n            #[cfg(feature = \"bg_sqlt\")]\n            Self::Sqlite(_, _, _, _) => {\n                let jobs: Vec<sqlt::Job> = serde_yaml::from_reader(File::open(path)?)?;\n                for job in jobs {\n                    self.enqueue(job.name.clone(), None, job.data, None).await?;\n                }\n                Ok(())\n            }\n            #[cfg(feature = \"bg_redis\")]\n            Self::Redis(_, _, _, _) => {\n                let jobs: Vec<redis::Job> = serde_yaml::from_reader(File::open(path)?)?;\n                for job in jobs {\n                    self.enqueue(job.name.clone(), None, job.data, None).await?;\n                }\n                Ok(())\n            }\n            Self::None => {\n                tracing::error!(\n                    \"No queue provider is configured: compile with at least one queue provider feature\"\n                );\n                Err(Error::string(\"provider not configured\"))\n            }\n        }\n    }\n}\n\n#[async_trait]\npub trait BackgroundWorker<A: Send + Sync + serde::Serialize + 'static>: Send + Sync {\n    /// If you have a specific queue\n    /// in mind and the provider supports custom / priority queues, make your\n    /// worker return it. Otherwise, return `None`.\n    #[must_use]\n    fn queue() -> Option<String> {\n        None\n    }\n\n    /// Specifies tags associated with this worker. Workers might only process jobs\n    /// matching specific tags during startup.\n    #[must_use]\n    fn tags() -> Vec<String> {\n        Vec::new()\n    }\n\n    fn build(ctx: &AppContext) -> Self;\n    #[must_use]\n    fn class_name() -> String\n    where\n        Self: Sized,\n    {\n        use heck::ToUpperCamelCase;\n        let type_name = std::any::type_name::<Self>();\n        let name = type_name.split(\"::\").last().unwrap_or(type_name);\n        name.to_upper_camel_case()\n    }\n    async fn perform_later(ctx: &AppContext, args: A) -> crate::Result<()>\n    where\n        Self: Sized,\n    {\n        match &ctx.config.workers.mode {\n            WorkerMode::BackgroundQueue => {\n                if let Some(p) = &ctx.queue_provider {\n                    let tags = Self::tags();\n                    let tags_option = if tags.is_empty() { None } else { Some(tags) };\n                    p.enqueue(Self::class_name(), Self::queue(), args, tags_option)\n                        .await?;\n                } else {\n                    tracing::error!(\n                        \"perform_later: background queue is selected, but queue was not populated \\\n                         in context\"\n                    );\n                }\n            }\n            WorkerMode::ForegroundBlocking => {\n                Self::build(ctx).perform(args).await?;\n            }\n            WorkerMode::BackgroundAsync => {\n                let dx = ctx.clone();\n                tokio::spawn(async move {\n                    if let Err(err) = Self::build(&dx).perform(args).await {\n                        tracing::error!(err = err.to_string(), \"worker failed to perform job\");\n                    }\n                });\n            }\n        }\n        Ok(())\n    }\n\n    async fn perform(&self, args: A) -> crate::Result<()>;\n}\n\n/// Initialize the system according to configuration\n///\n/// # Errors\n///\n/// This function will return an error if it fails\npub async fn converge(queue: &Queue, config: &QueueConfig) -> Result<()> {\n    queue.setup().await?;\n    match config {\n        QueueConfig::Postgres(PostgresQueueConfig {\n            dangerously_flush,\n            uri: _,\n            max_connections: _,\n            enable_logging: _,\n            connect_timeout: _,\n            idle_timeout: _,\n            poll_interval_sec: _,\n            num_workers: _,\n            min_connections: _,\n        })\n        | QueueConfig::Sqlite(SqliteQueueConfig {\n            dangerously_flush,\n            uri: _,\n            max_connections: _,\n            enable_logging: _,\n            connect_timeout: _,\n            idle_timeout: _,\n            poll_interval_sec: _,\n            num_workers: _,\n            min_connections: _,\n        })\n        | QueueConfig::Redis(RedisQueueConfig {\n            dangerously_flush,\n            uri: _,\n            queues: _,\n            num_workers: _,\n        }) => {\n            if *dangerously_flush {\n                tracing::warn!(\"Flush mode enabled - clearing all jobs from queue\");\n                queue.clear().await?;\n            }\n        }\n    }\n    Ok(())\n}\n\n/// Create a provider\n///\n/// # Errors\n///\n/// This function will return an error if fails to build\n#[allow(clippy::missing_panics_doc)]\npub async fn create_queue_provider(config: &Config) -> Result<Option<Arc<Queue>>> {\n    if config.workers.mode == config::WorkerMode::BackgroundQueue {\n        if let Some(queue) = &config.queue {\n            match queue {\n                #[cfg(feature = \"bg_redis\")]\n                config::QueueConfig::Redis(qcfg) => {\n                    tracing::debug!(\"Creating Redis queue provider\");\n                    Ok(Some(Arc::new(redis::create_provider(qcfg).await?)))\n                }\n                #[cfg(feature = \"bg_pg\")]\n                config::QueueConfig::Postgres(qcfg) => {\n                    tracing::debug!(\"Creating Postgres queue provider\");\n                    Ok(Some(Arc::new(pg::create_provider(qcfg).await?)))\n                }\n                #[cfg(feature = \"bg_sqlt\")]\n                config::QueueConfig::Sqlite(qcfg) => {\n                    tracing::debug!(\"Creating SQLite queue provider\");\n                    Ok(Some(Arc::new(sqlt::create_provider(qcfg).await?)))\n                }\n\n                #[allow(unreachable_patterns)]\n                _ => Err(Error::string(\n                    \"No queue provider feature was selected and compiled, but queue configuration \\\n                     is present\",\n                )),\n            }\n        } else {\n            // tracing::warn!(\"Worker mode is BackgroundQueue but no queue configuration is present\");\n            Ok(None)\n        }\n    } else {\n        // tracing::debug!(\"Worker mode is not BackgroundQueue, skipping queue provider creation\");\n        Ok(None)\n    }\n}\n\n#[cfg(test)]\nmod tests {\n\n    use std::path::Path;\n\n    use insta::assert_debug_snapshot;\n\n    use super::*;\n    use crate::tests_cfg;\n\n    fn sqlite_config(db_path: &Path) -> SqliteQueueConfig {\n        SqliteQueueConfig {\n            uri: format!(\n                \"sqlite://{}?mode=rwc\",\n                db_path.join(\"sample.sqlite\").display()\n            ),\n            dangerously_flush: false,\n            enable_logging: false,\n            max_connections: 1,\n            min_connections: 1,\n            connect_timeout: 500,\n            idle_timeout: 500,\n            poll_interval_sec: 1,\n            num_workers: 1,\n        }\n    }\n\n    #[tokio::test]\n    async fn can_dump_jobs() {\n        let tree_fs = tree_fs::TreeBuilder::default()\n            .drop(true)\n            .create()\n            .expect(\"create temp folder\");\n        let qcfg = sqlite_config(tree_fs.root.as_path());\n        let queue = sqlt::create_provider(&qcfg)\n            .await\n            .expect(\"create sqlite queue\");\n\n        let pool = sqlx::SqlitePool::connect(&qcfg.uri)\n            .await\n            .expect(\"connect to sqlite db\");\n\n        queue.setup().await.expect(\"setup sqlite db\");\n        tests_cfg::queue::sqlite_seed_data(&pool).await;\n\n        let dump_file = queue\n            .dump(\n                tree_fs.root.as_path(),\n                Some(&vec![JobStatus::Failed, JobStatus::Cancelled]),\n                None,\n            )\n            .await\n            .expect(\"dump jobs\");\n\n        assert_debug_snapshot!(std::fs::read_to_string(dump_file).unwrap());\n    }\n\n    #[tokio::test]\n    async fn cat_import_jobs_form_file() {\n        let tree_fs = tree_fs::TreeBuilder::default()\n            .drop(true)\n            .create()\n            .expect(\"create temp folder\");\n        let qcfg = sqlite_config(tree_fs.root.as_path());\n        let queue = sqlt::create_provider(&qcfg)\n            .await\n            .expect(\"create sqlite queue\");\n\n        let pool = sqlx::SqlitePool::connect(&qcfg.uri)\n            .await\n            .expect(\"connect to sqlite db\");\n\n        queue.setup().await.expect(\"setup sqlite db\");\n\n        let count: i64 = sqlx::query_scalar(\"SELECT COUNT(*) FROM sqlt_loco_queue\")\n            .fetch_one(&pool)\n            .await\n            .unwrap();\n\n        assert_eq!(count, 0);\n\n        queue\n            .import(\n                PathBuf::from(\"tests\")\n                    .join(\"fixtures\")\n                    .join(\"queue\")\n                    .join(\"jobs.yaml\")\n                    .as_path(),\n            )\n            .await\n            .expect(\"dump import\");\n\n        let count: i64 = sqlx::query_scalar(\"SELECT COUNT(*) FROM sqlt_loco_queue\")\n            .fetch_one(&pool)\n            .await\n            .unwrap();\n\n        assert_eq!(count, 14);\n    }\n}\n"
  },
  {
    "path": "src/bgworker/pg.rs",
    "content": "/// Postgres based background job queue provider\nuse std::{\n    collections::HashMap, future::Future, panic::AssertUnwindSafe, pin::Pin, sync::Arc,\n    time::Duration,\n};\n\nuse super::{BackgroundWorker, JobStatus, Queue};\nuse crate::{config::PostgresQueueConfig, Error, Result};\nuse chrono::{DateTime, Utc};\nuse futures_util::FutureExt;\nuse serde::{Deserialize, Serialize};\nuse serde_json::Value as JsonValue;\npub use sqlx::PgPool;\nuse sqlx::{\n    postgres::{PgConnectOptions, PgPoolOptions, PgRow},\n    ConnectOptions, Row,\n};\nuse std::fmt::Write;\nuse tokio::{task::JoinHandle, time::sleep};\nuse tokio_util::sync::CancellationToken;\nuse tracing::{debug, error, trace};\nuse ulid::Ulid;\ntype JobId = String;\ntype JobData = JsonValue;\n\ntype JobHandler = Box<\n    dyn Fn(\n            JobId,\n            JobData,\n        ) -> Pin<Box<dyn std::future::Future<Output = Result<(), crate::Error>> + Send>>\n        + Send\n        + Sync,\n>;\n\n#[derive(Clone, Debug, Deserialize, Serialize)]\npub struct Job {\n    pub id: JobId,\n    pub name: String,\n    #[serde(rename = \"task_data\")]\n    pub data: JobData,\n    pub status: JobStatus,\n    pub run_at: DateTime<Utc>,\n    pub interval: Option<i64>,\n    pub created_at: Option<DateTime<Utc>>,\n    pub updated_at: Option<DateTime<Utc>>,\n    pub tags: Option<Vec<String>>,\n}\n\npub struct JobRegistry {\n    handlers: Arc<HashMap<String, JobHandler>>,\n}\n\nimpl JobRegistry {\n    /// Creates a new `JobRegistry`.\n    #[must_use]\n    pub fn new() -> Self {\n        Self {\n            handlers: Arc::new(HashMap::new()),\n        }\n    }\n\n    /// Registers a job handler with the provided name.\n    /// # Errors\n    /// Fails if cannot register worker\n    pub fn register_worker<Args, W>(&mut self, name: String, worker: W) -> Result<()>\n    where\n        Args: Send + Serialize + Sync + 'static,\n        W: BackgroundWorker<Args> + 'static,\n        for<'de> Args: Deserialize<'de>,\n    {\n        let worker = Arc::new(worker);\n        let wrapped_handler = move |_job_id: String, job_data: JobData| {\n            let w = worker.clone();\n\n            Box::pin(async move {\n                let args = serde_json::from_value::<Args>(job_data);\n                match args {\n                    Ok(args) => {\n                        // Wrap the perform call in catch_unwind to handle panics\n                        match AssertUnwindSafe(w.perform(args)).catch_unwind().await {\n                            Ok(result) => result,\n                            Err(panic) => {\n                                let panic_msg = panic\n                                    .downcast_ref::<String>()\n                                    .map(String::as_str)\n                                    .or_else(|| panic.downcast_ref::<&str>().copied())\n                                    .unwrap_or(\"Unknown panic occurred\");\n                                error!(err = panic_msg, \"worker panicked\");\n                                Err(Error::string(panic_msg))\n                            }\n                        }\n                    }\n                    Err(err) => Err(err.into()),\n                }\n            }) as Pin<Box<dyn Future<Output = Result<(), crate::Error>> + Send>>\n        };\n\n        Arc::get_mut(&mut self.handlers)\n            .ok_or_else(|| Error::string(\"cannot register worker\"))?\n            .insert(name, Box::new(wrapped_handler));\n        Ok(())\n    }\n\n    /// Returns a reference to the job handlers.\n    #[must_use]\n    pub fn handlers(&self) -> &Arc<HashMap<String, JobHandler>> {\n        &self.handlers\n    }\n\n    /// Runs the job handlers with the provided number of workers.\n    #[must_use]\n    pub fn run(\n        &self,\n        pool: &PgPool,\n        opts: &RunOpts,\n        token: &CancellationToken,\n        tags: &[String],\n    ) -> Vec<JoinHandle<()>> {\n        let mut jobs = Vec::new();\n\n        let interval = opts.poll_interval_sec;\n        for idx in 0..opts.num_workers {\n            let handlers = self.handlers.clone();\n            let worker_token = token.clone(); // Clone token for this worker\n            let worker_tags = tags.to_vec();\n\n            let pool = pool.clone();\n            let job = tokio::spawn(async move {\n                loop {\n                    // Check for cancellation before potentially blocking on dequeue\n                    if worker_token.is_cancelled() {\n                        trace!(worker_id = idx, \"Cancellation received, stopping worker\");\n                        break;\n                    }\n                    trace!(\n                        pool_size = pool.num_idle(),\n                        worker_id = idx,\n                        \"Connection pool stats\"\n                    );\n                    let job_opt = match dequeue(&pool, &worker_tags).await {\n                        Ok(t) => t,\n                        Err(err) => {\n                            error!(error = %err, \"Failed to fetch job from queue\");\n                            None\n                        }\n                    };\n\n                    if let Some(job) = job_opt {\n                        debug!(job_id = %job.id, job_name = %job.name, \"Processing job\");\n                        if let Some(handler) = handlers.get(&job.name) {\n                            match handler(job.id.clone(), job.data.clone()).await {\n                                Ok(()) => {\n                                    if let Err(err) =\n                                        complete_job(&pool, &job.id, job.interval).await\n                                    {\n                                        error!(\n                                            error = %err,\n                                            job_id = %job.id,\n                                            job_name = %job.name,\n                                            \"Failed to mark job as completed\"\n                                        );\n                                    } else {\n                                        debug!(job_id = %job.id, \"Job completed successfully\");\n                                    }\n                                }\n                                Err(err) => {\n                                    if let Err(fail_err) = fail_job(&pool, &job.id, &err).await {\n                                        error!(\n                                            error = %fail_err,\n                                            job_id = %job.id,\n                                            job_name = %job.name,\n                                            \"Failed to mark job as failed\"\n                                        );\n                                    } else {\n                                        debug!(job_id = %job.id, error = %err, \"Job execution failed\");\n                                    }\n                                }\n                            }\n                        } else {\n                            error!(job_name = %job.name, \"No handler registered for job\");\n                        }\n                    } else {\n                        // Use tokio::select! to wait for interval or cancellation\n                        tokio::select! {\n                            biased;\n                            () = worker_token.cancelled() => {\n                                trace!(worker_id = idx, \"Cancellation received during sleep, stopping worker\");\n                                break;\n                            }\n                            () = sleep(Duration::from_secs(interval.into())) => {\n                                // Interval elapsed, continue loop\n                            }\n                        }\n                    }\n                }\n            });\n\n            jobs.push(job);\n        }\n\n        jobs\n    }\n}\n\nimpl Default for JobRegistry {\n    fn default() -> Self {\n        Self::new()\n    }\n}\n\nasync fn connect(cfg: &PostgresQueueConfig) -> Result<PgPool> {\n    let mut conn_opts: PgConnectOptions = cfg.uri.parse()?;\n    if !cfg.enable_logging {\n        conn_opts = conn_opts.disable_statement_logging();\n    }\n    let pool = PgPoolOptions::new()\n        .min_connections(cfg.min_connections)\n        .max_connections(cfg.max_connections)\n        .idle_timeout(Duration::from_millis(cfg.idle_timeout))\n        .acquire_timeout(Duration::from_millis(cfg.connect_timeout))\n        .connect_with(conn_opts)\n        .await?;\n    Ok(pool)\n}\n\n/// Initialize job tables\n///\n/// # Errors\n///\n/// This function will return an error if it fails\npub async fn initialize_database(pool: &PgPool) -> Result<()> {\n    debug!(\"Initializing job database tables\");\n    sqlx::raw_sql(&format!(\n        r\"\n            CREATE TABLE IF NOT EXISTS pg_loco_queue (\n                id VARCHAR NOT NULL,\n                name VARCHAR NOT NULL,\n                task_data JSONB NOT NULL,\n                status VARCHAR NOT NULL DEFAULT '{}',\n                run_at TIMESTAMPTZ NOT NULL,\n                interval BIGINT,\n                created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n                updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n                tags JSONB\n            );\n            \",\n        JobStatus::Queued\n    ))\n    .execute(pool)\n    .await?;\n    Ok(())\n}\n\n/// Add a job\n///\n/// # Errors\n///\n/// This function will return an error if it fails\npub async fn enqueue(\n    pool: &PgPool,\n    name: &str,\n    data: JobData,\n    run_at: DateTime<Utc>,\n    interval: Option<Duration>,\n    tags: Option<Vec<String>>,\n) -> Result<JobId> {\n    let data_json = serde_json::to_value(data)?;\n    let tags_json = tags\n        .as_ref()\n        .map(|t| serde_json::to_value(t).unwrap_or(serde_json::Value::Null));\n\n    #[allow(clippy::cast_possible_truncation)]\n    let interval_ms: Option<i64> = interval.map(|i| i.as_millis() as i64);\n\n    let id = Ulid::new().to_string();\n    debug!(job_id = %id, job_name = %name, run_at = %run_at, tags = ?tags, \"Enqueueing job\");\n    sqlx::query(\n        \"INSERT INTO pg_loco_queue (id, task_data, name, run_at, interval, tags) VALUES ($1, $2, $3, \\\n         $4, $5, $6)\",\n    )\n    .bind(id.clone())\n    .bind(data_json)\n    .bind(name)\n    .bind(run_at)\n    .bind(interval_ms)\n    .bind(tags_json)\n    .execute(pool)\n    .await?;\n    Ok(id)\n}\n\nasync fn dequeue(client: &PgPool, worker_tags: &[String]) -> Result<Option<Job>> {\n    let mut tx = client.begin().await?;\n\n    // Base query\n    let mut query = String::from(\n        \"SELECT id, name, task_data, status, run_at, interval, tags FROM pg_loco_queue WHERE status = $1 AND run_at <= NOW() \"\n    );\n\n    // Apply tag filtering logic\n    if worker_tags.is_empty() {\n        // If worker has no tags, only process jobs with no tags\n        query.push_str(\"AND (tags IS NULL) \");\n    } else {\n        // If worker has tags, we need a more complex condition\n        query.push_str(\"AND (tags IS NOT NULL) \");\n\n        // In PostgreSQL, we need to build a condition for each tag individually\n        let mut conditions = Vec::new();\n\n        for (i, _) in worker_tags.iter().enumerate() {\n            // Check if the tag exists as a JSON string in the tags array\n            // Using ? operator checks if string exists as array element\n            conditions.push(format!(\"(tags)::jsonb ? ${}\", i + 2));\n        }\n\n        if !conditions.is_empty() {\n            query.push_str(\" AND (\");\n            query.push_str(&conditions.join(\" OR \"));\n            query.push(')');\n        }\n    }\n\n    query.push_str(\" ORDER BY run_at LIMIT 1 FOR UPDATE SKIP LOCKED\");\n\n    // Create the query\n    let mut db_query = sqlx::query(&query).bind(JobStatus::Queued.to_string());\n\n    // Bind tag parameters\n    for tag in worker_tags {\n        db_query = db_query.bind(tag);\n    }\n\n    let row = db_query\n        .map(|row: PgRow| to_job(&row).ok())\n        .fetch_optional(&mut *tx)\n        .await?\n        .flatten();\n\n    if let Some(job) = row {\n        trace!(job_id = %job.id, job_name = %job.name, job_tags = ?job.tags, \"Dequeueing job for processing\");\n        sqlx::query(\"UPDATE pg_loco_queue SET status = $1, updated_at = NOW() WHERE id = $2\")\n            .bind(JobStatus::Processing.to_string())\n            .bind(&job.id)\n            .execute(&mut *tx)\n            .await?;\n\n        tx.commit().await?;\n\n        Ok(Some(job))\n    } else {\n        Ok(None)\n    }\n}\n\nasync fn complete_job(pool: &PgPool, id: &JobId, interval_ms: Option<i64>) -> Result<()> {\n    let (status, run_at) = interval_ms.map_or_else(\n        || (JobStatus::Completed.to_string(), Utc::now()),\n        |interval_ms| {\n            (\n                JobStatus::Queued.to_string(),\n                Utc::now() + chrono::Duration::milliseconds(interval_ms),\n            )\n        },\n    );\n\n    trace!(\n        job_id = %id,\n        status = %status,\n        run_at = %run_at,\n        \"Marking job as completed\"\n    );\n\n    sqlx::query(\n        \"UPDATE pg_loco_queue SET status = $1, updated_at = NOW(), run_at = $2 WHERE id = $3\",\n    )\n    .bind(status)\n    .bind(run_at)\n    .bind(id)\n    .execute(pool)\n    .await?;\n\n    Ok(())\n}\n\nasync fn fail_job(pool: &PgPool, id: &JobId, error: &crate::Error) -> Result<()> {\n    let msg = error.to_string();\n    debug!(job_id = %id, error = %msg, \"Marking job as failed\");\n    let error_json = serde_json::json!({ \"error\": msg });\n    sqlx::query(\n        \"UPDATE pg_loco_queue SET status = $1, updated_at = NOW(), task_data = task_data || \\\n         $2::jsonb WHERE id = $3\",\n    )\n    .bind(JobStatus::Failed.to_string())\n    .bind(error_json)\n    .bind(id)\n    .execute(pool)\n    .await?;\n    Ok(())\n}\n\n/// Cancels jobs in the `pg_loco_queue` table by their name.\n///\n/// This function updates the status of all jobs with the given `name` and a status of\n/// [`JobStatus::Queued`] to [`JobStatus::Cancelled`]. The update also sets the `updated_at` timestamp to the\n/// current time.\n///\n/// # Errors\n///\n/// This function will return an error if it fails\npub async fn cancel_jobs_by_name(pool: &PgPool, name: &str) -> Result<()> {\n    debug!(job_name = %name, \"Cancelling queued jobs by name\");\n    sqlx::query(\n        \"UPDATE pg_loco_queue SET status = $1, updated_at = NOW() WHERE name = $2 AND status = $3\",\n    )\n    .bind(JobStatus::Cancelled.to_string())\n    .bind(name)\n    .bind(JobStatus::Queued.to_string())\n    .execute(pool)\n    .await?;\n    Ok(())\n}\n\n/// Clear all jobs\n///\n/// # Errors\n///\n/// This function will return an error if it fails\npub async fn clear(pool: &PgPool) -> Result<()> {\n    sqlx::query(\"DELETE FROM pg_loco_queue\")\n        .execute(pool)\n        .await?;\n    Ok(())\n}\n\n/// Deletes jobs from the `pg_loco_queue` table based on their status.\n///\n/// This function removes all jobs with a status that matches any of the statuses provided\n/// in the `status` argument. The statuses are checked against the `status` column in the\n/// database, and any matching rows are deleted.\n///\n/// # Errors\n///\n/// This function will return an error if it fails\npub async fn clear_by_status(pool: &PgPool, status: Vec<JobStatus>) -> Result<()> {\n    let status_in = status\n        .iter()\n        .map(std::string::ToString::to_string)\n        .collect::<Vec<String>>();\n\n    debug!(status = ?status, \"Clearing jobs by status\");\n    sqlx::query(\"DELETE FROM pg_loco_queue WHERE status = ANY($1)\")\n        .bind(status_in)\n        .execute(pool)\n        .await?;\n    Ok(())\n}\n\n/// Deletes jobs from the `pg_loco_queue` table that are older than a specified number of days.\n///\n/// This function removes jobs that have a `created_at` timestamp older than the provided\n/// number of days. Additionally, if a `status` is provided, only jobs with a status matching\n/// one of the provided values will be deleted.\n///\n/// # Errors\n///\n/// This function will return an error if it fails\npub async fn clear_jobs_older_than(\n    pool: &PgPool,\n    age_days: i64,\n    status: Option<&Vec<JobStatus>>,\n) -> Result<()> {\n    let mut query_builder = sqlx::query_builder::QueryBuilder::<sqlx::Postgres>::new(\n        \"DELETE FROM pg_loco_queue WHERE created_at < NOW() - INTERVAL '1 day' * \",\n    );\n\n    query_builder.push_bind(age_days);\n\n    if let Some(status_list) = status {\n        if !status_list.is_empty() {\n            let status_in = status_list\n                .iter()\n                .map(|s| format!(\"'{s}'\"))\n                .collect::<Vec<String>>()\n                .join(\",\");\n\n            query_builder.push(format!(\" AND status IN ({status_in})\"));\n        }\n    }\n\n    debug!(age_days = age_days, status = ?status, \"Clearing older jobs\");\n    query_builder.build().execute(pool).await?;\n\n    Ok(())\n}\n\n/// Requeues jobs from [`JobStatus::Processing`] to [`JobStatus::Queued`].\n///\n/// This function updates the status of all jobs that are currently in the [`JobStatus::Processing`] state\n/// to the [`JobStatus::Queued`] state, provided they have been updated more than the specified age (`age_minutes`).\n/// The jobs that meet the criteria will have their `updated_at` timestamp set to the current time.\n///\n/// # Errors\n///\n/// This function will return an error if it fails\npub async fn requeue(pool: &PgPool, age_minutes: &i64) -> Result<()> {\n    let interval = format!(\"{age_minutes} MINUTE\");\n\n    let query = format!(\n        \"UPDATE pg_loco_queue SET status = $1, updated_at = NOW() WHERE status = $2 AND updated_at <= NOW() - INTERVAL '{interval}'\"\n    );\n\n    debug!(age_minutes = age_minutes, \"Requeueing stalled jobs\");\n    sqlx::query(&query)\n        .bind(JobStatus::Queued.to_string())\n        .bind(JobStatus::Processing.to_string())\n        .execute(pool)\n        .await?;\n\n    Ok(())\n}\n\n/// Ping system\n///\n/// # Errors\n///\n/// This function will return an error if it fails\npub async fn ping(pool: &PgPool) -> Result<()> {\n    trace!(\"Pinging job queue database\");\n    sqlx::query(\"SELECT id from pg_loco_queue LIMIT 1\")\n        .execute(pool)\n        .await?;\n    Ok(())\n}\n\n/// Retrieves a list of jobs from the `pg_loco_queue` table in the database.\n///\n/// This function queries the database for jobs, optionally filtering by their\n/// `status`. If a status is provided, only jobs with statuses included in the\n/// provided list will be fetched. If no status is provided, all jobs will be\n/// returned.\n///\n/// # Errors\n///\n/// This function will return an error if it fails\npub async fn get_jobs(\n    pool: &PgPool,\n    status: Option<&Vec<JobStatus>>,\n    age_days: Option<i64>,\n) -> Result<Vec<Job>, sqlx::Error> {\n    let mut query = String::from(\"SELECT * FROM pg_loco_queue where true\");\n\n    if let Some(status) = status {\n        let status_in = status\n            .iter()\n            .map(|s| format!(\"'{s}'\"))\n            .collect::<Vec<String>>()\n            .join(\",\");\n        let _ = write!(query, \" AND status in ({status_in})\");\n    }\n\n    if let Some(age_days) = age_days {\n        let _ = write!(\n            query,\n            \" AND created_at <= NOW() - INTERVAL '1 day' * {age_days}\"\n        );\n    }\n\n    debug!(status = ?status, age_days = ?age_days, \"Retrieving jobs\");\n    let rows = sqlx::query(&query).fetch_all(pool).await?;\n    let jobs = rows.iter().filter_map(|row| to_job(row).ok()).collect();\n    debug!(job_count = rows.len(), \"Retrieved jobs from database\");\n    Ok(jobs)\n}\n\n/// Converts a row from the database into a [`Job`] object.\n///\n/// This function takes a row from the `Postgres` database and manually extracts the necessary\n/// fields to populate a [`Job`] object.\n///\n/// **Note:** This function manually extracts values from the database row instead of using\n/// the `FromRow` trait, which would require enabling the 'macros' feature in the dependencies.\n/// The decision to avoid `FromRow` is made to keep the build smaller and faster, as the 'macros'\n/// feature is unnecessary in the current dependency tree.\nfn to_job(row: &PgRow) -> Result<Job> {\n    let tags_json: Option<serde_json::Value> = row.try_get(\"tags\").unwrap_or_default();\n    let tags = tags_json.and_then(|json_val| {\n        if json_val.is_array() {\n            let tags_vec: Vec<String> =\n                serde_json::from_value(json_val).unwrap_or_else(|_| Vec::new());\n            if tags_vec.is_empty() {\n                None\n            } else {\n                Some(tags_vec)\n            }\n        } else {\n            None\n        }\n    });\n\n    Ok(Job {\n        id: row.get(\"id\"),\n        name: row.get(\"name\"),\n        data: row.get(\"task_data\"),\n        status: row.get::<String, _>(\"status\").parse().map_err(|err| {\n            let status: String = row.get(\"status\");\n            tracing::error!(status, err = %err, \"Unsupported job status in database\");\n            Error::string(\"invalid job status\")\n        })?,\n        run_at: row.get(\"run_at\"),\n        interval: row.get(\"interval\"),\n        created_at: row.try_get(\"created_at\").unwrap_or_default(),\n        updated_at: row.try_get(\"updated_at\").unwrap_or_default(),\n        tags,\n    })\n}\n\n#[derive(Debug)]\npub struct RunOpts {\n    pub num_workers: u32,\n    pub poll_interval_sec: u32,\n}\n\n/// Create this provider\n///\n/// # Errors\n///\n/// This function will return an error if it fails\npub async fn create_provider(qcfg: &PostgresQueueConfig) -> Result<Queue> {\n    debug!(\n        num_workers = qcfg.num_workers,\n        poll_interval = qcfg.poll_interval_sec,\n        \"Creating job queue provider\"\n    );\n    let pool = connect(qcfg).await.map_err(Box::from)?;\n    let registry = JobRegistry::new();\n    let token = CancellationToken::new(); // Create the token\n    Ok(Queue::Postgres(\n        pool,\n        Arc::new(tokio::sync::Mutex::new(registry)),\n        RunOpts {\n            num_workers: qcfg.num_workers,\n            poll_interval_sec: qcfg.poll_interval_sec,\n        },\n        token, // Pass the token\n    ))\n}\n\n#[cfg(test)]\nmod tests {\n    use chrono::{NaiveDate, NaiveTime, TimeZone};\n    use insta::{assert_debug_snapshot, with_settings};\n    use sqlx::{query_as, FromRow};\n    use tokio::time::sleep;\n\n    use super::*;\n    use crate::tests_cfg::{self, postgres::setup_postgres_container};\n\n    fn reduction() -> &'static [(&'static str, &'static str)] {\n        &[\n            (\"[A-Z0-9]{26}\", \"<REDACTED>\"),\n            (\n                r\"\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?Z\",\n                \"<REDACTED>\",\n            ),\n        ]\n    }\n\n    #[derive(Debug, Serialize, FromRow)]\n    pub struct TableInfo {\n        pub table_schema: Option<String>,\n        pub column_name: Option<String>,\n        pub column_default: Option<String>,\n        pub is_nullable: Option<String>,\n        pub data_type: Option<String>,\n        pub is_updatable: Option<String>,\n    }\n\n    async fn get_all_jobs(pool: &PgPool) -> Vec<Job> {\n        sqlx::query(\"select * from pg_loco_queue\")\n            .fetch_all(pool)\n            .await\n            .expect(\"get jobs\")\n            .iter()\n            .filter_map(|row| to_job(row).ok())\n            .collect()\n    }\n\n    async fn get_job(pool: &PgPool, id: &str) -> Job {\n        sqlx::query(&format!(\"select * from pg_loco_queue where id = '{id}'\"))\n            .fetch_all(pool)\n            .await\n            .expect(\"get jobs\")\n            .first()\n            .and_then(|row| to_job(row).ok())\n            .expect(\"job not found\")\n    }\n\n    // New setup function that uses our testcontainer\n    async fn setup_pg_test() -> (\n        PgPool,\n        testcontainers::ContainerAsync<testcontainers::GenericImage>,\n    ) {\n        let (pg_url, container) = setup_postgres_container().await;\n        let pool = PgPool::connect(&pg_url)\n            .await\n            .expect(\"Failed to connect to PostgreSQL\");\n\n        // Initialize the database\n        initialize_database(&pool)\n            .await\n            .expect(\"Failed to initialize database\");\n\n        (pool, container)\n    }\n\n    #[tokio::test]\n    async fn can_initialize_database() {\n        let (pool, _container) = setup_pg_test().await;\n\n        let table_info: Vec<TableInfo> = query_as::<_, TableInfo>(\n            \"SELECT * FROM information_schema.columns WHERE table_name =\n    'pg_loco_queue'\",\n        )\n        .fetch_all(&pool)\n        .await\n        .unwrap();\n\n        assert_debug_snapshot!(table_info);\n    }\n\n    #[tokio::test]\n    async fn can_enqueue() {\n        let (pool, _container) = setup_pg_test().await;\n\n        let jobs = get_all_jobs(&pool).await;\n\n        assert_eq!(jobs.len(), 0);\n\n        let run_at = Utc.from_utc_datetime(\n            &NaiveDate::from_ymd_opt(2023, 1, 15)\n                .unwrap()\n                .and_time(NaiveTime::from_hms_opt(12, 30, 0).unwrap()),\n        );\n\n        let job_data: JobData = serde_json::json!({\"user_id\": 1});\n        assert!(enqueue(\n            &pool,\n            \"PasswordChangeNotification\",\n            job_data,\n            run_at,\n            None,\n            None\n        )\n        .await\n        .is_ok());\n\n        let jobs = get_all_jobs(&pool).await;\n\n        assert_eq!(jobs.len(), 1);\n        with_settings!({\n                filters => reduction().iter().map(|&(pattern, replacement)|\n        (pattern, replacement)),     }, {\n                assert_debug_snapshot!(jobs);\n            });\n    }\n\n    #[tokio::test]\n    async fn can_dequeue() {\n        let (pool, _container) = setup_pg_test().await;\n\n        let run_at = Utc.from_utc_datetime(\n            &NaiveDate::from_ymd_opt(2023, 1, 15)\n                .unwrap()\n                .and_time(NaiveTime::from_hms_opt(12, 30, 0).unwrap()),\n        );\n\n        let job_data: JobData = serde_json::json!({\"user_id\": 1});\n        assert!(enqueue(\n            &pool,\n            \"PasswordChangeNotification\",\n            job_data,\n            run_at,\n            None,\n            None\n        )\n        .await\n        .is_ok());\n\n        let job_before_dequeue = get_all_jobs(&pool)\n            .await\n            .first()\n            .cloned()\n            .expect(\"gets first job\");\n\n        assert_eq!(job_before_dequeue.status, JobStatus::Queued);\n\n        std::thread::sleep(std::time::Duration::from_secs(1));\n\n        assert!(dequeue(&pool, &[]).await.is_ok());\n\n        let job_after_dequeue = get_all_jobs(&pool)\n            .await\n            .first()\n            .cloned()\n            .expect(\"gets first job\");\n\n        assert_ne!(job_after_dequeue.updated_at, job_before_dequeue.updated_at);\n        with_settings!({\n                filters => reduction().iter().map(|&(pattern, replacement)|\n        (pattern, replacement)),     }, {\n                assert_debug_snapshot!(job_after_dequeue);\n            });\n    }\n\n    #[tokio::test]\n    async fn can_complete_job_without_interval() {\n        let (pool, _container) = setup_pg_test().await;\n        tests_cfg::queue::postgres_seed_data(&pool).await;\n\n        let job = get_job(&pool, \"01JDM0X8EVAM823JZBGKYNBA99\").await;\n\n        assert_eq!(job.status, JobStatus::Queued);\n        assert!(complete_job(&pool, &job.id, None).await.is_ok());\n\n        let job = get_job(&pool, \"01JDM0X8EVAM823JZBGKYNBA99\").await;\n\n        assert_eq!(job.status, JobStatus::Completed);\n    }\n\n    #[tokio::test]\n    async fn can_complete_job_with_interval() {\n        let (pool, _container) = setup_pg_test().await;\n        tests_cfg::queue::postgres_seed_data(&pool).await;\n\n        let before_complete_job = get_job(&pool, \"01JDM0X8EVAM823JZBGKYNBA98\").await;\n\n        assert_eq!(before_complete_job.status, JobStatus::Completed);\n\n        std::thread::sleep(std::time::Duration::from_secs(1));\n\n        assert!(complete_job(&pool, &before_complete_job.id, Some(10))\n            .await\n            .is_ok());\n\n        let after_complete_job = get_job(&pool, \"01JDM0X8EVAM823JZBGKYNBA98\").await;\n\n        assert_ne!(\n            after_complete_job.updated_at,\n            before_complete_job.updated_at\n        );\n        with_settings!({\n                filters => reduction().iter().map(|&(pattern, replacement)| (pattern,\n        replacement)),     }, {\n                assert_debug_snapshot!(after_complete_job);\n            });\n    }\n\n    #[tokio::test]\n    async fn can_fail_job() {\n        let (pool, _container) = setup_pg_test().await;\n        tests_cfg::queue::postgres_seed_data(&pool).await;\n\n        let before_fail_job = get_job(&pool, \"01JDM0X8EVAM823JZBGKYNBA97\").await;\n\n        std::thread::sleep(std::time::Duration::from_secs(1));\n\n        assert!(fail_job(\n            &pool,\n            &before_fail_job.id,\n            &crate::Error::string(\"some error\")\n        )\n        .await\n        .is_ok());\n\n        let after_fail_job = get_job(&pool, \"01JDM0X8EVAM823JZBGKYNBA97\").await;\n\n        assert_ne!(after_fail_job.updated_at, before_fail_job.updated_at);\n        with_settings!({\n                filters => reduction().iter().map(|&(pattern, replacement)| (pattern,\n        replacement)),     }, {\n                assert_debug_snapshot!(after_fail_job);\n            });\n    }\n\n    #[tokio::test]\n    async fn can_cancel_job_by_name() {\n        let (pool, _container) = setup_pg_test().await;\n        tests_cfg::queue::postgres_seed_data(&pool).await;\n\n        let count_cancelled_jobs = get_all_jobs(&pool)\n            .await\n            .iter()\n            .filter(|j| j.status == JobStatus::Cancelled)\n            .count();\n\n        assert_eq!(count_cancelled_jobs, 1);\n\n        assert!(cancel_jobs_by_name(&pool, \"UserAccountActivation\")\n            .await\n            .is_ok());\n\n        let count_cancelled_jobs = get_all_jobs(&pool)\n            .await\n            .iter()\n            .filter(|j| j.status == JobStatus::Cancelled)\n            .count();\n\n        assert_eq!(count_cancelled_jobs, 2);\n    }\n\n    #[tokio::test]\n    async fn can_clear() {\n        let (pool, _container) = setup_pg_test().await;\n        tests_cfg::queue::postgres_seed_data(&pool).await;\n\n        let job_count: i64 = sqlx::query_scalar(\"SELECT COUNT(*) FROM pg_loco_queue\")\n            .fetch_one(&pool)\n            .await\n            .unwrap();\n\n        assert_ne!(job_count, 0);\n\n        assert!(clear(&pool).await.is_ok());\n        let job_count: i64 = sqlx::query_scalar(\"SELECT COUNT(*) FROM pg_loco_queue\")\n            .fetch_one(&pool)\n            .await\n            .unwrap();\n\n        assert_eq!(job_count, 0);\n    }\n\n    #[tokio::test]\n    async fn can_clear_by_status() {\n        let (pool, _container) = setup_pg_test().await;\n        tests_cfg::queue::postgres_seed_data(&pool).await;\n\n        let jobs = get_all_jobs(&pool).await;\n\n        assert_eq!(jobs.len(), 14);\n        assert_eq!(\n            jobs.iter()\n                .filter(|j| j.status == JobStatus::Completed)\n                .count(),\n            3\n        );\n        assert_eq!(\n            jobs.iter()\n                .filter(|j| j.status == JobStatus::Failed)\n                .count(),\n            2\n        );\n\n        assert!(\n            clear_by_status(&pool, vec![JobStatus::Completed, JobStatus::Failed])\n                .await\n                .is_ok()\n        );\n        let jobs = get_all_jobs(&pool).await;\n\n        assert_eq!(jobs.len(), 9);\n        assert_eq!(\n            jobs.iter()\n                .filter(|j| j.status == JobStatus::Completed)\n                .count(),\n            0\n        );\n        assert_eq!(\n            jobs.iter()\n                .filter(|j| j.status == JobStatus::Failed)\n                .count(),\n            0\n        );\n    }\n\n    #[tokio::test]\n    async fn can_clear_jobs_older_than() {\n        let (pool, _container) = setup_pg_test().await;\n\n        sqlx::query(\n           r\"INSERT INTO pg_loco_queue (id, name, task_data, status, run_at,created_at, updated_at) VALUES\n             ('job1', 'Test Job 1', '{}', 'queued', NOW(), NOW() - INTERVAL '15days', NOW()),\n             ('job2', 'Test Job 2', '{}', 'queued', NOW(),NOW() - INTERVAL '5 days', NOW()),\n             ('job3', 'Test Job 3', '{}','queued', NOW(), NOW(), NOW())\"\n            )\n        .execute(&pool)\n        .await\n        .unwrap();\n\n        assert_eq!(get_all_jobs(&pool).await.len(), 3);\n        assert!(clear_jobs_older_than(&pool, 10, None).await.is_ok());\n        assert_eq!(get_all_jobs(&pool).await.len(), 2);\n    }\n\n    #[tokio::test]\n    async fn can_clear_jobs_older_than_with_status() {\n        let (pool, _container) = setup_pg_test().await;\n\n        sqlx::query(\n           r\"INSERT INTO pg_loco_queue (id, name, task_data, status, run_at,created_at, updated_at) VALUES\n             ('job1', 'Test Job 1', '{}', 'completed', NOW(), NOW() - INTERVAL '20days', NOW()),\n             ('job2', 'Test Job 2', '{}', 'failed', NOW(),NOW() - INTERVAL '15 days', NOW()),\n             ('job3', 'Test Job 3', '{}', 'completed', NOW(),NOW() - INTERVAL '5 days', NOW()),\n             ('job4', 'Test Job 3', '{}','cancelled', NOW(), NOW(), NOW())\"\n            )\n        .execute(&pool)\n        .await\n        .unwrap();\n\n        assert_eq!(get_all_jobs(&pool).await.len(), 4);\n        assert!(clear_jobs_older_than(\n            &pool,\n            10,\n            Some(&vec![JobStatus::Cancelled, JobStatus::Completed])\n        )\n        .await\n        .is_ok());\n\n        assert_eq!(get_all_jobs(&pool).await.len(), 3);\n    }\n\n    #[tokio::test]\n    async fn can_get_jobs() {\n        let (pool, _container) = setup_pg_test().await;\n        tests_cfg::queue::postgres_seed_data(&pool).await;\n\n        assert_eq!(\n            get_jobs(&pool, Some(&vec![JobStatus::Failed]), None)\n                .await\n                .expect(\"get jobs\")\n                .len(),\n            2\n        );\n        assert_eq!(\n            get_jobs(\n                &pool,\n                Some(&vec![JobStatus::Failed, JobStatus::Completed]),\n                None\n            )\n            .await\n            .expect(\"get jobs\")\n            .len(),\n            5\n        );\n        assert_eq!(\n            get_jobs(&pool, None, None).await.expect(\"get jobs\").len(),\n            14\n        );\n    }\n\n    #[tokio::test]\n    async fn can_get_jobs_with_age() {\n        let (pool, _container) = setup_pg_test().await;\n\n        sqlx::query(\n            r\"INSERT INTO pg_loco_queue (id, name, task_data, status, run_at,created_at, updated_at) VALUES\n             ('job1', 'Test Job 1', '{}', 'completed', NOW(), NOW() - INTERVAL '20days', NOW()),\n             ('job2', 'Test Job 2', '{}', 'failed', NOW(),NOW() - INTERVAL '15 days', NOW()),\n             ('job3', 'Test Job 3', '{}', 'completed', NOW(),NOW() - INTERVAL '5 days', NOW()),\n             ('job4', 'Test Job 3', '{}','cancelled', NOW(), NOW(), NOW())\"\n        )\n        .execute(&pool)\n        .await\n        .unwrap();\n        assert_eq!(\n            get_jobs(\n                &pool,\n                Some(&vec![JobStatus::Failed, JobStatus::Completed]),\n                Some(11)\n            )\n            .await\n            .expect(\"get jobs\")\n            .len(),\n            2\n        );\n    }\n\n    #[tokio::test]\n    async fn can_requeue() {\n        let (pool, _container) = setup_pg_test().await;\n\n        sqlx::query(\n            r\"INSERT INTO pg_loco_queue (id, name, task_data, status, run_at,created_at, updated_at) VALUES\n             ('job1', 'Test Job 1', '{}', 'processing', NOW(),NOW(), NOW() - INTERVAL '20 minutes'),\n             ('job2', 'Test Job 2', '{}', 'processing', NOW(),NOW(), NOW() - INTERVAL '5 minutes'),\n             ('job3', 'Test Job 3', '{}', 'completed', NOW(),NOW(),NOW() - INTERVAL '5 minutes'),\n             ('job4', 'Test Job 4', '{}', 'queued', NOW(),NOW(), NOW()),\n             ('job4', 'Test Job 5', '{}', 'processing', NOW(), NOW(), NOW())\"\n        )\n        .execute(&pool)\n        .await\n        .unwrap();\n\n        assert_eq!(\n            get_jobs(&pool, Some(&vec![JobStatus::Processing]), None)\n                .await\n                .expect(\"get jobs\")\n                .len(),\n            3\n        );\n        assert_eq!(\n            get_jobs(&pool, Some(&vec![JobStatus::Queued]), None)\n                .await\n                .expect(\"get jobs\")\n                .len(),\n            1\n        );\n\n        requeue(&pool, &10).await.expect(\"update jobs\");\n\n        assert_eq!(\n            get_jobs(&pool, Some(&vec![JobStatus::Processing]), None)\n                .await\n                .expect(\"get jobs\")\n                .len(),\n            2\n        );\n        assert_eq!(\n            get_jobs(&pool, Some(&vec![JobStatus::Queued]), None)\n                .await\n                .expect(\"get jobs\")\n                .len(),\n            2\n        );\n    }\n\n    #[tokio::test]\n    async fn can_handle_worker_panic() {\n        let (pool, _container) = setup_pg_test().await;\n\n        let job_data: JobData = serde_json::json!(null);\n        let job_id = enqueue(&pool, \"PanicJob\", job_data, Utc::now(), None, None)\n            .await\n            .expect(\"Failed to enqueue job\");\n\n        struct PanicWorker;\n        #[async_trait::async_trait]\n        impl BackgroundWorker<()> for PanicWorker {\n            fn build(_ctx: &crate::app::AppContext) -> Self {\n                Self\n            }\n            async fn perform(&self, _args: ()) -> crate::Result<()> {\n                panic!(\"intentional panic for testing\");\n            }\n        }\n\n        let mut registry = JobRegistry::new();\n        assert!(registry\n            .register_worker(\"PanicJob\".to_string(), PanicWorker)\n            .is_ok());\n\n        // Get the initial job state\n        let job = get_job(&pool, &job_id).await;\n        assert_eq!(job.status, JobStatus::Queued);\n\n        // Start the worker\n        let opts = RunOpts {\n            num_workers: 1,\n            poll_interval_sec: 1,\n        };\n        let token = CancellationToken::new();\n        let handles = registry.run(&pool, &opts, &token, &[]);\n\n        // Wait a bit for the worker to process the job\n        sleep(Duration::from_secs(1)).await;\n\n        // Stop the worker\n        for handle in handles {\n            handle.abort();\n        }\n\n        // Verify the job is marked as failed\n        let failed_job = get_job(&pool, &job_id).await;\n        assert_eq!(failed_job.status, JobStatus::Failed);\n\n        // Verify the error message stored in job data\n        let error_msg = failed_job\n            .data\n            .as_array()\n            .and_then(|arr| arr.get(1))\n            .and_then(|obj| obj.as_object())\n            .and_then(|obj| obj.get(\"error\"))\n            .and_then(|v| v.as_str())\n            .expect(\"Expected error message in job data\");\n        assert!(\n            error_msg.contains(\"intentional panic for testing\"),\n            \"Error message '{error_msg}' did not contain expected text\"\n        );\n    }\n\n    #[tokio::test]\n    async fn can_dequeue_with_tags() {\n        let (pool, _container) = setup_pg_test().await;\n\n        // Add a job with email tag\n        let run_at = Utc::now() - chrono::Duration::minutes(5); // In the past so it's ready to process\n        let job_data = serde_json::json!({\"user_id\": 1});\n\n        // Insert email job\n        let email_tags = Some(vec![\"email\".to_string()]);\n        let email_id = enqueue(\n            &pool,\n            \"EmailNotification\",\n            job_data.clone(),\n            run_at,\n            None,\n            email_tags,\n        )\n        .await\n        .expect(\"Failed to enqueue email job\");\n\n        // Insert job with \"sms\" tag\n        let sms_tags = Some(vec![\"sms\".to_string()]);\n        let sms_id = enqueue(\n            &pool,\n            \"SmsNotification\",\n            job_data.clone(),\n            run_at,\n            None,\n            sms_tags,\n        )\n        .await\n        .expect(\"Failed to enqueue sms job\");\n\n        // Insert job with multiple tags\n        let multi_tags = Some(vec![\"email\".to_string(), \"priority\".to_string()]);\n        let multi_id = enqueue(\n            &pool,\n            \"PriorityEmail\",\n            job_data.clone(),\n            run_at,\n            None,\n            multi_tags,\n        )\n        .await\n        .expect(\"Failed to enqueue multi-tag job\");\n\n        // Insert job with no tags\n        let no_tag_id = enqueue(\n            &pool,\n            \"GenericNotification\",\n            job_data.clone(),\n            run_at,\n            None,\n            None,\n        )\n        .await\n        .expect(\"Failed to enqueue untagged job\");\n\n        // Verify all jobs are in the database\n        let all_jobs = get_all_jobs(&pool).await;\n        assert_eq!(all_jobs.len(), 4);\n\n        // 1. Worker with no tags should only get untagged jobs\n        let job = dequeue(&pool, &[]).await.expect(\"dequeue failed\");\n        assert!(job.is_some());\n        let job = job.unwrap();\n        assert_eq!(job.id, no_tag_id);\n        assert!(job.tags.is_none());\n\n        // Mark the job as completed to remove it from the queued items\n        complete_job(&pool, &job.id, None)\n            .await\n            .expect(\"Failed to complete job\");\n\n        // 2. Worker with \"email\" tag should get one of the email-tagged jobs\n        let job = dequeue(&pool, &[\"email\".to_string()])\n            .await\n            .expect(\"dequeue failed\");\n        assert!(job.is_some());\n        let job = job.unwrap();\n        assert!(\n            job.id == email_id || job.id == multi_id,\n            \"Expected either email job or multi-tag job\"\n        );\n        assert!(job.tags.is_some());\n\n        // Mark the job as completed\n        complete_job(&pool, &job.id, None)\n            .await\n            .expect(\"Failed to complete job\");\n\n        // 3. Worker with \"email\" tag should get the remaining email job\n        let job = dequeue(&pool, &[\"email\".to_string()])\n            .await\n            .expect(\"dequeue failed\");\n        assert!(job.is_some());\n        let job = job.unwrap();\n        assert!(\n            job.id == email_id || job.id == multi_id,\n            \"Expected either email job or multi-tag job\"\n        );\n        assert!(job.tags.is_some());\n\n        // Mark the job as completed\n        complete_job(&pool, &job.id, None)\n            .await\n            .expect(\"Failed to complete job\");\n\n        // 4. Worker with \"sms\" tag should get the sms job\n        let job = dequeue(&pool, &[\"sms\".to_string()])\n            .await\n            .expect(\"dequeue failed\");\n        assert!(job.is_some());\n        let job = job.unwrap();\n        assert_eq!(job.id, sms_id);\n        assert!(job.tags.is_some());\n\n        // Mark the job as completed\n        complete_job(&pool, &job.id, None)\n            .await\n            .expect(\"Failed to complete job\");\n\n        // 5. No more jobs should be available\n        let job = dequeue(&pool, &[\"email\".to_string()])\n            .await\n            .expect(\"dequeue failed\");\n        assert!(job.is_none());\n\n        // 6. No more jobs should be available for untagged worker\n        let job = dequeue(&pool, &[]).await.expect(\"dequeue failed\");\n        assert!(job.is_none());\n    }\n}\n"
  },
  {
    "path": "src/bgworker/redis.rs",
    "content": "/// Redis based background job queue provider\nuse std::{\n    collections::HashMap, future::Future, panic::AssertUnwindSafe, pin::Pin, sync::Arc,\n    time::Duration,\n};\n\nuse super::{BackgroundWorker, JobStatus, Queue};\nuse crate::{config::RedisQueueConfig, Error, Result};\nuse chrono::{DateTime, Utc};\nuse futures_util::FutureExt;\nuse redis::{aio::MultiplexedConnection as Connection, AsyncCommands, Client, Script};\nuse serde::{Deserialize, Serialize};\nuse serde_json::Value as JsonValue;\nuse tokio::{task::JoinHandle, time::sleep};\nuse tokio_util::sync::CancellationToken;\nuse tracing::{debug, error, trace};\nuse ulid::Ulid;\n\npub type RedisPool = Client;\ntype JobId = String;\ntype JobData = JsonValue;\n\nconst QUEUE_KEY_PREFIX: &str = \"queue:\";\nconst JOB_KEY_PREFIX: &str = \"job:\";\nconst PROCESSING_KEY_PREFIX: &str = \"processing:\";\n\ntype JobHandler = Box<\n    dyn Fn(\n            JobId,\n            JobData,\n        ) -> Pin<Box<dyn std::future::Future<Output = Result<(), crate::Error>> + Send>>\n        + Send\n        + Sync,\n>;\n\n#[derive(Clone, Debug, Deserialize, Serialize)]\npub struct Job {\n    pub id: JobId,\n    pub name: String,\n    #[serde(rename = \"task_data\")]\n    pub data: JobData,\n    pub status: JobStatus,\n    pub run_at: DateTime<Utc>,\n    pub interval: Option<i64>,\n    pub created_at: Option<DateTime<Utc>>,\n    pub updated_at: Option<DateTime<Utc>>,\n    pub tags: Option<Vec<String>>,\n}\n\n// Implementation for job creation and serialization\nimpl Job {\n    fn new(id: String, name: String, data: JsonValue) -> Self {\n        let now = Utc::now();\n        Self {\n            id,\n            name,\n            data,\n            status: JobStatus::Queued,\n            run_at: now,\n            interval: None,\n            created_at: Some(now),\n            updated_at: Some(now),\n            tags: None,\n        }\n    }\n\n    // Create JSON format for storing in Redis\n    fn to_json(&self) -> Result<String> {\n        Ok(serde_json::to_string(self)?)\n    }\n\n    // Parse from JSON format\n    fn from_json(json: &str) -> Result<Self> {\n        Ok(serde_json::from_str(json)?)\n    }\n}\n\npub struct JobRegistry {\n    handlers: Arc<HashMap<String, JobHandler>>,\n}\n\nimpl JobRegistry {\n    /// Creates a new [`JobRegistry`].\n    #[must_use]\n    pub fn new() -> Self {\n        Self {\n            handlers: Arc::new(HashMap::new()),\n        }\n    }\n\n    /// Registers a job handler with the provided name.\n    ///\n    /// # Errors\n    ///\n    /// Fails if cannot register worker\n    pub fn register_worker<Args, W>(&mut self, name: String, worker: W) -> Result<()>\n    where\n        Args: Send + Serialize + Sync + 'static,\n        W: BackgroundWorker<Args> + 'static,\n        for<'de> Args: Deserialize<'de>,\n    {\n        let worker = Arc::new(worker);\n        let wrapped_handler = move |_job_id: String, job_data: JobData| {\n            let w = worker.clone();\n            Box::pin(async move {\n                let args = serde_json::from_value::<Args>(job_data);\n                match args {\n                    Ok(args) => {\n                        // Wrap the perform call in catch_unwind to handle panics\n                        match AssertUnwindSafe(w.perform(args)).catch_unwind().await {\n                            Ok(result) => result,\n                            Err(panic) => {\n                                let panic_msg = panic\n                                    .downcast_ref::<String>()\n                                    .map(String::as_str)\n                                    .or_else(|| panic.downcast_ref::<&str>().copied())\n                                    .unwrap_or(\"Unknown panic occurred\");\n                                error!(err = panic_msg, \"worker panicked\");\n                                Err(Error::string(panic_msg))\n                            }\n                        }\n                    }\n                    Err(err) => Err(err.into()),\n                }\n            }) as Pin<Box<dyn Future<Output = Result<(), crate::Error>> + Send>>\n        };\n        Arc::get_mut(&mut self.handlers)\n            .ok_or_else(|| Error::string(\"cannot register worker\"))?\n            .insert(name, Box::new(wrapped_handler));\n        Ok(())\n    }\n\n    /// Returns a reference to the job handlers.\n    #[must_use]\n    pub fn handlers(&self) -> &Arc<HashMap<String, JobHandler>> {\n        &self.handlers\n    }\n\n    /// Runs the job handlers with the provided number of workers.\n    #[must_use]\n    pub fn run(\n        &self,\n        client: &RedisPool,\n        opts: &RunOpts,\n        token: &CancellationToken,\n        tags: &[String],\n    ) -> Vec<JoinHandle<()>> {\n        let mut jobs = Vec::new();\n        let queues = get_queues(&opts.queues);\n        let interval = opts.poll_interval_sec;\n\n        for idx in 0..opts.num_workers {\n            let handlers = self.handlers.clone();\n            let worker_token = token.clone();\n            let client = client.clone();\n            let queues = queues.clone();\n            let tags = tags.to_owned();\n\n            let job = tokio::spawn(async move {\n                let mut conn = match client.get_multiplexed_async_connection().await {\n                    Ok(conn) => conn,\n                    Err(err) => {\n                        error!(err = err.to_string(), \"Failed to create worker connection\");\n                        return;\n                    }\n                };\n\n                loop {\n                    // Check for cancellation before potentially blocking on dequeue\n                    if worker_token.is_cancelled() {\n                        trace!(worker_num = idx, \"cancellation received, stopping worker\");\n                        break;\n                    }\n\n                    let job_opt = match dequeue_with_conn(&mut conn, &queues, &tags).await {\n                        Ok(t) => t,\n                        Err(err) => {\n                            error!(err = err.to_string(), \"cannot fetch from queue\");\n                            None\n                        }\n                    };\n\n                    if let Some((job, queue_name)) = job_opt {\n                        debug!(job_id = job.id, name = job.name, \"working on job\");\n                        if let Some(handler) = handlers.get(&job.name) {\n                            match handler(job.id.clone(), job.data.clone()).await {\n                                Ok(()) => {\n                                    if let Err(err) = complete_job_with_conn(\n                                        &mut conn,\n                                        &job.id,\n                                        &queue_name,\n                                        job.interval,\n                                    )\n                                    .await\n                                    {\n                                        error!(err = err.to_string(), job = ?job, \"cannot complete job\");\n                                    }\n                                }\n                                Err(err) => {\n                                    if let Err(err) =\n                                        fail_job_with_conn(&mut conn, &job.id, &queue_name, &err)\n                                            .await\n                                    {\n                                        error!(err = err.to_string(), job = ?job, \"cannot fail job\");\n                                    }\n                                }\n                            }\n                        } else {\n                            error!(job = job.name, \"no handler found for job\");\n                        }\n                    } else {\n                        tokio::select! {\n                            biased;\n                            () = worker_token.cancelled() => {\n                                trace!(worker_num = idx, \"cancellation received during sleep, stopping worker\");\n                                break;\n                            }\n                            () = sleep(Duration::from_secs(interval.into())) => {}\n                        }\n                    }\n                }\n            });\n            jobs.push(job);\n        }\n        jobs\n    }\n}\n\nimpl Default for JobRegistry {\n    fn default() -> Self {\n        Self::new()\n    }\n}\n\nfn connect(url: &str) -> Result<RedisPool> {\n    let client = Client::open(url.to_string())?;\n    Ok(client)\n}\n\nasync fn get_connection(client: &RedisPool) -> Result<Connection> {\n    let conn = client.get_multiplexed_async_connection().await?;\n    Ok(conn)\n}\n\n/// Clear tasks\n///\n/// # Errors\n///\n/// This function will return an error if it fails\npub async fn clear(client: &RedisPool) -> Result<()> {\n    let mut conn = get_connection(client).await?;\n    redis::cmd(\"FLUSHDB\").query_async::<()>(&mut conn).await?;\n    Ok(())\n}\n\n/// Add a task\n///\n/// # Errors\n///\n/// This function will return an error if it fails\npub async fn enqueue(\n    client: &RedisPool,\n    class: String,\n    queue: Option<String>,\n    args: impl serde::Serialize + Send,\n    tags: Option<Vec<String>>,\n) -> Result<()> {\n    let mut conn = get_connection(client).await?;\n    let queue_name = queue.unwrap_or_else(|| \"default\".to_string());\n    let queue_key = format!(\"{QUEUE_KEY_PREFIX}{queue_name}\");\n\n    // Convert args to JSON\n    let args_json = serde_json::to_value(args)?;\n\n    // Create a job ID using ULID\n    let job_id = Ulid::new().to_string();\n\n    // Create job\n    let mut job = Job::new(job_id.clone(), class, args_json);\n    job.tags = tags;\n\n    // Serialize job for Redis storage\n    let job_json = job.to_json()?;\n\n    // Store job in Redis queue and in job key\n    let job_key = format!(\"{JOB_KEY_PREFIX}{}\", job.id);\n    let _: () = conn.set(&job_key, &job_json).await?;\n    let _: () = conn.rpush(&queue_key, &job.id).await?;\n\n    Ok(())\n}\n\nconst DEQUEUE_SCRIPT: &str = r#\"\nlocal queue_key = KEYS[1]\nlocal processing_key = KEYS[2]\nlocal job_id = redis.call('LPOP', queue_key)\nif job_id then\n    local added = redis.call('SADD', processing_key, job_id)\n    if added == 1 then\n        return job_id\n    else\n        redis.log(redis.LOG_WARNING, \"Job already in processing: \" .. job_id)\n        return nil\n    end\nelse\n    return nil\nend\n\"#;\n\nasync fn dequeue_with_conn(\n    conn: &mut Connection,\n    queues: &[String],\n    tags: &[String],\n) -> Result<Option<(Job, String)>> {\n    if queues.is_empty() {\n        return Ok(None);\n    }\n\n    let script = Script::new(DEQUEUE_SCRIPT);\n\n    // Try to get a job from each queue in order (round-robin is more complex)\n    for queue_name in queues {\n        let queue_key = format!(\"{QUEUE_KEY_PREFIX}{queue_name}\");\n        let processing_key = format!(\"{PROCESSING_KEY_PREFIX}{queue_name}\");\n\n        let job_id: Option<String> = script\n            .key(&queue_key)\n            .key(&processing_key)\n            .invoke_async(conn)\n            .await?;\n\n        if let Some(job_id) = job_id {\n            let job_key = format!(\"{JOB_KEY_PREFIX}{job_id}\");\n            let job_json: Option<String> = conn.get(&job_key).await?;\n\n            if let Some(json) = job_json {\n                match Job::from_json(&json) {\n                    Ok(job) => {\n                        let should_process = if tags.is_empty() {\n                            job.tags.is_none() || job.tags.as_ref().map_or(true, Vec::is_empty)\n                        } else {\n                            job.tags.as_ref().is_some_and(|job_tags| {\n                                job_tags.iter().any(|tag| tags.contains(tag))\n                            })\n                        };\n\n                        if should_process {\n                            return Ok(Some((job, queue_name.clone())));\n                        }\n                        let _: () = conn.srem(&processing_key, &job_id).await?;\n                        let _: () = conn.rpush(&queue_key, &job_id).await?;\n                        trace!(\n                            job_id = job_id,\n                            job_tags = ?job.tags,\n                            worker_tags = ?tags,\n                            \"Job doesn't match tag criteria, returned to queue\"\n                        );\n                    }\n                    Err(err) => {\n                        error!(\n                            err = err.to_string(),\n                            job_id = job_id,\n                            \"Failed to parse job JSON\"\n                        );\n                        let _: () = conn.srem(&processing_key, &job_id).await?;\n                    }\n                }\n            } else {\n                error!(job_id = job_id, queue = queue_name, \"Job data not found.\");\n                let _: () = conn.srem(&processing_key, &job_id).await?;\n            }\n        }\n    }\n    Ok(None)\n}\n\nasync fn complete_job_with_conn(\n    conn: &mut Connection,\n    id: &JobId,\n    queue_name: &str,\n    interval_ms: Option<i64>,\n) -> Result<()> {\n    let job_key = format!(\"{JOB_KEY_PREFIX}{id}\");\n    let processing_key = format!(\"{PROCESSING_KEY_PREFIX}{queue_name}\");\n\n    let job_json: Option<String> = conn.get(&job_key).await?;\n    if let Some(json) = job_json {\n        if let Ok(mut job) = Job::from_json(&json) {\n            if let Some(interval) = interval_ms {\n                job.run_at = Utc::now() + chrono::Duration::milliseconds(interval);\n                job.status = JobStatus::Queued;\n                let new_json = job.to_json()?;\n                let queue_key = format!(\"{QUEUE_KEY_PREFIX}{queue_name}\");\n                let _: () = redis::pipe()\n                    .set(&job_key, &new_json)\n                    .rpush(&queue_key, id)\n                    .query_async(conn)\n                    .await?;\n            } else {\n                job.status = JobStatus::Completed;\n                job.updated_at = Some(Utc::now());\n                let updated_json = job.to_json()?;\n                let _: () = conn.set(&job_key, &updated_json).await?;\n            }\n            let _: () = conn.srem(&processing_key, id).await?;\n        }\n    }\n    Ok(())\n}\n\nasync fn fail_job_with_conn(\n    conn: &mut Connection,\n    id: &JobId,\n    queue_name: &str,\n    error: &crate::Error,\n) -> Result<()> {\n    let job_key = format!(\"{JOB_KEY_PREFIX}{id}\");\n    let processing_key = format!(\"{PROCESSING_KEY_PREFIX}{queue_name}\");\n\n    let job_json: Option<String> = conn.get(&job_key).await?;\n    if let Some(json) = job_json {\n        if let Ok(mut job) = Job::from_json(&json) {\n            let error_json = serde_json::json!({ \"error\": error.to_string() });\n            job.data = error_json;\n            job.status = JobStatus::Failed;\n            job.updated_at = Some(Utc::now());\n            let updated_json = job.to_json()?;\n            let _: () = conn.set(&job_key, &updated_json).await?;\n        }\n    }\n    let _: () = conn.srem(&processing_key, id).await?;\n    Ok(())\n}\n\n/// Ping system\n///\n/// # Errors\n///\n/// This function will return an error if it fails\npub async fn ping(client: &RedisPool) -> Result<()> {\n    let mut conn = get_connection(client).await?;\n    let _: String = redis::cmd(\"PING\").query_async(&mut conn).await?;\n    Ok(())\n}\n\n/// Retrieves a list of jobs from the Redis queues.\n///\n/// This function queries Redis for jobs, optionally filtering by their\n/// `status` and age. It will search through all processing sets and queue keys\n/// to find jobs matching the criteria.\n///\n/// # Errors\n///\n/// This function will return an error if it fails\npub async fn get_jobs(\n    client: &RedisPool,\n    status: Option<&Vec<JobStatus>>,\n    age_days: Option<i64>,\n) -> Result<Vec<Job>> {\n    let mut conn = get_connection(client).await?;\n    let mut jobs = Vec::new();\n\n    // Get all queue keys\n    let queue_pattern = format!(\"{QUEUE_KEY_PREFIX}*\");\n    let queue_keys: Vec<String> = redis::cmd(\"KEYS\")\n        .arg(&queue_pattern)\n        .query_async(&mut conn)\n        .await?;\n\n    // Get all processing keys\n    let processing_pattern = format!(\"{PROCESSING_KEY_PREFIX}*\");\n    let processing_keys: Vec<String> = redis::cmd(\"KEYS\")\n        .arg(&processing_pattern)\n        .query_async(&mut conn)\n        .await?;\n\n    // Collect jobs from queues\n    for queue_key in queue_keys {\n        let job_ids: Vec<String> = conn.lrange(&queue_key, 0, -1).await?;\n        for job_id in job_ids {\n            let job_key = format!(\"{JOB_KEY_PREFIX}{job_id}\");\n            let job_json: Option<String> = conn.get(&job_key).await?;\n            if let Some(json) = job_json {\n                if let Ok(job) = Job::from_json(&json) {\n                    if should_include_job(&job, status, age_days) {\n                        jobs.push(job);\n                    }\n                }\n            }\n        }\n    }\n\n    // Collect jobs from processing sets\n    for processing_key in processing_keys {\n        let job_ids: Vec<String> = conn.smembers(&processing_key).await?;\n        for job_id in job_ids {\n            // Get the job from the job_key using the ID\n            let job_key = format!(\"{JOB_KEY_PREFIX}{job_id}\");\n            let job_json: Option<String> = conn.get(&job_key).await?;\n            if let Some(json) = job_json {\n                if let Ok(mut job) = Job::from_json(&json) {\n                    // Jobs in processing sets have status \"queued\" but should be \"processing\"\n                    if job.status == JobStatus::Queued {\n                        job.status = JobStatus::Processing;\n                    }\n                    if should_include_job(&job, status, age_days) {\n                        jobs.push(job);\n                    }\n                }\n            }\n        }\n    }\n\n    Ok(jobs)\n}\n\n// Helper function to check if a job matches the filter criteria\nfn should_include_job(job: &Job, status: Option<&Vec<JobStatus>>, age_days: Option<i64>) -> bool {\n    if let Some(status_list) = status {\n        if !status_list.contains(&job.status) {\n            return false;\n        }\n    }\n    if let Some(age_days) = age_days {\n        if let Some(created_at) = job.created_at {\n            let cutoff_date = Utc::now() - chrono::Duration::days(age_days);\n            if created_at > cutoff_date {\n                return false;\n            }\n        }\n    }\n    true\n}\n\n/// Clears jobs based on their status from the Redis queue.\n///\n/// This function removes all jobs with a status matching any of the statuses provided\n/// in the `status` argument. It searches through all queue keys and processing sets\n/// and removes matching jobs.\n///\n/// # Errors\n///\n/// This function will return an error if it fails\npub async fn clear_by_status(client: &RedisPool, status: Vec<JobStatus>) -> Result<()> {\n    let mut conn = get_connection(client).await?;\n\n    // Get all queue keys\n    let queue_pattern = format!(\"{QUEUE_KEY_PREFIX}*\");\n    let queue_keys: Vec<String> = redis::cmd(\"KEYS\")\n        .arg(&queue_pattern)\n        .query_async(&mut conn)\n        .await?;\n\n    // Get all processing keys\n    let processing_pattern = format!(\"{PROCESSING_KEY_PREFIX}*\");\n    let processing_keys: Vec<String> = redis::cmd(\"KEYS\")\n        .arg(&processing_pattern)\n        .query_async(&mut conn)\n        .await?;\n\n    // Get all job keys\n    let job_pattern = format!(\"{JOB_KEY_PREFIX}*\");\n    let job_keys: Vec<String> = redis::cmd(\"KEYS\")\n        .arg(&job_pattern)\n        .query_async(&mut conn)\n        .await?;\n\n    // Process queues\n    for queue_key in queue_keys {\n        // Get all jobs in the queue\n        let job_ids: Vec<String> = conn.lrange(&queue_key, 0, -1).await?;\n\n        // Process each job individually\n        for job_id in job_ids {\n            let job_key = format!(\"{JOB_KEY_PREFIX}{job_id}\");\n            let job_json: Option<String> = conn.get(&job_key).await?;\n            if let Some(json) = job_json {\n                if let Ok(job) = Job::from_json(&json) {\n                    if status.contains(&job.status) {\n                        let _: () = conn.lrem(&queue_key, 1, &job_id).await?;\n                        let _: () = conn.del(&job_key).await?;\n                    }\n                }\n            }\n        }\n    }\n\n    for processing_key in processing_keys {\n        let job_ids: Vec<String> = conn.smembers(&processing_key).await?;\n        for job_id in job_ids {\n            let job_key = format!(\"{JOB_KEY_PREFIX}{job_id}\");\n            let job_json: Option<String> = conn.get(&job_key).await?;\n            if let Some(json) = job_json {\n                if let Ok(mut job) = Job::from_json(&json) {\n                    if job.status == JobStatus::Queued {\n                        job.status = JobStatus::Processing;\n                    }\n                    if status.contains(&job.status) {\n                        let _: () = conn.srem(&processing_key, &job_id).await?;\n                        let _: () = conn.del(&job_key).await?;\n                    }\n                }\n            }\n        }\n    }\n\n    for job_key in job_keys {\n        let job_json: Option<String> = conn.get(&job_key).await?;\n        if let Some(json) = job_json {\n            if let Ok(job) = Job::from_json(&json) {\n                if status.contains(&job.status) {\n                    let _: () = conn.del(&job_key).await?;\n                }\n            }\n        }\n    }\n\n    Ok(())\n}\n\n/// Clears jobs older than the specified number of days from the Redis queue.\n///\n/// This function removes all jobs that were created more than `age_days` days ago\n/// and have a status matching any of the statuses provided in the `status` argument.\n/// It searches through all queue keys and processing sets and removes matching jobs.\n///\n/// # Errors\n///\n/// This function will return an error if it fails\npub async fn clear_jobs_older_than(\n    client: &RedisPool,\n    age_days: i64,\n    status: Option<&Vec<JobStatus>>,\n) -> Result<()> {\n    let mut conn = get_connection(client).await?;\n    let cutoff_date = Utc::now() - chrono::Duration::days(age_days);\n\n    // Get all queue keys\n    let queue_pattern = format!(\"{QUEUE_KEY_PREFIX}*\");\n    let queue_keys: Vec<String> = redis::cmd(\"KEYS\")\n        .arg(&queue_pattern)\n        .query_async(&mut conn)\n        .await?;\n\n    // Get all processing keys\n    let processing_pattern = format!(\"{PROCESSING_KEY_PREFIX}*\");\n    let processing_keys: Vec<String> = redis::cmd(\"KEYS\")\n        .arg(&processing_pattern)\n        .query_async(&mut conn)\n        .await?;\n\n    // Get all job keys\n    let job_pattern = format!(\"{JOB_KEY_PREFIX}*\");\n    let job_keys: Vec<String> = redis::cmd(\"KEYS\")\n        .arg(&job_pattern)\n        .query_async(&mut conn)\n        .await?;\n\n    // Process queues\n    for queue_key in queue_keys {\n        // Get all jobs in the queue\n        let job_ids: Vec<String> = conn.lrange(&queue_key, 0, -1).await?;\n\n        // Process each job individually\n        for job_id in job_ids {\n            let job_key = format!(\"{JOB_KEY_PREFIX}{job_id}\");\n            let job_json: Option<String> = conn.get(&job_key).await?;\n            if let Some(json) = job_json {\n                if let Ok(job) = Job::from_json(&json) {\n                    let should_remove = job.created_at.is_some_and(|created_at| {\n                        created_at < cutoff_date && status.map_or(true, |s| s.contains(&job.status))\n                    });\n                    if should_remove {\n                        let _: () = conn.lrem(&queue_key, 1, &job_id).await?;\n                        let _: () = conn.del(&job_key).await?;\n                    }\n                }\n            }\n        }\n    }\n\n    for processing_key in processing_keys {\n        let job_ids: Vec<String> = conn.smembers(&processing_key).await?;\n        for job_id in job_ids {\n            let job_key = format!(\"{JOB_KEY_PREFIX}{job_id}\");\n            let job_json: Option<String> = conn.get(&job_key).await?;\n            if let Some(json) = job_json {\n                if let Ok(mut job) = Job::from_json(&json) {\n                    if job.status == JobStatus::Queued {\n                        job.status = JobStatus::Processing;\n                    }\n                    let should_remove = job.created_at.is_some_and(|created_at| {\n                        created_at < cutoff_date && status.map_or(true, |s| s.contains(&job.status))\n                    });\n                    if should_remove {\n                        let _: () = conn.srem(&processing_key, &job_id).await?;\n                        let _: () = conn.del(&job_key).await?;\n                    }\n                }\n            }\n        }\n    }\n\n    for job_key in job_keys {\n        let job_json: Option<String> = conn.get(&job_key).await?;\n        if let Some(json) = job_json {\n            if let Ok(job) = Job::from_json(&json) {\n                let should_remove = job.created_at.is_some_and(|created_at| {\n                    created_at < cutoff_date && status.map_or(true, |s| s.contains(&job.status))\n                });\n                if should_remove {\n                    let _: () = conn.del(&job_key).await?;\n                }\n            }\n        }\n    }\n\n    Ok(())\n}\n\n/// Requeues failed or stalled jobs that are older than a specified number of minutes.\n///\n/// This function finds jobs in processing sets that have been there for longer than\n/// `age_minutes` and moves them back to their respective queues. This is useful for\n/// recovering from job failures or worker crashes.\n///\n/// # Errors\n///\n/// This function will return an error if it fails to interact with Redis\npub async fn requeue(client: &RedisPool, age_minutes: &i64) -> Result<()> {\n    let mut conn = get_connection(client).await?;\n    let cutoff_time = Utc::now() - chrono::Duration::minutes(*age_minutes);\n    let mut requeued_counts: HashMap<String, usize> = HashMap::new();\n\n    // Get all processing set keys\n    let processing_pattern = format!(\"{PROCESSING_KEY_PREFIX}*\");\n    let processing_keys: Vec<String> = redis::cmd(\"KEYS\")\n        .arg(&processing_pattern)\n        .query_async(&mut conn)\n        .await?;\n\n    // Process each processing set\n    for processing_key in processing_keys {\n        // Extract queue name from processing key\n        let queue_name = processing_key\n            .trim_start_matches(PROCESSING_KEY_PREFIX)\n            .to_string();\n        let queue_key = format!(\"{QUEUE_KEY_PREFIX}{queue_name}\");\n\n        // Get all jobs in the processing set\n        let job_ids: Vec<String> = conn.smembers(&processing_key).await?;\n\n        // Check each job in the processing set\n        for job_id in job_ids {\n            let job_key = format!(\"{JOB_KEY_PREFIX}{job_id}\");\n            let job_json: Option<String> = conn.get(&job_key).await?;\n            if let Some(json) = job_json {\n                if let Ok(mut job) = Job::from_json(&json) {\n                    let should_requeue = if let Some(updated_at) = job.updated_at {\n                        updated_at < cutoff_time\n                    } else if let Some(created_at) = job.created_at {\n                        created_at < cutoff_time\n                    } else {\n                        false\n                    };\n                    if should_requeue {\n                        job.status = JobStatus::Queued;\n                        job.updated_at = Some(Utc::now());\n                        let updated_json = job.to_json()?;\n                        let _: () = conn.srem(&processing_key, &job_id).await?;\n                        let _: () = conn.set(&job_key, &updated_json).await?;\n                        let _: () = conn.rpush(&queue_key, &job_id).await?;\n                        *requeued_counts.entry(queue_name.clone()).or_insert(0) += 1;\n                    }\n                }\n            }\n        }\n    }\n\n    let failed_pattern = \"failed:*\";\n    let failed_keys: Vec<String> = redis::cmd(\"KEYS\")\n        .arg(failed_pattern)\n        .query_async(&mut conn)\n        .await?;\n\n    for failed_key in failed_keys {\n        let queue_name = failed_key.trim_start_matches(\"failed:\").to_string();\n        let queue_key = format!(\"{QUEUE_KEY_PREFIX}{queue_name}\");\n        let job_ids: Vec<String> = conn.smembers(&failed_key).await?;\n\n        for job_id in job_ids {\n            let job_key = format!(\"{JOB_KEY_PREFIX}{job_id}\");\n            let job_json: Option<String> = conn.get(&job_key).await?;\n            if let Some(json) = job_json {\n                if let Ok(mut job) = Job::from_json(&json) {\n                    let should_requeue = if let Some(updated_at) = job.updated_at {\n                        updated_at < cutoff_time && job.status == JobStatus::Failed\n                    } else {\n                        false\n                    };\n                    if should_requeue {\n                        job.status = JobStatus::Queued;\n                        job.updated_at = Some(Utc::now());\n                        let updated_json = job.to_json()?;\n                        let _: () = conn.srem(&failed_key, &job_id).await?;\n                        let _: () = conn.set(&job_key, &updated_json).await?;\n                        let _: () = conn.rpush(&queue_key, &job_id).await?;\n                        *requeued_counts.entry(queue_name.clone()).or_insert(0) += 1;\n                    }\n                }\n            }\n        }\n    }\n\n    for (queue, count) in requeued_counts {\n        if count > 0 {\n            debug!(queue = queue, count = count, \"requeued jobs\");\n        }\n    }\n    Ok(())\n}\n\n/// Cancels jobs with the specified name in the Redis queue.\n///\n/// This function updates the status of jobs that match the provided `job_name`\n/// from [`JobStatus::Queued`] to [`JobStatus::Cancelled`]. Jobs are searched for in all queue keys,\n/// and only those that are currently in the [`JobStatus::Queued`] state will be affected.\n///\n/// # Errors\n///\n/// This function will return an error if it fails\npub async fn cancel_jobs_by_name(client: &RedisPool, job_name: &str) -> Result<()> {\n    let mut conn = get_connection(client).await?;\n\n    // Get all queue keys\n    let queue_pattern = format!(\"{QUEUE_KEY_PREFIX}*\");\n    let queue_keys: Vec<String> = redis::cmd(\"KEYS\")\n        .arg(&queue_pattern)\n        .query_async(&mut conn)\n        .await?;\n\n    // Process each queue\n    for queue_key in queue_keys {\n        // Get all jobs in the queue\n        let job_ids: Vec<String> = conn.lrange(&queue_key, 0, -1).await?;\n        for job_id in job_ids {\n            let job_key = format!(\"{JOB_KEY_PREFIX}{job_id}\");\n            let job_json: Option<String> = conn.get(&job_key).await?;\n            if let Some(json) = job_json {\n                if let Ok(mut job) = Job::from_json(&json) {\n                    if job.name == job_name && job.status == JobStatus::Queued {\n                        job.status = JobStatus::Cancelled;\n                        job.updated_at = Some(Utc::now());\n                        let updated_json = job.to_json()?;\n                        let _: () = conn.lrem(&queue_key, 1, &job_id).await?;\n                        let _: () = conn.set(&job_key, &updated_json).await?;\n                        let cancelled_key = format!(\n                            \"cancelled:{}\",\n                            queue_key.trim_start_matches(QUEUE_KEY_PREFIX)\n                        );\n                        let _: () = conn.sadd(&cancelled_key, &job_id).await?;\n                    }\n                }\n            }\n        }\n    }\n    Ok(())\n}\n\npub const DEFAULT_QUEUES: &[&str] = &[\"default\", \"mailer\"];\n\npub fn get_queues(config_queues: &Option<Vec<String>>) -> Vec<String> {\n    let mut queues = DEFAULT_QUEUES\n        .iter()\n        .map(ToString::to_string)\n        .collect::<Vec<_>>();\n    if let Some(config_queues) = config_queues {\n        for q in config_queues {\n            if !queues.iter().any(|aq| q == aq) {\n                queues.push(q.clone());\n            }\n        }\n    }\n    queues\n}\n\npub struct RunOpts {\n    pub num_workers: u32,\n    pub poll_interval_sec: u32,\n    pub queues: Option<Vec<String>>,\n}\n\n/// Create this provider\n///\n/// # Errors\n///\n/// This function will return an error if it fails\n#[allow(clippy::unused_async)]\npub async fn create_provider(qcfg: &RedisQueueConfig) -> Result<Queue> {\n    let client = connect(&qcfg.uri)?;\n    let registry = JobRegistry::new();\n    let token = CancellationToken::new();\n    let run_opts = RunOpts {\n        num_workers: qcfg.num_workers,\n        poll_interval_sec: 1,\n        queues: qcfg.queues.clone(),\n    };\n    debug!(\n        queues = ?qcfg.queues,\n        num_workers = qcfg.num_workers,\n        \"creating Redis queue provider\"\n    );\n    tokio::time::sleep(std::time::Duration::from_secs(3)).await;\n    Ok(Queue::Redis(\n        client,\n        Arc::new(tokio::sync::Mutex::new(registry)),\n        run_opts,\n        token,\n    ))\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use crate::tests_cfg::redis::setup_redis_container;\n    use chrono::Utc;\n    use testcontainers::{ContainerAsync, GenericImage};\n\n    async fn setup_redis() -> (RedisPool, ContainerAsync<GenericImage>) {\n        let (redis_url, container) = setup_redis_container().await;\n        let client = connect(&redis_url).expect(\"connect to redis\");\n        (client, container)\n    }\n\n    async fn get_test_connection(client: &RedisPool) -> Connection {\n        client\n            .get_multiplexed_async_connection()\n            .await\n            .expect(\"get connection\")\n    }\n\n    async fn redis_seed_data(client: &RedisPool) -> Result<()> {\n        // Creating processed jobs\n        let now = Utc::now();\n        for i in 0..5 {\n            let complete_job = Job {\n                id: format!(\"job{i}\"),\n                name: \"TestJob\".to_string(),\n                data: serde_json::json!({\"counter\": i}),\n                status: JobStatus::Completed,\n                run_at: now,\n                interval: None,\n                created_at: Some(now - chrono::Duration::days(15)),\n                updated_at: Some(now - chrono::Duration::days(15)),\n                tags: None,\n            };\n\n            let mut conn = get_connection(client).await?;\n            // Store job data\n            let _: () = conn\n                .set(format!(\"{JOB_KEY_PREFIX}job{i}\"), complete_job.to_json()?)\n                .await?;\n        }\n\n        // Create queued jobs\n        let args = serde_json::json!({\"hello\": \"world\"});\n        enqueue(client, \"TestJob\".to_string(), None, args, None).await?;\n\n        // Create job with tags\n        let args = serde_json::json!({\"hello\": \"tagged\"});\n        enqueue(\n            client,\n            \"TaggedJob\".to_string(),\n            None,\n            args,\n            Some(vec![\"important\".to_string(), \"urgent\".to_string()]),\n        )\n        .await?;\n\n        Ok(())\n    }\n\n    async fn get_all_jobs(client: &RedisPool) -> Vec<Job> {\n        get_jobs(client, None, None).await.unwrap_or_default()\n    }\n\n    #[tokio::test]\n    async fn test_can_dequeue_redis() {\n        let (client, _container) = setup_redis().await;\n        redis_seed_data(&client).await.expect(\"seed data\");\n\n        // Dequeue job\n        let queues = vec![\"default\".to_string()];\n        let mut conn = get_test_connection(&client).await;\n        let job_opt = dequeue_with_conn(&mut conn, &queues, &[])\n            .await\n            .expect(\"dequeue\");\n\n        // Verify job was dequeued\n        assert!(job_opt.is_some());\n    }\n\n    #[tokio::test]\n    async fn test_can_clear_redis() {\n        // Setup Redis directly with testcontainer\n        let (client, _container) = setup_redis().await;\n\n        // Seed data\n        if let Err(e) = redis_seed_data(&client).await {\n            panic!(\"Failed to seed data: {e}\");\n        }\n\n        // Verify data exists first\n        let mut conn = get_connection(&client).await.expect(\"get connection\");\n        let keys: Vec<String> = redis::cmd(\"KEYS\")\n            .arg(\"*\")\n            .query_async(&mut conn)\n            .await\n            .expect(\"get keys\");\n        assert!(!keys.is_empty(), \"Should have keys before clearing\");\n\n        // Clear data\n        assert!(clear(&client).await.is_ok());\n\n        // Verify data is gone\n        let keys: Vec<String> = redis::cmd(\"KEYS\")\n            .arg(\"*\")\n            .query_async(&mut conn)\n            .await\n            .expect(\"get keys\");\n        assert!(keys.is_empty(), \"All keys should be removed after clearing\");\n    }\n\n    #[tokio::test]\n    async fn test_can_enqueue_redis() {\n        // Setup Redis directly with testcontainer\n        let (client, _container) = setup_redis().await;\n\n        // Test enqueue\n        let args = serde_json::json!({\"user_id\": 42});\n        assert!(\n            enqueue(&client, \"PasswordReset\".to_string(), None, args, None)\n                .await\n                .is_ok()\n        );\n\n        // Verify job was created\n        let jobs = get_all_jobs(&client).await;\n        assert_eq!(jobs.len(), 1);\n\n        let job = &jobs[0];\n        assert_eq!(job.name, \"PasswordReset\");\n        assert_eq!(job.status, JobStatus::Queued);\n        assert_eq!(job.data, serde_json::json!({\"user_id\": 42}));\n    }\n\n    #[tokio::test]\n    async fn test_can_enqueue_with_queue_redis() {\n        let (client, _container) = setup_redis().await;\n\n        // Test enqueue with custom queue\n        let args = serde_json::json!({\"email\": \"user@example.com\"});\n        assert!(enqueue(\n            &client,\n            \"EmailNotification\".to_string(),\n            Some(\"mailer\".to_string()),\n            args,\n            None\n        )\n        .await\n        .is_ok());\n\n        // Verify job was created in correct queue first\n        let mut conn = get_test_connection(&client).await;\n        let queue_key = format!(\"{QUEUE_KEY_PREFIX}mailer\");\n        let queue_len: i64 = conn.llen(&queue_key).await.expect(\"get queue length\");\n        assert_eq!(queue_len, 1);\n\n        // Test dequeue from mailer queue\n        let queues = vec![\"mailer\".to_string()];\n        let _job_opt = dequeue_with_conn(&mut conn, &queues, &[])\n            .await\n            .expect(\"dequeue\");\n\n        // Queue should now be empty\n        let queue_len: i64 = conn.llen(&queue_key).await.expect(\"get queue length\");\n        assert_eq!(queue_len, 0);\n    }\n\n    #[tokio::test]\n    async fn test_can_complete_job_redis() {\n        let (client, _container) = setup_redis().await;\n\n        // Add job\n        let args = serde_json::json!({\"task\": \"test\"});\n        assert!(enqueue(&client, \"TestJob\".to_string(), None, args, None)\n            .await\n            .is_ok());\n\n        // Dequeue job\n        let queues = vec![\"default\".to_string()];\n        let mut conn = get_test_connection(&client).await;\n        let job_opt = dequeue_with_conn(&mut conn, &queues, &[])\n            .await\n            .expect(\"dequeue\");\n        let (job, queue) = job_opt.unwrap();\n\n        // Complete job\n        assert!(complete_job_with_conn(&mut conn, &job.id, &queue, None)\n            .await\n            .is_ok());\n\n        // Verify job is not in processing set\n        let processing_key = format!(\"{PROCESSING_KEY_PREFIX}{queue}\");\n        let is_member: bool = conn\n            .sismember(&processing_key, &job.id)\n            .await\n            .expect(\"check membership\");\n        assert!(!is_member);\n\n        // Verify job status is updated to Completed\n        let job_key = String::from(JOB_KEY_PREFIX) + &job.id;\n        let job_json: String = conn.get(&job_key).await.expect(\"get job\");\n        let completed_job = Job::from_json(&job_json).expect(\"parse job\");\n        assert_eq!(\n            completed_job.status,\n            JobStatus::Completed,\n            \"Job status should be Completed after completion\"\n        );\n    }\n\n    #[tokio::test]\n    async fn test_can_complete_job_with_interval_redis() {\n        let (client, _container) = setup_redis().await;\n\n        // Add job\n        let args = serde_json::json!({\"task\": \"recurring\"});\n        assert!(\n            enqueue(&client, \"RecurringJob\".to_string(), None, args, None)\n                .await\n                .is_ok()\n        );\n\n        // Dequeue job\n        let queues = vec![\"default\".to_string()];\n        let mut conn = get_test_connection(&client).await;\n        let job_opt = dequeue_with_conn(&mut conn, &queues, &[])\n            .await\n            .expect(\"dequeue\");\n        let (job, queue) = job_opt.unwrap();\n\n        // Complete job with interval to reschedule\n        assert!(\n            complete_job_with_conn(&mut conn, &job.id, &queue, Some(1000))\n                .await\n                .is_ok()\n        );\n\n        // Verify job is back in queue\n        let queue_key = format!(\"{QUEUE_KEY_PREFIX}{queue}\");\n        let queue_len: i64 = conn.llen(&queue_key).await.expect(\"get queue length\");\n        assert_eq!(queue_len, 1);\n\n        // Get the job ID from the queue\n        let job_id: String = conn.lindex(&queue_key, 0).await.expect(\"get job id\");\n\n        // Get the job data using the ID\n        let job_key = format!(\"{JOB_KEY_PREFIX}{job_id}\");\n        let job_json: String = conn.get(&job_key).await.expect(\"get job data\");\n        let requeued_job = Job::from_json(&job_json).expect(\"parse job\");\n\n        // Verify the job has future run_at time\n        assert!(requeued_job.run_at > Utc::now());\n    }\n\n    #[tokio::test]\n    async fn test_can_fail_job_redis() {\n        let (client, _container) = setup_redis().await;\n\n        // Add job\n        let args = serde_json::json!({\"task\": \"test\"});\n        assert!(enqueue(&client, \"TestJob\".to_string(), None, args, None)\n            .await\n            .is_ok());\n\n        // Dequeue job\n        let queues = vec![\"default\".to_string()];\n        let mut conn = get_test_connection(&client).await;\n        let job_opt = dequeue_with_conn(&mut conn, &queues, &[])\n            .await\n            .expect(\"dequeue\");\n        let (job, queue) = job_opt.unwrap();\n\n        // Fail job\n        let error = Error::string(\"test failure\");\n        assert!(fail_job_with_conn(&mut conn, &job.id, &queue, &error)\n            .await\n            .is_ok());\n\n        // Verify job is not in processing set\n        let processing_key = format!(\"{PROCESSING_KEY_PREFIX}{queue}\");\n        let is_member: bool = conn\n            .sismember(&processing_key, &job.id)\n            .await\n            .expect(\"check membership\");\n        assert!(!is_member);\n\n        // Verify job has error data\n        let job_key = String::from(JOB_KEY_PREFIX) + &job.id;\n        let job_json: String = conn.get(&job_key).await.expect(\"get job\");\n        let failed_job = Job::from_json(&job_json).expect(\"parse job\");\n        assert_eq!(failed_job.status, JobStatus::Failed);\n        assert!(failed_job.data.get(\"error\").is_some());\n    }\n\n    #[tokio::test]\n    async fn test_can_get_jobs_redis() {\n        // Setup Redis directly with testcontainer\n        let (client, _container) = setup_redis().await;\n\n        // Seed data\n        redis_seed_data(&client).await.expect(\"seed data\");\n\n        // Get all jobs\n        let all_jobs = get_jobs(&client, None, None).await.expect(\"get all jobs\");\n        assert!(!all_jobs.is_empty());\n\n        // Get jobs by status\n        let queued_jobs = get_jobs(&client, Some(&vec![JobStatus::Queued]), None)\n            .await\n            .expect(\"get queued jobs\");\n        for job in &queued_jobs {\n            assert_eq!(job.status, JobStatus::Queued);\n        }\n\n        let failed_jobs = get_jobs(&client, Some(&vec![JobStatus::Failed]), None)\n            .await\n            .expect(\"get failed jobs\");\n        for job in &failed_jobs {\n            assert_eq!(job.status, JobStatus::Failed);\n        }\n\n        // Verify combined status filter\n        let combined_jobs = get_jobs(\n            &client,\n            Some(&vec![JobStatus::Completed, JobStatus::Failed]),\n            None,\n        )\n        .await\n        .expect(\"get combined jobs\");\n        for job in &combined_jobs {\n            assert!(job.status == JobStatus::Completed || job.status == JobStatus::Failed);\n        }\n    }\n\n    #[tokio::test]\n    async fn test_job_registry_redis() {\n        // Setup Redis directly with testcontainer\n        let (client, _container) = setup_redis().await;\n\n        // Create job registry\n        let mut registry = JobRegistry::new();\n\n        // Create a mock worker\n        struct TestWorker;\n        #[async_trait::async_trait]\n        impl BackgroundWorker<String> for TestWorker {\n            fn build(_ctx: &crate::app::AppContext) -> Self {\n                Self\n            }\n\n            async fn perform(&self, args: String) -> crate::Result<()> {\n                assert_eq!(args, \"test args\");\n                Ok(())\n            }\n        }\n\n        // Register worker\n        assert!(registry\n            .register_worker(\"TestJob\".to_string(), TestWorker)\n            .is_ok());\n\n        // Add job\n        let args = serde_json::json!(\"test args\");\n        assert!(enqueue(&client, \"TestJob\".to_string(), None, args, None)\n            .await\n            .is_ok());\n\n        // Run registry with worker for a short time\n        let opts = RunOpts {\n            num_workers: 1,\n            poll_interval_sec: 1,\n            queues: None,\n        };\n\n        let token = CancellationToken::new();\n        let worker_handles = registry.run(&client, &opts, &token, &[] as &[String]);\n\n        // Allow some time for job processing\n        tokio::time::sleep(Duration::from_secs(2)).await;\n\n        // Stop workers\n        token.cancel();\n        for handle in worker_handles {\n            let _ = handle.await;\n        }\n    }\n\n    #[tokio::test]\n    async fn test_job_filtering_by_tags() {\n        let (client, _container) = setup_redis().await;\n\n        // Clear any existing data for clean test environment\n        assert!(clear(&client).await.is_ok());\n\n        // Create jobs with different tags using the proper enqueue function\n        let args1 = serde_json::json!({\"task\": \"task1\"});\n        assert!(enqueue(\n            &client,\n            \"TaggedJob\".to_string(),\n            Some(\"default\".to_string()),\n            args1,\n            Some(vec![\"tag1\".to_string(), \"common\".to_string()])\n        )\n        .await\n        .is_ok());\n\n        let args2 = serde_json::json!({\"task\": \"task2\"});\n        assert!(enqueue(\n            &client,\n            \"TaggedJob\".to_string(),\n            Some(\"default\".to_string()),\n            args2,\n            Some(vec![\"tag2\".to_string(), \"common\".to_string()])\n        )\n        .await\n        .is_ok());\n\n        let args3 = serde_json::json!({\"task\": \"task3\"});\n        assert!(enqueue(\n            &client,\n            \"TaggedJob\".to_string(),\n            Some(\"default\".to_string()),\n            args3,\n            Some(vec![\"tag3\".to_string()])\n        )\n        .await\n        .is_ok());\n\n        // Test dequeue with tag1 filter\n        let queues = vec![\"default\".to_string()];\n        let mut conn = get_test_connection(&client).await;\n        let job_opt = dequeue_with_conn(&mut conn, &queues, &[\"tag1\".to_string()])\n            .await\n            .expect(\"dequeue with tag1\");\n\n        assert!(job_opt.is_some(), \"Should have found a job with tag1\");\n        if let Some((dequeued_job, _)) = job_opt {\n            assert_eq!(dequeued_job.name, \"TaggedJob\");\n            assert!(dequeued_job.tags.is_some(), \"Job should have tags\");\n            let tags = dequeued_job.tags.unwrap();\n            assert!(\n                tags.contains(&\"tag1\".to_string()),\n                \"Job should contain tag1\"\n            );\n        }\n    }\n\n    #[tokio::test]\n    async fn test_ping_redis() {\n        let (client, _container) = setup_redis().await;\n        ping(&client).await.expect(\"ping redis\");\n    }\n\n    #[tokio::test]\n    async fn test_can_clear_by_status_redis() {\n        // Setup Redis directly with testcontainer using the reliable method\n        let (client, _container) = setup_redis().await;\n\n        // Seed data with error handling\n        match redis_seed_data(&client).await {\n            Ok(()) => (),\n            Err(e) => panic!(\"Failed to seed data: {e}\"),\n        }\n\n        // Count jobs by status before clearing\n        let all_jobs = get_all_jobs(&client).await;\n        let completed_count = all_jobs\n            .iter()\n            .filter(|j| j.status == JobStatus::Completed)\n            .count();\n        let failed_count = all_jobs\n            .iter()\n            .filter(|j| j.status == JobStatus::Failed)\n            .count();\n        let total_count = all_jobs.len();\n\n        // Clear completed and failed jobs\n        assert!(\n            clear_by_status(&client, vec![JobStatus::Completed, JobStatus::Failed])\n                .await\n                .is_ok()\n        );\n\n        // Verify jobs were cleared\n        let remaining_jobs = get_all_jobs(&client).await;\n        assert_eq!(\n            remaining_jobs.len(),\n            total_count - completed_count - failed_count\n        );\n        assert_eq!(\n            remaining_jobs\n                .iter()\n                .filter(|j| j.status == JobStatus::Completed)\n                .count(),\n            0\n        );\n        assert_eq!(\n            remaining_jobs\n                .iter()\n                .filter(|j| j.status == JobStatus::Failed)\n                .count(),\n            0\n        );\n    }\n\n    #[tokio::test]\n    async fn test_can_clear_jobs_older_than_with_status_redis() {\n        // Setup with clean Redis\n        let (client, _container) = setup_redis().await;\n\n        // Add specific test jobs with known ages and statuses\n        let mut conn = get_connection(&client).await.expect(\"get connection\");\n\n        // Create an old failed job (older than 10 days)\n        let old_failed_job = Job {\n            id: \"old_failed_job_test\".to_string(),\n            name: \"OldFailedTestJob\".to_string(),\n            data: serde_json::json!({\"test\": \"data\"}),\n            status: JobStatus::Failed,\n            run_at: Utc::now(),\n            interval: None,\n            created_at: Some(Utc::now() - chrono::Duration::days(15)),\n            updated_at: Some(Utc::now() - chrono::Duration::days(15)),\n            tags: None,\n        };\n\n        // Create an old completed job (older than 10 days)\n        let old_completed_job = Job {\n            id: \"old_completed_job_test\".to_string(),\n            name: \"OldCompletedTestJob\".to_string(),\n            data: serde_json::json!({\"test\": \"data\"}),\n            status: JobStatus::Completed,\n            run_at: Utc::now(),\n            interval: None,\n            created_at: Some(Utc::now() - chrono::Duration::days(15)),\n            updated_at: Some(Utc::now() - chrono::Duration::days(15)),\n            tags: None,\n        };\n\n        // Store both jobs directly\n        let old_failed_job_json = old_failed_job.to_json().expect(\"serialize old failed job\");\n        let old_completed_job_json = old_completed_job\n            .to_json()\n            .expect(\"serialize old completed job\");\n\n        let old_failed_job_key = String::from(JOB_KEY_PREFIX) + &old_failed_job.id;\n        let old_completed_job_key = String::from(JOB_KEY_PREFIX) + &old_completed_job.id;\n\n        let _: () = conn\n            .set(&old_failed_job_key, &old_failed_job_json)\n            .await\n            .expect(\"set old failed job\");\n        let _: () = conn\n            .set(&old_completed_job_key, &old_completed_job_json)\n            .await\n            .expect(\"set old completed job\");\n\n        // Clear only failed jobs older than 10 days\n        assert!(\n            clear_jobs_older_than(&client, 10, Some(&vec![JobStatus::Failed]))\n                .await\n                .is_ok()\n        );\n\n        // Check if old failed job was removed and old completed job still exists\n        let exists_failed_after: bool = conn\n            .exists(&old_failed_job_key)\n            .await\n            .expect(\"check failed job after\");\n        let exists_completed_after: bool = conn\n            .exists(&old_completed_job_key)\n            .await\n            .expect(\"check completed job after\");\n\n        assert!(!exists_failed_after, \"Old failed job should be removed\");\n        assert!(\n            exists_completed_after,\n            \"Old completed job should still exist\"\n        );\n    }\n\n    #[tokio::test]\n    async fn test_can_get_jobs_with_age_redis() {\n        // Setup Redis directly with testcontainer\n        let (client, _container) = setup_redis().await;\n\n        // Seed data with jobs of different ages\n        redis_seed_data(&client).await.expect(\"seed data\");\n\n        // Get jobs older than 10 days\n        let old_jobs = get_jobs(&client, None, Some(10))\n            .await\n            .expect(\"get old jobs\");\n        for job in &old_jobs {\n            if let Some(created_at) = job.created_at {\n                assert!(created_at <= Utc::now() - chrono::Duration::days(10));\n            }\n        }\n\n        // Get old jobs with specific status\n        let old_failed_jobs = get_jobs(&client, Some(&vec![JobStatus::Failed]), Some(10))\n            .await\n            .expect(\"get old failed jobs\");\n        for job in &old_failed_jobs {\n            assert_eq!(job.status, JobStatus::Failed);\n            if let Some(created_at) = job.created_at {\n                assert!(created_at <= Utc::now() - chrono::Duration::days(10));\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/bgworker/snapshots/loco_rs__bgworker__pg__tests__can_complete_job_with_interval.snap",
    "content": "---\nsource: src/bgworker/pg.rs\nexpression: after_complete_job\nsnapshot_kind: text\n---\nJob {\n    id: \"<REDACTED>\",\n    name: \"PasswordChangeNotification\",\n    data: Object {\n        \"change_time\": String(\"<REDACTED>\"),\n        \"email\": String(\"user12@example.com\"),\n        \"user_id\": Number(134),\n    },\n    status: Queued,\n    run_at: <REDACTED>,\n    interval: None,\n    created_at: Some(\n        <REDACTED>,\n    ),\n    updated_at: Some(\n        <REDACTED>,\n    ),\n    tags: None,\n}\n"
  },
  {
    "path": "src/bgworker/snapshots/loco_rs__bgworker__pg__tests__can_dequeue.snap",
    "content": "---\nsource: src/bgworker/pg.rs\nexpression: job_after_dequeue\nsnapshot_kind: text\n---\nJob {\n    id: \"<REDACTED>\",\n    name: \"PasswordChangeNotification\",\n    data: Object {\n        \"user_id\": Number(1),\n    },\n    status: Processing,\n    run_at: <REDACTED>,\n    interval: None,\n    created_at: Some(\n        <REDACTED>,\n    ),\n    updated_at: Some(\n        <REDACTED>,\n    ),\n    tags: None,\n}\n"
  },
  {
    "path": "src/bgworker/snapshots/loco_rs__bgworker__pg__tests__can_enqueue.snap",
    "content": "---\nsource: src/bgworker/pg.rs\nexpression: jobs\nsnapshot_kind: text\n---\n[\n    Job {\n        id: \"<REDACTED>\",\n        name: \"PasswordChangeNotification\",\n        data: Object {\n            \"user_id\": Number(1),\n        },\n        status: Queued,\n        run_at: <REDACTED>,\n        interval: None,\n        created_at: Some(\n            <REDACTED>,\n        ),\n        updated_at: Some(\n            <REDACTED>,\n        ),\n        tags: None,\n    },\n]\n"
  },
  {
    "path": "src/bgworker/snapshots/loco_rs__bgworker__pg__tests__can_fail_job.snap",
    "content": "---\nsource: src/bgworker/pg.rs\nexpression: after_fail_job\nsnapshot_kind: text\n---\nJob {\n    id: \"<REDACTED>\",\n    name: \"SendInvoice\",\n    data: Object {\n        \"email\": String(\"user13@example.com\"),\n        \"error\": String(\"some error\"),\n        \"invoice_id\": String(\"INV-2024-01\"),\n        \"user_id\": Number(135),\n    },\n    status: Failed,\n    run_at: <REDACTED>,\n    interval: None,\n    created_at: Some(\n        <REDACTED>,\n    ),\n    updated_at: Some(\n        <REDACTED>,\n    ),\n    tags: None,\n}\n"
  },
  {
    "path": "src/bgworker/snapshots/loco_rs__bgworker__pg__tests__can_initialize_database.snap",
    "content": "---\nsource: src/bgworker/pg.rs\nexpression: table_info\nsnapshot_kind: text\n---\n[\n    TableInfo {\n        table_schema: Some(\n            \"public\",\n        ),\n        column_name: Some(\n            \"tags\",\n        ),\n        column_default: None,\n        is_nullable: Some(\n            \"YES\",\n        ),\n        data_type: Some(\n            \"jsonb\",\n        ),\n        is_updatable: Some(\n            \"YES\",\n        ),\n    },\n    TableInfo {\n        table_schema: Some(\n            \"public\",\n        ),\n        column_name: Some(\n            \"interval\",\n        ),\n        column_default: None,\n        is_nullable: Some(\n            \"YES\",\n        ),\n        data_type: Some(\n            \"bigint\",\n        ),\n        is_updatable: Some(\n            \"YES\",\n        ),\n    },\n    TableInfo {\n        table_schema: Some(\n            \"public\",\n        ),\n        column_name: Some(\n            \"created_at\",\n        ),\n        column_default: Some(\n            \"now()\",\n        ),\n        is_nullable: Some(\n            \"NO\",\n        ),\n        data_type: Some(\n            \"timestamp with time zone\",\n        ),\n        is_updatable: Some(\n            \"YES\",\n        ),\n    },\n    TableInfo {\n        table_schema: Some(\n            \"public\",\n        ),\n        column_name: Some(\n            \"updated_at\",\n        ),\n        column_default: Some(\n            \"now()\",\n        ),\n        is_nullable: Some(\n            \"NO\",\n        ),\n        data_type: Some(\n            \"timestamp with time zone\",\n        ),\n        is_updatable: Some(\n            \"YES\",\n        ),\n    },\n    TableInfo {\n        table_schema: Some(\n            \"public\",\n        ),\n        column_name: Some(\n            \"task_data\",\n        ),\n        column_default: None,\n        is_nullable: Some(\n            \"NO\",\n        ),\n        data_type: Some(\n            \"jsonb\",\n        ),\n        is_updatable: Some(\n            \"YES\",\n        ),\n    },\n    TableInfo {\n        table_schema: Some(\n            \"public\",\n        ),\n        column_name: Some(\n            \"run_at\",\n        ),\n        column_default: None,\n        is_nullable: Some(\n            \"NO\",\n        ),\n        data_type: Some(\n            \"timestamp with time zone\",\n        ),\n        is_updatable: Some(\n            \"YES\",\n        ),\n    },\n    TableInfo {\n        table_schema: Some(\n            \"public\",\n        ),\n        column_name: Some(\n            \"name\",\n        ),\n        column_default: None,\n        is_nullable: Some(\n            \"NO\",\n        ),\n        data_type: Some(\n            \"character varying\",\n        ),\n        is_updatable: Some(\n            \"YES\",\n        ),\n    },\n    TableInfo {\n        table_schema: Some(\n            \"public\",\n        ),\n        column_name: Some(\n            \"id\",\n        ),\n        column_default: None,\n        is_nullable: Some(\n            \"NO\",\n        ),\n        data_type: Some(\n            \"character varying\",\n        ),\n        is_updatable: Some(\n            \"YES\",\n        ),\n    },\n    TableInfo {\n        table_schema: Some(\n            \"public\",\n        ),\n        column_name: Some(\n            \"status\",\n        ),\n        column_default: Some(\n            \"'queued'::character varying\",\n        ),\n        is_nullable: Some(\n            \"NO\",\n        ),\n        data_type: Some(\n            \"character varying\",\n        ),\n        is_updatable: Some(\n            \"YES\",\n        ),\n    },\n]\n"
  },
  {
    "path": "src/bgworker/snapshots/loco_rs__bgworker__sqlt__tests__can_complete_job_with_interval.snap",
    "content": "---\nsource: src/bgworker/sqlt.rs\nexpression: after_complete_job\nsnapshot_kind: text\n---\nJob {\n    id: \"<REDACTED>\",\n    name: \"PasswordChangeNotification\",\n    data: Object {\n        \"change_time\": String(\"<REDACTED>\"),\n        \"email\": String(\"user12@example.com\"),\n        \"user_id\": Number(134),\n    },\n    status: Queued,\n    run_at: <REDACTED>,\n    interval: None,\n    created_at: Some(\n        <REDACTED>,\n    ),\n    updated_at: Some(\n        <REDACTED>,\n    ),\n    tags: None,\n}\n"
  },
  {
    "path": "src/bgworker/snapshots/loco_rs__bgworker__sqlt__tests__can_dequeue.snap",
    "content": "---\nsource: src/bgworker/sqlt.rs\nexpression: job_after_dequeue\nsnapshot_kind: text\n---\nJob {\n    id: \"<REDACTED>\",\n    name: \"PasswordChangeNotification\",\n    data: Object {\n        \"user_id\": Number(1),\n    },\n    status: Processing,\n    run_at: <REDACTED>,\n    interval: None,\n    created_at: Some(\n        <REDACTED>,\n    ),\n    updated_at: Some(\n        <REDACTED>,\n    ),\n    tags: None,\n}\n"
  },
  {
    "path": "src/bgworker/snapshots/loco_rs__bgworker__sqlt__tests__can_enqueue.snap",
    "content": "---\nsource: src/bgworker/sqlt.rs\nexpression: jobs\nsnapshot_kind: text\n---\n[\n    Job {\n        id: \"<REDACTED>\",\n        name: \"PasswordChangeNotification\",\n        data: Object {\n            \"user_id\": Number(1),\n        },\n        status: Queued,\n        run_at: <REDACTED>,\n        interval: None,\n        created_at: Some(\n            <REDACTED>,\n        ),\n        updated_at: Some(\n            <REDACTED>,\n        ),\n        tags: Some(\n            [\n                \"email\",\n                \"notification\",\n            ],\n        ),\n    },\n]\n"
  },
  {
    "path": "src/bgworker/snapshots/loco_rs__bgworker__sqlt__tests__can_fail_job.snap",
    "content": "---\nsource: src/bgworker/sqlt.rs\nexpression: after_fail_job\nsnapshot_kind: text\n---\nJob {\n    id: \"<REDACTED>\",\n    name: \"SendInvoice\",\n    data: Object {\n        \"email\": String(\"user13@example.com\"),\n        \"error\": String(\"some error\"),\n        \"invoice_id\": String(\"INV-2024-01\"),\n        \"user_id\": Number(135),\n    },\n    status: Failed,\n    run_at: <REDACTED>,\n    interval: None,\n    created_at: Some(\n        <REDACTED>,\n    ),\n    updated_at: Some(\n        <REDACTED>,\n    ),\n    tags: None,\n}\n"
  },
  {
    "path": "src/bgworker/snapshots/loco_rs__bgworker__sqlt__tests__sqlt_loco_queue.snap",
    "content": "---\nsource: src/bgworker/sqlt.rs\nexpression: table_info\nsnapshot_kind: text\n---\n[\n    TableInfo {\n        cid: 0,\n        name: \"id\",\n        _type: \"TEXT\",\n        notnull: true,\n        dflt_value: None,\n        pk: false,\n    },\n    TableInfo {\n        cid: 1,\n        name: \"name\",\n        _type: \"TEXT\",\n        notnull: true,\n        dflt_value: None,\n        pk: false,\n    },\n    TableInfo {\n        cid: 2,\n        name: \"task_data\",\n        _type: \"JSON\",\n        notnull: true,\n        dflt_value: None,\n        pk: false,\n    },\n    TableInfo {\n        cid: 3,\n        name: \"status\",\n        _type: \"TEXT\",\n        notnull: true,\n        dflt_value: Some(\n            \"'queued'\",\n        ),\n        pk: false,\n    },\n    TableInfo {\n        cid: 4,\n        name: \"run_at\",\n        _type: \"TIMESTAMP\",\n        notnull: true,\n        dflt_value: None,\n        pk: false,\n    },\n    TableInfo {\n        cid: 5,\n        name: \"interval\",\n        _type: \"INTEGER\",\n        notnull: false,\n        dflt_value: None,\n        pk: false,\n    },\n    TableInfo {\n        cid: 6,\n        name: \"created_at\",\n        _type: \"TIMESTAMP\",\n        notnull: true,\n        dflt_value: Some(\n            \"CURRENT_TIMESTAMP\",\n        ),\n        pk: false,\n    },\n    TableInfo {\n        cid: 7,\n        name: \"updated_at\",\n        _type: \"TIMESTAMP\",\n        notnull: true,\n        dflt_value: Some(\n            \"CURRENT_TIMESTAMP\",\n        ),\n        pk: false,\n    },\n    TableInfo {\n        cid: 8,\n        name: \"tags\",\n        _type: \"JSON\",\n        notnull: false,\n        dflt_value: None,\n        pk: false,\n    },\n]\n"
  },
  {
    "path": "src/bgworker/snapshots/loco_rs__bgworker__sqlt__tests__sqlt_loco_queue_lock.snap",
    "content": "---\nsource: src/bgworker/sqlt.rs\nexpression: table_info\nsnapshot_kind: text\n---\n[\n    TableInfo {\n        cid: 0,\n        name: \"id\",\n        _type: \"INTEGER\",\n        notnull: false,\n        dflt_value: None,\n        pk: true,\n    },\n    TableInfo {\n        cid: 1,\n        name: \"is_locked\",\n        _type: \"BOOLEAN\",\n        notnull: true,\n        dflt_value: Some(\n            \"FALSE\",\n        ),\n        pk: false,\n    },\n    TableInfo {\n        cid: 2,\n        name: \"locked_at\",\n        _type: \"TIMESTAMP\",\n        notnull: false,\n        dflt_value: None,\n        pk: false,\n    },\n]\n"
  },
  {
    "path": "src/bgworker/snapshots/loco_rs__bgworker__tests__can_dump_jobs.snap",
    "content": "---\nsource: src/bgworker/mod.rs\nexpression: \"std::fs::read_to_string(dump_file).unwrap()\"\nsnapshot_kind: text\n---\n\"- created_at: 2024-11-28T08:03:25Z\\n  id: 01JDM0X8EVAM823JZBGKYNBA94\\n  interval: null\\n  name: DataBackup\\n  run_at: 2024-11-28T08:04:25Z\\n  status: cancelled\\n  tags: null\\n  task_data:\\n    backup_id: backup-12345\\n    email: user16@example.com\\n    user_id: 138\\n  updated_at: 2024-11-28T08:03:25Z\\n- created_at: 2024-11-28T08:03:25Z\\n  id: 01JDM0X8EVAM823JZBGKYNBA96\\n  interval: null\\n  name: UserDeactivation\\n  run_at: 2024-11-28T08:04:25Z\\n  status: failed\\n  tags: null\\n  task_data:\\n    deactivation_reason: user requested\\n    email: user14@example.com\\n    user_id: 136\\n  updated_at: 2024-11-28T08:03:25Z\\n- created_at: 2024-11-28T08:03:25Z\\n  id: 01JDM0X8EVAM823JZBGKYNBA87\\n  interval: null\\n  name: UserDeactivation\\n  run_at: 2024-11-28T08:04:25Z\\n  status: failed\\n  tags: null\\n  task_data:\\n    deactivation_reason: account inactive\\n    email: user24@example.com\\n    user_id: 146\\n  updated_at: 2024-11-28T08:03:25Z\\n\"\n"
  },
  {
    "path": "src/bgworker/sqlt.rs",
    "content": "/// `SQLite` based background job queue provider\nuse std::{\n    collections::HashMap, future::Future, panic::AssertUnwindSafe, pin::Pin, sync::Arc,\n    time::Duration,\n};\n\nuse super::{BackgroundWorker, JobStatus, Queue};\nuse crate::{config::SqliteQueueConfig, Error, Result};\nuse chrono::{DateTime, Utc};\nuse futures_util::FutureExt;\nuse serde::{Deserialize, Serialize};\nuse serde_json::Value as JsonValue;\npub use sqlx::SqlitePool;\nuse sqlx::{\n    sqlite::{SqliteConnectOptions, SqlitePoolOptions, SqliteRow},\n    ConnectOptions, QueryBuilder, Row,\n};\nuse std::fmt::Write;\nuse tokio::{task::JoinHandle, time::sleep};\nuse tokio_util::sync::CancellationToken;\nuse tracing::{debug, error, trace};\nuse ulid::Ulid;\ntype JobId = String;\ntype JobData = JsonValue;\n\ntype JobHandler = Box<\n    dyn Fn(\n            JobId,\n            JobData,\n        ) -> Pin<Box<dyn std::future::Future<Output = Result<(), crate::Error>> + Send>>\n        + Send\n        + Sync,\n>;\n\n#[derive(Clone, Debug, Deserialize, Serialize)]\npub struct Job {\n    pub id: JobId,\n    pub name: String,\n    #[serde(rename = \"task_data\")]\n    pub data: JobData,\n    pub status: JobStatus,\n    pub run_at: DateTime<Utc>,\n    pub interval: Option<i64>,\n    pub created_at: Option<DateTime<Utc>>,\n    pub updated_at: Option<DateTime<Utc>>,\n    pub tags: Option<Vec<String>>,\n}\n\npub struct JobRegistry {\n    handlers: Arc<HashMap<String, JobHandler>>,\n}\n\nimpl JobRegistry {\n    /// Creates a new `JobRegistry`.\n    #[must_use]\n    pub fn new() -> Self {\n        Self {\n            handlers: Arc::new(HashMap::new()),\n        }\n    }\n\n    /// Registers a job handler with the provided name.\n    /// # Errors\n    /// Fails if cannot register worker\n    pub fn register_worker<Args, W>(&mut self, name: String, worker: W) -> Result<()>\n    where\n        Args: Send + Serialize + Sync + 'static,\n        W: BackgroundWorker<Args> + 'static,\n        for<'de> Args: Deserialize<'de>,\n    {\n        let worker = Arc::new(worker);\n        let wrapped_handler = move |_job_id: String, job_data: JobData| {\n            let w = worker.clone();\n\n            Box::pin(async move {\n                let args = serde_json::from_value::<Args>(job_data);\n                match args {\n                    Ok(args) => {\n                        // Wrap the perform call in catch_unwind to handle panics\n                        match AssertUnwindSafe(w.perform(args)).catch_unwind().await {\n                            Ok(result) => result,\n                            Err(panic) => {\n                                let panic_msg = panic\n                                    .downcast_ref::<String>()\n                                    .map(String::as_str)\n                                    .or_else(|| panic.downcast_ref::<&str>().copied())\n                                    .unwrap_or(\"Unknown panic occurred\");\n                                error!(error = panic_msg, \"Worker panicked during execution\");\n                                Err(Error::string(panic_msg))\n                            }\n                        }\n                    }\n                    Err(err) => Err(err.into()),\n                }\n            }) as Pin<Box<dyn Future<Output = Result<(), crate::Error>> + Send>>\n        };\n\n        Arc::get_mut(&mut self.handlers)\n            .ok_or_else(|| Error::string(\"cannot register worker\"))?\n            .insert(name, Box::new(wrapped_handler));\n        Ok(())\n    }\n\n    /// Returns a reference to the job handlers.\n    #[must_use]\n    pub fn handlers(&self) -> &Arc<HashMap<String, JobHandler>> {\n        &self.handlers\n    }\n\n    /// Runs the job handlers with the provided number of workers.\n    #[must_use]\n    pub fn run(\n        &self,\n        pool: &SqlitePool,\n        opts: &RunOpts,\n        token: &CancellationToken,\n        tags: &[String],\n    ) -> Vec<JoinHandle<()>> {\n        let mut jobs = Vec::new();\n\n        let interval = opts.poll_interval_sec;\n        for idx in 0..opts.num_workers {\n            let handlers = self.handlers.clone();\n            let worker_token = token.clone();\n            let worker_tags = tags.to_vec();\n\n            let pool = pool.clone();\n            let job = tokio::spawn(async move {\n                loop {\n                    if worker_token.is_cancelled() {\n                        trace!(worker_id = idx, \"Cancellation received, stopping worker\");\n                        break;\n                    }\n                    trace!(\n                        pool_size = pool.num_idle(),\n                        worker_id = idx,\n                        \"Connection pool stats\"\n                    );\n                    let job_opt = match dequeue(&pool, &worker_tags).await {\n                        Ok(t) => t,\n                        Err(err) => {\n                            error!(error = %err, \"Failed to fetch job from queue\");\n                            None\n                        }\n                    };\n\n                    if let Some(job) = job_opt {\n                        debug!(job_id = %job.id, job_name = %job.name, \"Processing job\");\n                        if let Some(handler) = handlers.get(&job.name) {\n                            match handler(job.id.clone(), job.data.clone()).await {\n                                Ok(()) => {\n                                    if let Err(err) =\n                                        complete_job(&pool, &job.id, job.interval).await\n                                    {\n                                        error!(\n                                            error = %err,\n                                            job_id = %job.id,\n                                            job_name = %job.name,\n                                            \"Failed to mark job as completed\"\n                                        );\n                                    } else {\n                                        debug!(job_id = %job.id, \"Job completed successfully\");\n                                    }\n                                }\n                                Err(err) => {\n                                    if let Err(fail_err) = fail_job(&pool, &job.id, &err).await {\n                                        error!(\n                                            error = %fail_err,\n                                            job_id = %job.id,\n                                            job_name = %job.name,\n                                            \"Failed to mark job as failed\"\n                                        );\n                                    } else {\n                                        debug!(job_id = %job.id, error = %err, \"Job execution failed\");\n                                    }\n                                }\n                            }\n                        } else {\n                            error!(job_name = %job.name, \"No handler registered for job\");\n                        }\n                    } else {\n                        tokio::select! {\n                            biased;\n                            () = worker_token.cancelled() => {\n                                trace!(worker_id = idx, \"Cancellation received during sleep, stopping worker\");\n                                break;\n                            }\n                            () = sleep(Duration::from_secs(interval.into())) => {\n                                // Interval elapsed, continue loop\n                            }\n                        }\n                    }\n                }\n            });\n\n            jobs.push(job);\n        }\n\n        jobs\n    }\n}\n\nimpl Default for JobRegistry {\n    fn default() -> Self {\n        Self::new()\n    }\n}\n\nasync fn connect(cfg: &SqliteQueueConfig) -> Result<SqlitePool> {\n    let mut conn_opts: SqliteConnectOptions = cfg.uri.parse()?;\n    if !cfg.enable_logging {\n        conn_opts = conn_opts.disable_statement_logging();\n    }\n    let pool = SqlitePoolOptions::new()\n        .min_connections(cfg.min_connections)\n        .max_connections(cfg.max_connections)\n        .idle_timeout(Duration::from_millis(cfg.idle_timeout))\n        .acquire_timeout(Duration::from_millis(cfg.connect_timeout))\n        .connect_with(conn_opts)\n        .await?;\n    Ok(pool)\n}\n\n/// Initialize job tables\n///\n/// # Errors\n///\n/// This function will return an error if it fails\npub async fn initialize_database(pool: &SqlitePool) -> Result<()> {\n    debug!(\"Initializing job database tables\");\n    sqlx::query(\n        &format!(r\"\n            CREATE TABLE IF NOT EXISTS sqlt_loco_queue (\n                id TEXT NOT NULL,\n                name TEXT NOT NULL,\n                task_data JSON NOT NULL,\n                status TEXT NOT NULL DEFAULT '{}',\n                run_at TIMESTAMP NOT NULL,\n                interval INTEGER,\n                created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n                updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n                tags JSON\n            );\n\n            CREATE TABLE IF NOT EXISTS sqlt_loco_queue_lock (\n                id INTEGER PRIMARY KEY CHECK (id = 1),\n                is_locked BOOLEAN NOT NULL DEFAULT FALSE,\n                locked_at TIMESTAMP NULL\n            );\n\n            INSERT OR IGNORE INTO sqlt_loco_queue_lock (id, is_locked) VALUES (1, FALSE);\n\n            CREATE INDEX IF NOT EXISTS idx_sqlt_queue_status_run_at ON sqlt_loco_queue(status, run_at);\n            \", JobStatus::Queued),\n    )\n    .execute(pool)\n    .await?;\n    Ok(())\n}\n\n/// Add a job\n///\n/// # Errors\n///\n/// This function will return an error if it fails\npub async fn enqueue(\n    pool: &SqlitePool,\n    name: &str,\n    data: JobData,\n    run_at: DateTime<Utc>,\n    interval: Option<Duration>,\n    tags: Option<Vec<String>>,\n) -> Result<JobId> {\n    let data = serde_json::to_value(data)?;\n    let tags_json = match &tags {\n        Some(tags) => Some(serde_json::to_value(tags)?),\n        None => None,\n    };\n\n    #[allow(clippy::cast_possible_truncation)]\n    let interval_ms: Option<i64> = interval.map(|i| i.as_millis() as i64);\n\n    let id = Ulid::new().to_string();\n    debug!(job_id = %id, job_name = %name, run_at = %run_at, tags = ?tags, \"Enqueueing job\");\n    sqlx::query(\n        \"INSERT INTO sqlt_loco_queue (id, task_data, name, run_at, interval, tags) VALUES ($1, $2, $3, \\\n         DATETIME($4), $5, $6)\",\n    )\n    .bind(id.clone())\n    .bind(data)\n    .bind(name)\n    .bind(run_at)\n    .bind(interval_ms)\n    .bind(tags_json)\n    .execute(pool)\n    .await?;\n    Ok(id)\n}\n\nasync fn dequeue(client: &SqlitePool, worker_tags: &[String]) -> Result<Option<Job>> {\n    let mut tx = client.begin().await?;\n\n    let acquired_write_lock = sqlx::query(\n        \"UPDATE sqlt_loco_queue_lock SET\n            is_locked = TRUE,\n            locked_at = CURRENT_TIMESTAMP\n        WHERE id = 1 AND is_locked = FALSE\",\n    )\n    .execute(&mut *tx)\n    .await?;\n\n    // Couldn't aquire the write lock\n    if acquired_write_lock.rows_affected() == 0 {\n        trace!(\"Unable to acquire queue lock, skipping job fetch\");\n        tx.rollback().await?;\n        return Ok(None);\n    }\n\n    // Build the query with tag filtering\n    let mut query = String::from(\n        \"SELECT id, name, task_data, status, run_at, interval, tags\n        FROM sqlt_loco_queue\n        WHERE\n            status = ? AND\n            run_at <= CURRENT_TIMESTAMP\",\n    );\n\n    // Apply tag filtering logic:\n    // 1. If worker has no tags, only process jobs with no tags\n    // 2. If worker has tags, only process jobs with at least one matching tag\n    if worker_tags.is_empty() {\n        query.push_str(\" AND (tags IS NULL)\");\n    } else {\n        query.push_str(\" AND (tags IS NOT NULL)\");\n\n        // Add placeholders for the LIKE conditions\n        let mut conditions = Vec::new();\n        for _ in worker_tags {\n            conditions.push(\"json_extract(tags, '$') LIKE ?\".to_string());\n        }\n\n        if !conditions.is_empty() {\n            query.push_str(\" AND (\");\n            query.push_str(&conditions.join(\" OR \"));\n            query.push(')');\n        }\n    }\n\n    query.push_str(\" ORDER BY run_at LIMIT 1\");\n\n    let mut db_query = sqlx::query(&query).bind(JobStatus::Queued.to_string());\n\n    // Add tag parameters to the query with proper JSON wildcard format\n    for tag in worker_tags {\n        // Format tag for JSON string search: each tag needs to be in format \"%\\\"tagname\\\"%\"\n        db_query = db_query.bind(format!(\"%\\\"{tag}\\\"%\"));\n    }\n\n    let row = db_query\n        .map(|row: SqliteRow| to_job(&row).ok())\n        .fetch_optional(&mut *tx)\n        .await?\n        .flatten();\n\n    if let Some(job) = row {\n        trace!(job_id = %job.id, job_name = %job.name, job_tags = ?job.tags, \"Dequeueing job for processing\");\n        sqlx::query(\n            \"UPDATE sqlt_loco_queue SET status = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2\",\n        )\n        .bind(JobStatus::Processing.to_string())\n        .bind(&job.id)\n        .execute(&mut *tx)\n        .await?;\n\n        // Release the write lock\n        sqlx::query(\n            \"UPDATE sqlt_loco_queue_lock \n              SET is_locked = FALSE,\n                  locked_at = NULL\n              WHERE id = 1\",\n        )\n        .execute(&mut *tx)\n        .await?;\n\n        tx.commit().await?;\n\n        Ok(Some(job))\n    } else {\n        trace!(\"No jobs available for processing\");\n        // Release the write lock, no job found\n        sqlx::query(\n            \"UPDATE sqlt_loco_queue_lock \n              SET is_locked = FALSE,\n                  locked_at = NULL\n              WHERE id = 1\",\n        )\n        .execute(&mut *tx)\n        .await?;\n\n        tx.commit().await?;\n        Ok(None)\n    }\n}\n\nasync fn complete_job(pool: &SqlitePool, id: &JobId, interval_ms: Option<i64>) -> Result<()> {\n    if let Some(interval_ms) = interval_ms {\n        let next_run_at = Utc::now() + chrono::Duration::milliseconds(interval_ms);\n        trace!(\n            job_id = %id,\n            status = \"queued\",\n            next_run_at = %next_run_at,\n            \"Rescheduling recurring job\"\n        );\n        sqlx::query(\n            \"UPDATE sqlt_loco_queue SET status = $1, updated_at = CURRENT_TIMESTAMP, run_at = \\\n             DATETIME($2) WHERE id = $3\",\n        )\n        .bind(JobStatus::Queued.to_string())\n        .bind(next_run_at)\n        .bind(id)\n        .execute(pool)\n        .await?;\n    } else {\n        trace!(job_id = %id, status = \"completed\", \"Marking job as completed\");\n        sqlx::query(\n            \"UPDATE sqlt_loco_queue SET status = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2\",\n        )\n        .bind(JobStatus::Completed.to_string())\n        .bind(id)\n        .execute(pool)\n        .await?;\n    }\n    Ok(())\n}\n\nasync fn fail_job(pool: &SqlitePool, id: &JobId, error: &crate::Error) -> Result<()> {\n    let msg = error.to_string();\n    debug!(job_id = %id, error = %msg, \"Marking job as failed\");\n    let error_json = serde_json::json!({ \"error\": msg });\n    sqlx::query(\n        \"UPDATE sqlt_loco_queue SET status = $1, updated_at = CURRENT_TIMESTAMP, task_data = \\\n         json_patch(task_data, $2) WHERE id = $3\",\n    )\n    .bind(JobStatus::Failed.to_string())\n    .bind(error_json)\n    .bind(id)\n    .execute(pool)\n    .await?;\n    Ok(())\n}\n\n/// Cancels jobs in the `sqlt_loco_queue` table by their name.\n///\n/// This function updates the status of all jobs with the given `name` and a status of\n/// [`JobStatus::Queued`] to [`JobStatus::Cancelled`]. The update also sets the `updated_at` timestamp to the\n/// current time.\n///\n/// # Errors\n///\n/// This function will return an error if it fails\npub async fn cancel_jobs_by_name(pool: &SqlitePool, name: &str) -> Result<()> {\n    debug!(job_name = %name, \"Cancelling queued jobs by name\");\n    sqlx::query(\n        \"UPDATE sqlt_loco_queue SET status = $1, updated_at = CURRENT_TIMESTAMP WHERE name = $2 \\\n         AND status = $3\",\n    )\n    .bind(JobStatus::Cancelled.to_string())\n    .bind(name)\n    .bind(JobStatus::Queued.to_string())\n    .execute(pool)\n    .await?;\n    Ok(())\n}\n\n/// Clear all jobs\n///\n/// # Errors\n///\n/// This function will return an error if it fails\npub async fn clear(pool: &SqlitePool) -> Result<()> {\n    // Clear all rows in the relevant tables\n    sqlx::query(\n        \"\n        DELETE FROM sqlt_loco_queue;\n        DELETE FROM sqlt_loco_queue_lock;\n        \",\n    )\n    .execute(pool)\n    .await?;\n\n    Ok(())\n}\n\n/// Deletes jobs from the `sqlt_loco_queue` table based on their status.\n///\n/// This function removes all jobs with a status that matches any of the statuses provided\n/// in the `status` argument. The statuses are checked against the `status` column in the\n/// database, and any matching rows are deleted.\n///\n/// # Errors\n///\n/// This function will return an error if it fails\npub async fn clear_by_status(pool: &SqlitePool, status: Vec<JobStatus>) -> Result<()> {\n    let status_in = status\n        .iter()\n        .map(|s| format!(\"'{s}'\"))\n        .collect::<Vec<String>>()\n        .join(\",\");\n\n    debug!(status = ?status, \"Clearing jobs by status\");\n    sqlx::query(&format!(\n        \"DELETE FROM sqlt_loco_queue WHERE status IN ({status_in})\"\n    ))\n    .execute(pool)\n    .await?;\n\n    Ok(())\n}\n\n/// Requeues jobs from [`JobStatus::Processing`] to [`JobStatus::Queued`].\n///\n/// This function updates the status of all jobs that are currently in the [`JobStatus::Processing`] state\n/// to the [`JobStatus::Queued`] state, provided they have been updated more than the specified age (`age_minutes`).\n/// The jobs that meet the criteria will have their `updated_at` timestamp set to the current time.\n///\n/// # Errors\n///\n/// This function will return an error if it fails\npub async fn requeue(pool: &SqlitePool, age_minutes: &i64) -> Result<()> {\n    let query = format!(\n        \"UPDATE sqlt_loco_queue SET status = $1, updated_at = CURRENT_TIMESTAMP WHERE status = $2 AND updated_at <= DATETIME('now', '-{age_minutes} minute')\"\n    );\n\n    debug!(age_minutes = age_minutes, \"Requeueing stalled jobs\");\n    sqlx::query(&query)\n        .bind(JobStatus::Queued.to_string())\n        .bind(JobStatus::Processing.to_string())\n        .execute(pool)\n        .await?;\n\n    Ok(())\n}\n\n/// Deletes jobs from the `sqlt_loco_queue` table that are older than a specified number of days.\n///\n/// This function removes jobs that have a `created_at` timestamp older than the provided\n/// number of days. Additionally, if a `status` is provided, only jobs with a status matching\n/// one of the provided values will be deleted.\n///\n/// # Errors\n///\n/// This function will return an error if it fails\npub async fn clear_jobs_older_than(\n    pool: &SqlitePool,\n    age_days: i64,\n    status: Option<&Vec<JobStatus>>,\n) -> Result<()> {\n    let cutoff_date = Utc::now() - chrono::Duration::days(age_days);\n    let threshold_date = cutoff_date.format(\"%+\").to_string();\n\n    let mut query_builder =\n        QueryBuilder::<sqlx::Sqlite>::new(\"DELETE FROM sqlt_loco_queue WHERE created_at <= \");\n    query_builder.push_bind(threshold_date);\n\n    if let Some(status_list) = status {\n        if !status_list.is_empty() {\n            let status_in = status_list\n                .iter()\n                .map(|s| format!(\"'{s}'\"))\n                .collect::<Vec<String>>()\n                .join(\",\");\n\n            query_builder.push(format!(\" AND status IN ({status_in})\"));\n        }\n    }\n\n    debug!(age_days = age_days, status = ?status, \"Clearing older jobs\");\n    query_builder.build().execute(pool).await?;\n    Ok(())\n}\n\n/// Ping system\n///\n/// # Errors\n///\n/// This function will return an error if it fails\npub async fn ping(pool: &SqlitePool) -> Result<()> {\n    trace!(\"Pinging job queue database\");\n    sqlx::query(\"SELECT id from sqlt_loco_queue LIMIT 1\")\n        .execute(pool)\n        .await?;\n    Ok(())\n}\n\n#[derive(Debug)]\npub struct RunOpts {\n    pub num_workers: u32,\n    pub poll_interval_sec: u32,\n}\n\n/// Create this provider\n///\n/// # Errors\n///\n/// This function will return an error if it fails\npub async fn create_provider(qcfg: &SqliteQueueConfig) -> Result<Queue> {\n    debug!(\n        num_workers = qcfg.num_workers,\n        poll_interval = qcfg.poll_interval_sec,\n        \"Creating job queue provider\"\n    );\n    let pool = connect(qcfg).await.map_err(Box::from)?;\n    let registry = JobRegistry::new();\n    let token = CancellationToken::new();\n    Ok(Queue::Sqlite(\n        pool,\n        Arc::new(tokio::sync::Mutex::new(registry)),\n        RunOpts {\n            num_workers: qcfg.num_workers,\n            poll_interval_sec: qcfg.poll_interval_sec,\n        },\n        token,\n    ))\n}\n\n/// Retrieves a list of jobs from the `sqlt_loco_queue` table in the database.\n///\n/// This function queries the database for jobs, optionally filtering by their\n/// `status`. If a status is provided, only jobs with statuses included in the\n/// provided list will be fetched. If no status is provided, all jobs will be\n/// returned.\n///\n/// # Errors\n///\n/// This function will return an error if it fails\npub async fn get_jobs(\n    pool: &SqlitePool,\n    status: Option<&Vec<JobStatus>>,\n    age_days: Option<i64>,\n) -> Result<Vec<Job>> {\n    let mut query = String::from(\"SELECT * FROM sqlt_loco_queue WHERE 1 = 1 \");\n\n    if let Some(status) = status {\n        let status_in = status\n            .iter()\n            .map(|s| format!(\"'{s}'\"))\n            .collect::<Vec<String>>()\n            .join(\",\");\n        let _ = write!(query, \" AND status IN ({status_in})\");\n    }\n\n    if let Some(age_days) = age_days {\n        let cutoff_date = Utc::now() - chrono::Duration::days(age_days);\n        let threshold_date = cutoff_date.format(\"%+\").to_string();\n        let _ = write!(query, \" AND created_at <= '{threshold_date}' \");\n    }\n\n    debug!(status = ?status, age_days = ?age_days, \"Retrieving jobs\");\n    let rows = sqlx::query(&query).fetch_all(pool).await?;\n    let jobs = rows.iter().filter_map(|row| to_job(row).ok()).collect();\n    debug!(job_count = rows.len(), \"Retrieved jobs from database\");\n    Ok(jobs)\n}\n\n/// Converts a row from the database into a [`Job`] object.\n///\n/// This function takes a row from the `SQLite` database and manually extracts the necessary\n/// fields to populate a [`Job`] object.\n///\n/// **Note:** This function manually extracts values from the database row instead of using\n/// the `FromRow` trait, which would require enabling the 'macros' feature in the dependencies.\n/// The decision to avoid `FromRow` is made to keep the build smaller and faster, as the 'macros'\n/// feature is unnecessary in the current dependency tree.\nfn to_job(row: &SqliteRow) -> Result<Job> {\n    let tags_json: Option<serde_json::Value> = row.try_get(\"tags\").unwrap_or_default();\n    let tags = tags_json.and_then(|json_val| {\n        if json_val.is_array() {\n            let tags_vec: Vec<String> =\n                serde_json::from_value(json_val).unwrap_or_else(|_| Vec::new());\n            if tags_vec.is_empty() {\n                None\n            } else {\n                Some(tags_vec)\n            }\n        } else {\n            None\n        }\n    });\n\n    Ok(Job {\n        id: row.get(\"id\"),\n        name: row.get(\"name\"),\n        data: row.get(\"task_data\"),\n        status: row.get::<String, _>(\"status\").parse().map_err(|err| {\n            let status: String = row.get(\"status\");\n            tracing::error!(status, err = %err, \"Unsupported job status in database\");\n            Error::string(\"invalid job status\")\n        })?,\n        run_at: row.get(\"run_at\"),\n        interval: row.get(\"interval\"),\n        created_at: row.try_get(\"created_at\").unwrap_or_default(),\n        updated_at: row.try_get(\"updated_at\").unwrap_or_default(),\n        tags,\n    })\n}\n\n#[cfg(test)]\nmod tests {\n\n    use std::path::Path;\n\n    use chrono::{NaiveDate, NaiveTime, TimeZone};\n    use insta::{assert_debug_snapshot, with_settings};\n    use sqlx::{query_as, FromRow, Pool, Sqlite};\n\n    use super::*;\n    use crate::tests_cfg;\n\n    #[derive(Debug, Serialize, FromRow)]\n    pub struct TableInfo {\n        cid: i32,\n        name: String,\n        #[sqlx(rename = \"type\")]\n        _type: String,\n        notnull: bool,\n        dflt_value: Option<String>,\n        pk: bool,\n    }\n\n    #[derive(Debug, Serialize, FromRow)]\n    struct JobQueueLock {\n        id: i32,\n        is_locked: bool,\n        locked_at: Option<DateTime<Utc>>,\n    }\n\n    fn reduction() -> &'static [(&'static str, &'static str)] {\n        &[\n            (\"[A-Z0-9]{26}\", \"<REDACTED>\"),\n            (r\"\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z\", \"<REDACTED>\"),\n        ]\n    }\n\n    async fn init(db_path: &Path) -> Pool<Sqlite> {\n        let qcfg = SqliteQueueConfig {\n            uri: format!(\n                \"sqlite://{}?mode=rwc\",\n                db_path.join(\"sample.sqlite\").display()\n            ),\n            dangerously_flush: false,\n            enable_logging: false,\n            max_connections: 1,\n            min_connections: 1,\n            connect_timeout: 500,\n            idle_timeout: 500,\n            poll_interval_sec: 1,\n            num_workers: 1,\n        };\n\n        let pool = connect(&qcfg).await.unwrap();\n        sqlx::raw_sql(\n            r\"\n        DROP TABLE IF EXISTS sqlt_loco_queue;\n        DROP TABLE IF EXISTS sqlt_loco_queue_lock;\n        \",\n        )\n        .execute(&pool)\n        .await\n        .expect(\"drop table if exists\");\n\n        pool\n    }\n\n    async fn get_all_jobs(pool: &SqlitePool) -> Vec<Job> {\n        sqlx::query(\"select * from sqlt_loco_queue\")\n            .fetch_all(pool)\n            .await\n            .expect(\"get jobs\")\n            .iter()\n            .filter_map(|row| to_job(row).ok())\n            .collect()\n    }\n\n    async fn get_job(pool: &SqlitePool, id: &str) -> Job {\n        sqlx::query(&format!(\"select * from sqlt_loco_queue where id = '{id}'\"))\n            .fetch_all(pool)\n            .await\n            .expect(\"get jobs\")\n            .first()\n            .and_then(|row| to_job(row).ok())\n            .expect(\"job not found\")\n    }\n\n    #[tokio::test]\n    async fn can_initialize_database() {\n        let tree_fs = tree_fs::TreeBuilder::default()\n            .drop(true)\n            .create()\n            .expect(\"create temp folder\");\n        let pool = init(&tree_fs.root).await;\n\n        assert!(initialize_database(&pool).await.is_ok());\n\n        for table in [\"sqlt_loco_queue\", \"sqlt_loco_queue_lock\"] {\n            let table_info: Vec<TableInfo> =\n                query_as::<_, TableInfo>(&format!(\"PRAGMA table_info({table})\"))\n                    .fetch_all(&pool)\n                    .await\n                    .unwrap();\n\n            assert_debug_snapshot!(table, table_info);\n        }\n    }\n\n    #[tokio::test]\n    async fn can_enqueue() {\n        let tree_fs = tree_fs::TreeBuilder::default()\n            .drop(true)\n            .create()\n            .expect(\"create temp folder\");\n        let pool = init(&tree_fs.root).await;\n\n        assert!(initialize_database(&pool).await.is_ok());\n\n        let jobs = get_all_jobs(&pool).await;\n\n        assert_eq!(jobs.len(), 0);\n\n        let run_at = Utc.from_utc_datetime(\n            &NaiveDate::from_ymd_opt(2023, 1, 15)\n                .unwrap()\n                .and_time(NaiveTime::from_hms_opt(12, 30, 0).unwrap()),\n        );\n\n        let job_data = serde_json::json!({\"user_id\": 1});\n        let tags = Some(vec![\"email\".to_string(), \"notification\".to_string()]);\n        assert!(enqueue(\n            &pool,\n            \"PasswordChangeNotification\",\n            job_data,\n            run_at,\n            None,\n            tags\n        )\n        .await\n        .is_ok());\n\n        let jobs = get_all_jobs(&pool).await;\n\n        assert_eq!(jobs.len(), 1);\n        with_settings!({\n            filters => reduction().iter().map(|&(pattern, replacement)| (pattern, replacement)),\n        }, {\n            assert_debug_snapshot!(jobs);\n        });\n\n        // validate lock status\n        let job_lock: JobQueueLock =\n            query_as::<_, JobQueueLock>(\"select * from sqlt_loco_queue_lock\")\n                .fetch_one(&pool)\n                .await\n                .unwrap();\n\n        assert!(!job_lock.is_locked);\n    }\n\n    #[tokio::test]\n    async fn can_dequeue() {\n        let tree_fs = tree_fs::TreeBuilder::default()\n            .drop(true)\n            .create()\n            .expect(\"create temp folder\");\n        let pool = init(&tree_fs.root).await;\n\n        assert!(initialize_database(&pool).await.is_ok());\n\n        let jobs = get_all_jobs(&pool).await;\n\n        assert_eq!(jobs.len(), 0);\n\n        let run_at = Utc.from_utc_datetime(\n            &NaiveDate::from_ymd_opt(2023, 1, 15)\n                .unwrap()\n                .and_time(NaiveTime::from_hms_opt(12, 30, 0).unwrap()),\n        );\n\n        let job_data = serde_json::json!({\"user_id\": 1});\n        assert!(enqueue(\n            &pool,\n            \"PasswordChangeNotification\",\n            job_data,\n            run_at,\n            None,\n            None\n        )\n        .await\n        .is_ok());\n\n        let job_before_dequeue = get_all_jobs(&pool)\n            .await\n            .first()\n            .cloned()\n            .expect(\"gets first job\");\n        assert_eq!(job_before_dequeue.status, JobStatus::Queued);\n\n        std::thread::sleep(std::time::Duration::from_secs(1));\n\n        assert!(dequeue(&pool, &[]).await.is_ok());\n\n        let job_after_dequeue = get_all_jobs(&pool)\n            .await\n            .first()\n            .cloned()\n            .expect(\"gets first job\");\n\n        assert_ne!(job_after_dequeue.updated_at, job_before_dequeue.updated_at);\n        with_settings!({\n            filters => reduction().iter().map(|&(pattern, replacement)| (pattern, replacement)),\n        }, {\n            assert_debug_snapshot!(job_after_dequeue);\n        });\n    }\n\n    #[tokio::test]\n    async fn can_complete_job_without_interval() {\n        let tree_fs = tree_fs::TreeBuilder::default()\n            .drop(true)\n            .create()\n            .expect(\"create temp folder\");\n        let pool = init(&tree_fs.root).await;\n\n        assert!(initialize_database(&pool).await.is_ok());\n        tests_cfg::queue::sqlite_seed_data(&pool).await;\n\n        let job = get_job(&pool, \"01JDM0X8EVAM823JZBGKYNBA99\").await;\n\n        assert_eq!(job.status, JobStatus::Queued);\n        assert!(complete_job(&pool, &job.id, None).await.is_ok());\n\n        let job = get_job(&pool, \"01JDM0X8EVAM823JZBGKYNBA99\").await;\n\n        assert_eq!(job.status, JobStatus::Completed);\n    }\n\n    #[tokio::test]\n    async fn can_complete_job_with_interval() {\n        let tree_fs = tree_fs::TreeBuilder::default()\n            .drop(true)\n            .create()\n            .expect(\"create temp folder\");\n        let pool = init(&tree_fs.root).await;\n\n        assert!(initialize_database(&pool).await.is_ok());\n        tests_cfg::queue::sqlite_seed_data(&pool).await;\n\n        let before_complete_job = get_job(&pool, \"01JDM0X8EVAM823JZBGKYNBA98\").await;\n        assert_eq!(before_complete_job.status, JobStatus::Completed);\n\n        std::thread::sleep(std::time::Duration::from_secs(1));\n\n        assert!(complete_job(&pool, &before_complete_job.id, Some(10))\n            .await\n            .is_ok());\n\n        let after_complete_job = get_job(&pool, \"01JDM0X8EVAM823JZBGKYNBA98\").await;\n\n        assert_ne!(\n            after_complete_job.updated_at,\n            before_complete_job.updated_at\n        );\n        with_settings!({\n            filters => reduction().iter().map(|&(pattern, replacement)| (pattern, replacement)),\n        }, {\n            assert_debug_snapshot!(after_complete_job);\n        });\n    }\n\n    #[tokio::test]\n    async fn can_fail_job() {\n        let tree_fs = tree_fs::TreeBuilder::default()\n            .drop(true)\n            .create()\n            .expect(\"create temp folder\");\n        let pool = init(&tree_fs.root).await;\n\n        assert!(initialize_database(&pool).await.is_ok());\n        tests_cfg::queue::sqlite_seed_data(&pool).await;\n\n        let before_fail_job = get_job(&pool, \"01JDM0X8EVAM823JZBGKYNBA97\").await;\n\n        std::thread::sleep(std::time::Duration::from_secs(1));\n\n        assert!(fail_job(\n            &pool,\n            &before_fail_job.id,\n            &crate::Error::string(\"some error\")\n        )\n        .await\n        .is_ok());\n\n        let after_fail_job = get_job(&pool, \"01JDM0X8EVAM823JZBGKYNBA97\").await;\n\n        assert_ne!(after_fail_job.updated_at, before_fail_job.updated_at);\n        with_settings!({\n            filters => reduction().iter().map(|&(pattern, replacement)| (pattern, replacement)),\n        }, {\n            assert_debug_snapshot!(after_fail_job);\n        });\n    }\n\n    #[tokio::test]\n    async fn can_cancel_job_by_name() {\n        let tree_fs = tree_fs::TreeBuilder::default()\n            .drop(true)\n            .create()\n            .expect(\"create temp folder\");\n        let pool = init(&tree_fs.root).await;\n\n        assert!(initialize_database(&pool).await.is_ok());\n        tests_cfg::queue::sqlite_seed_data(&pool).await;\n\n        let count_cancelled_jobs = get_all_jobs(&pool)\n            .await\n            .iter()\n            .filter(|j| j.status == JobStatus::Cancelled)\n            .count();\n\n        assert_eq!(count_cancelled_jobs, 1);\n\n        assert!(cancel_jobs_by_name(&pool, \"UserAccountActivation\")\n            .await\n            .is_ok());\n\n        let count_cancelled_jobs = get_all_jobs(&pool)\n            .await\n            .iter()\n            .filter(|j| j.status == JobStatus::Cancelled)\n            .count();\n\n        assert_eq!(count_cancelled_jobs, 2);\n    }\n\n    #[tokio::test]\n    async fn can_clear() {\n        let tree_fs = tree_fs::TreeBuilder::default()\n            .drop(true)\n            .create()\n            .expect(\"create temp folder\");\n        let pool = init(&tree_fs.root).await;\n\n        assert!(initialize_database(&pool).await.is_ok());\n        tests_cfg::queue::sqlite_seed_data(&pool).await;\n\n        let job_count: i64 = sqlx::query_scalar(\"SELECT COUNT(*) FROM sqlt_loco_queue\")\n            .fetch_one(&pool)\n            .await\n            .unwrap();\n        let lock_count: i64 = sqlx::query_scalar(\"SELECT COUNT(*) FROM sqlt_loco_queue_lock\")\n            .fetch_one(&pool)\n            .await\n            .unwrap();\n        assert_ne!(job_count, 0);\n        assert_ne!(lock_count, 0);\n\n        assert!(clear(&pool).await.is_ok());\n        let job_count: i64 = sqlx::query_scalar(\"SELECT COUNT(*) FROM sqlt_loco_queue\")\n            .fetch_one(&pool)\n            .await\n            .unwrap();\n        let lock_count: i64 = sqlx::query_scalar(\"SELECT COUNT(*) FROM sqlt_loco_queue_lock\")\n            .fetch_one(&pool)\n            .await\n            .unwrap();\n        assert_eq!(job_count, 0);\n        assert_eq!(lock_count, 0);\n    }\n\n    #[tokio::test]\n    async fn can_clear_by_status() {\n        let tree_fs = tree_fs::TreeBuilder::default()\n            .drop(true)\n            .create()\n            .expect(\"create temp folder\");\n        let pool = init(&tree_fs.root).await;\n\n        assert!(initialize_database(&pool).await.is_ok());\n        tests_cfg::queue::sqlite_seed_data(&pool).await;\n\n        let jobs = get_all_jobs(&pool).await;\n\n        assert_eq!(jobs.len(), 14);\n        assert_eq!(\n            jobs.iter()\n                .filter(|j| j.status == JobStatus::Completed)\n                .count(),\n            3\n        );\n        assert_eq!(\n            jobs.iter()\n                .filter(|j| j.status == JobStatus::Failed)\n                .count(),\n            2\n        );\n\n        assert!(\n            clear_by_status(&pool, vec![JobStatus::Completed, JobStatus::Failed])\n                .await\n                .is_ok()\n        );\n        let jobs = get_all_jobs(&pool).await;\n\n        assert_eq!(jobs.len(), 9);\n        assert_eq!(\n            jobs.iter()\n                .filter(|j| j.status == JobStatus::Completed)\n                .count(),\n            0\n        );\n        assert_eq!(\n            jobs.iter()\n                .filter(|j| j.status == JobStatus::Failed)\n                .count(),\n            0\n        );\n    }\n\n    #[tokio::test]\n    async fn can_clear_jobs_older_than() {\n        let tree_fs = tree_fs::TreeBuilder::default()\n            .drop(true)\n            .create()\n            .expect(\"create temp folder\");\n        let pool = init(&tree_fs.root).await;\n\n        assert!(initialize_database(&pool).await.is_ok());\n\n        sqlx::query(\n            r\"INSERT INTO sqlt_loco_queue (id, name, task_data, status,run_at, created_at, updated_at) VALUES\n            ('job1', 'Test Job 1', '{}', 'queued', CURRENT_TIMESTAMP,DATETIME('now', '-15 days'), CURRENT_TIMESTAMP),\n            ('job2', 'Test Job 2', '{}', 'queued', CURRENT_TIMESTAMP, DATETIME('now', '-5 days'), CURRENT_TIMESTAMP),\n            ('job3', 'Test Job 3', '{}', 'queued', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)\",\n        )\n        .execute(&pool)\n        .await\n        .unwrap();\n\n        assert_eq!(get_all_jobs(&pool).await.len(), 3);\n        assert!(clear_jobs_older_than(&pool, 10, None).await.is_ok());\n        assert_eq!(get_all_jobs(&pool).await.len(), 2);\n    }\n\n    #[tokio::test]\n    async fn can_clear_jobs_older_than_with_status() {\n        let tree_fs = tree_fs::TreeBuilder::default()\n            .drop(true)\n            .create()\n            .expect(\"create temp folder\");\n        let pool = init(&tree_fs.root).await;\n\n        assert!(initialize_database(&pool).await.is_ok());\n\n        sqlx::query(\n            r\"INSERT INTO sqlt_loco_queue (id, name, task_data, status,run_at, created_at, updated_at) VALUES\n            ('job1', 'Test Job 1', '{}', 'completed', CURRENT_TIMESTAMP,DATETIME('now', '-20 days'), CURRENT_TIMESTAMP),\n            ('job2', 'Test Job 2', '{}', 'failed', CURRENT_TIMESTAMP,DATETIME('now', '-15 days'), CURRENT_TIMESTAMP),\n            ('job3', 'Test Job 3', '{}', 'completed', CURRENT_TIMESTAMP, DATETIME('now', '-5 days'), CURRENT_TIMESTAMP),\n            ('job4', 'Test Job 4', '{}', 'cancelled', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)\",\n        )\n        .execute(&pool)\n        .await\n        .unwrap();\n\n        assert_eq!(get_all_jobs(&pool).await.len(), 4);\n        assert!(clear_jobs_older_than(\n            &pool,\n            10,\n            Some(&vec![JobStatus::Cancelled, JobStatus::Completed])\n        )\n        .await\n        .is_ok());\n\n        assert_eq!(get_all_jobs(&pool).await.len(), 3);\n    }\n\n    #[tokio::test]\n    async fn can_get_jobs() {\n        let tree_fs = tree_fs::TreeBuilder::default()\n            .drop(true)\n            .create()\n            .expect(\"create temp folder\");\n        let pool = init(&tree_fs.root).await;\n        assert!(initialize_database(&pool).await.is_ok());\n        tests_cfg::queue::sqlite_seed_data(&pool).await;\n\n        assert_eq!(\n            get_jobs(&pool, Some(&vec![JobStatus::Failed]), None)\n                .await\n                .expect(\"get jobs\")\n                .len(),\n            2\n        );\n        assert_eq!(\n            get_jobs(\n                &pool,\n                Some(&vec![JobStatus::Failed, JobStatus::Completed]),\n                None\n            )\n            .await\n            .expect(\"get jobs\")\n            .len(),\n            5\n        );\n        assert_eq!(\n            get_jobs(&pool, None, None).await.expect(\"get jobs\").len(),\n            14\n        );\n    }\n\n    #[tokio::test]\n    async fn can_get_jobs_with_age() {\n        let tree_fs = tree_fs::TreeBuilder::default()\n            .drop(true)\n            .create()\n            .expect(\"create temp folder\");\n        let pool = init(&tree_fs.root).await;\n        assert!(initialize_database(&pool).await.is_ok());\n\n        sqlx::query(\n            r\"INSERT INTO sqlt_loco_queue (id, name, task_data, status,run_at, created_at, updated_at) VALUES\n            ('job1', 'Test Job 1', '{}', 'completed', CURRENT_TIMESTAMP,DATETIME('now', '-20 days'), CURRENT_TIMESTAMP),\n            ('job2', 'Test Job 2', '{}', 'failed', CURRENT_TIMESTAMP,DATETIME('now', '-15 days'), CURRENT_TIMESTAMP),\n            ('job3', 'Test Job 3', '{}', 'completed', CURRENT_TIMESTAMP, DATETIME('now', '-5 days'), CURRENT_TIMESTAMP),\n            ('job4', 'Test Job 4', '{}', 'cancelled', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)\",\n        )\n        .execute(&pool)\n        .await\n        .unwrap();\n        assert_eq!(\n            get_jobs(\n                &pool,\n                Some(&vec![JobStatus::Failed, JobStatus::Completed]),\n                Some(10)\n            )\n            .await\n            .expect(\"get jobs\")\n            .len(),\n            2\n        );\n    }\n\n    #[tokio::test]\n    async fn can_requeue() {\n        let tree_fs = tree_fs::TreeBuilder::default()\n            .drop(true)\n            .create()\n            .expect(\"create temp folder\");\n        let pool = init(&tree_fs.root).await;\n\n        assert!(initialize_database(&pool).await.is_ok());\n        sqlx::query(\n            r\"INSERT INTO sqlt_loco_queue (id, name, task_data, status,run_at, created_at, updated_at) VALUES\n            ('job1', 'Test Job 1', '{}', 'processing', CURRENT_TIMESTAMP,CURRENT_TIMESTAMP, DATETIME('now', '-20 minute')),\n            ('job2', 'Test Job 2', '{}', 'processing', CURRENT_TIMESTAMP,CURRENT_TIMESTAMP, DATETIME('now', '-5 minute')),\n            ('job3', 'Test Job 3', '{}', 'completed', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, DATETIME('now', '-5 minute')),\n            ('job4', 'Test Job 4', '{}', 'queued', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),\n            ('job5', 'Test Job 5', '{}', 'processing', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)\",\n        )\n        .execute(&pool)\n        .await\n        .unwrap();\n        let jobs = get_all_jobs(&pool).await;\n\n        let processing_job_count = jobs\n            .iter()\n            .filter(|job| job.status == JobStatus::Processing)\n            .count();\n        let queued_job_count = jobs\n            .iter()\n            .filter(|job| job.status == JobStatus::Queued)\n            .count();\n\n        assert_eq!(processing_job_count, 3);\n        assert_eq!(queued_job_count, 1);\n        assert!(requeue(&pool, &10).await.is_ok());\n        let jobs = get_all_jobs(&pool).await;\n        let processing_job_count = jobs\n            .iter()\n            .filter(|job| job.status == JobStatus::Processing)\n            .count();\n        let queued_job_count = jobs\n            .iter()\n            .filter(|job| job.status == JobStatus::Queued)\n            .count();\n\n        assert_eq!(processing_job_count, 2);\n        assert_eq!(queued_job_count, 2);\n    }\n\n    #[tokio::test]\n    async fn can_handle_worker_panic() {\n        let tree_fs = tree_fs::TreeBuilder::default()\n            .drop(true)\n            .create()\n            .expect(\"create temp folder\");\n        let pool = init(&tree_fs.root).await;\n\n        assert!(initialize_database(&pool).await.is_ok());\n\n        let job_data = serde_json::json!(null);\n        let job_id = enqueue(&pool, \"PanicJob\", job_data, Utc::now(), None, None)\n            .await\n            .expect(\"Failed to enqueue job\");\n\n        struct PanicWorker;\n        #[async_trait::async_trait]\n        impl BackgroundWorker<()> for PanicWorker {\n            fn build(_ctx: &crate::app::AppContext) -> Self {\n                Self\n            }\n            async fn perform(&self, _args: ()) -> crate::Result<()> {\n                panic!(\"intentional panic for testing\");\n            }\n        }\n\n        let mut registry = JobRegistry::new();\n        assert!(registry\n            .register_worker(\"PanicJob\".to_string(), PanicWorker)\n            .is_ok());\n\n        // Get the initial job state\n        let job = get_job(&pool, &job_id).await;\n        assert_eq!(job.status, JobStatus::Queued);\n\n        // Start the worker\n        let opts = RunOpts {\n            num_workers: 1,\n            poll_interval_sec: 1,\n        };\n        let token = CancellationToken::new();\n        let handles = registry.run(&pool, &opts, &token, &[]);\n\n        // Wait a bit for the worker to process the job\n        sleep(Duration::from_secs(1)).await;\n\n        // Stop the worker\n        for handle in handles {\n            handle.abort();\n        }\n\n        // Verify the job is marked as failed\n        let failed_job = get_job(&pool, &job_id).await;\n        assert_eq!(failed_job.status, JobStatus::Failed);\n\n        // Print and verify the error message stored in job data\n        println!(\"Job data: {:?}\", failed_job.data);\n        let error_msg = failed_job\n            .data\n            .as_object()\n            .and_then(|obj| obj.get(\"error\"))\n            .and_then(|v| v.as_str())\n            .expect(\"Expected error message in job data\");\n        assert!(\n            error_msg.contains(\"intentional panic for testing\"),\n            \"Error message '{error_msg}' did not contain expected text\"\n        );\n    }\n\n    #[tokio::test]\n    async fn can_dequeue_with_tags() {\n        let tree_fs = tree_fs::TreeBuilder::default()\n            .drop(true)\n            .create()\n            .expect(\"create temp folder\");\n        let pool = init(&tree_fs.root).await;\n\n        assert!(initialize_database(&pool).await.is_ok());\n\n        // Add a job with email tag\n        let run_at = Utc::now() - chrono::Duration::minutes(5); // In the past so it's ready to process\n        let job_data = serde_json::json!({\"user_id\": 1});\n        let email_tags = Some(vec![\"email\".to_string()]);\n\n        // Insert email job\n        let email_id = enqueue(\n            &pool,\n            \"EmailNotification\",\n            job_data.clone(),\n            run_at,\n            None,\n            email_tags,\n        )\n        .await\n        .expect(\"Failed to enqueue email job\");\n\n        // Insert job with \"sms\" tag\n        let sms_tags = Some(vec![\"sms\".to_string()]);\n        let sms_id = enqueue(\n            &pool,\n            \"SmsNotification\",\n            job_data.clone(),\n            run_at,\n            None,\n            sms_tags,\n        )\n        .await\n        .expect(\"Failed to enqueue sms job\");\n\n        // Insert job with multiple tags\n        let multi_tags = Some(vec![\"email\".to_string(), \"priority\".to_string()]);\n        let multi_id = enqueue(\n            &pool,\n            \"PriorityEmail\",\n            job_data.clone(),\n            run_at,\n            None,\n            multi_tags,\n        )\n        .await\n        .expect(\"Failed to enqueue multi-tag job\");\n\n        // Insert job with no tags\n        let no_tag_id = enqueue(\n            &pool,\n            \"GenericNotification\",\n            job_data.clone(),\n            run_at,\n            None,\n            None,\n        )\n        .await\n        .expect(\"Failed to enqueue untagged job\");\n\n        // Verify all jobs are in the database\n        let all_jobs = get_all_jobs(&pool).await;\n        assert_eq!(all_jobs.len(), 4);\n\n        // 1. Worker with no tags should only get untagged jobs\n        let job = dequeue(&pool, &[]).await.expect(\"dequeue failed\");\n        assert!(job.is_some());\n        let job = job.unwrap();\n        assert_eq!(job.id, no_tag_id);\n        assert!(job.tags.is_none());\n\n        // Mark the job as completed to remove it from the queued items\n        complete_job(&pool, &job.id, None)\n            .await\n            .expect(\"Failed to complete job\");\n\n        // 2. Worker with \"email\" tag should get one of the email-tagged jobs\n        let job = dequeue(&pool, &[\"email\".to_string()])\n            .await\n            .expect(\"dequeue failed\");\n        assert!(job.is_some());\n        let job = job.unwrap();\n        assert!(\n            job.id == email_id || job.id == multi_id,\n            \"Expected either email job or multi-tag job\"\n        );\n        assert!(job.tags.is_some());\n        assert!(job.tags.as_ref().unwrap().contains(&\"email\".to_string()));\n\n        // Mark the job as completed\n        complete_job(&pool, &job.id, None)\n            .await\n            .expect(\"Failed to complete job\");\n\n        // 3. Worker with \"email\" tag should get the remaining email job\n        let job = dequeue(&pool, &[\"email\".to_string()])\n            .await\n            .expect(\"dequeue failed\");\n        assert!(job.is_some());\n        let job = job.unwrap();\n        assert!(\n            job.id == email_id || job.id == multi_id,\n            \"Expected either email job or multi-tag job\"\n        );\n        assert!(job.tags.is_some());\n        assert!(job.tags.as_ref().unwrap().contains(&\"email\".to_string()));\n\n        // Mark the job as completed\n        complete_job(&pool, &job.id, None)\n            .await\n            .expect(\"Failed to complete job\");\n\n        // 4. Worker with \"sms\" tag should get the sms job\n        let job = dequeue(&pool, &[\"sms\".to_string()])\n            .await\n            .expect(\"dequeue failed\");\n        assert!(job.is_some());\n        let job = job.unwrap();\n        assert_eq!(job.id, sms_id);\n        assert!(job.tags.is_some());\n        assert_eq!(job.tags.as_ref().unwrap(), &vec![\"sms\".to_string()]);\n\n        // Mark the job as completed\n        complete_job(&pool, &job.id, None)\n            .await\n            .expect(\"Failed to complete job\");\n\n        // 5. No more jobs should be available\n        let job = dequeue(&pool, &[\"email\".to_string()])\n            .await\n            .expect(\"dequeue failed\");\n        assert!(job.is_none());\n\n        // 6. No more jobs should be available for untagged worker\n        let job = dequeue(&pool, &[]).await.expect(\"dequeue failed\");\n        assert!(job.is_none());\n    }\n}\n"
  },
  {
    "path": "src/boot.rs",
    "content": "//! # Application Bootstrapping and Logic\n//! This module contains functions and structures for bootstrapping and running\n//! your application.\nuse std::{\n    env,\n    path::{Path, PathBuf},\n    sync::Arc,\n};\n\nuse axum::Router;\n#[cfg(feature = \"with-db\")]\nuse sea_orm_migration::MigratorTrait;\nuse tokio::{select, signal, task::JoinHandle};\nuse tracing::{debug, error, info, warn};\n\n#[cfg(feature = \"with-db\")]\nuse crate::db;\nuse crate::{\n    app::{AppContext, Hooks, Initializer},\n    banner::print_banner,\n    bgworker, cache,\n    config::{self, Config, WorkerMode},\n    controller::ListRoutes,\n    env_vars,\n    environment::Environment,\n    errors::Error,\n    mailer::{EmailSender, MailerWorker},\n    prelude::BackgroundWorker,\n    scheduler::{self, Scheduler},\n    storage::{self, Storage},\n    task::{self, Tasks},\n    Result,\n};\n\n/// Represents the application startup mode.\n#[derive(Debug)]\npub enum StartMode {\n    /// Run the application as a server only. when running web server only,\n    /// workers job will not handle.\n    ServerOnly,\n    /// Run the application web server and the worker in the same process.\n    ServerAndWorker,\n    /// Pulling job worker and execute them\n    WorkerOnly {\n        /// Specifies that the worker should only handle jobs associated with one of these tags.\n        /// If empty, the worker handles all jobs.\n        tags: Vec<String>,\n    },\n    /// Run the app with all available components in the same process.\n    All,\n}\n\npub struct BootResult {\n    /// Application Context\n    pub app_context: AppContext,\n    /// Web server routes\n    pub router: Option<Router>,\n    /// worker processor\n    pub worker: Option<Vec<String>>,\n    /// scheduler processor\n    pub run_scheduler: bool,\n}\n\n/// Configuration structure for serving an application.\n#[derive(Debug)]\npub struct ServeParams {\n    /// The port number on which the server will listen for incoming\n    /// connections.\n    pub port: i32,\n    /// The network address to which the server will bind. It specifies the\n    /// interface to listen on.\n    pub binding: String,\n}\n\n/// Runs the application based on the provided `BootResult`.\n///\n/// This function is responsible for starting the application, including the\n/// server and/or workers.\n///\n/// # Errors\n///\n/// When could not initialize the application.\npub async fn start<H: Hooks>(\n    boot: BootResult,\n    server_config: ServeParams,\n    no_banner: bool,\n) -> Result<()> {\n    if boot.run_scheduler {\n        let scheduler = scheduler::<H>(&boot.app_context, None, None, None)?;\n        tokio::spawn(async move {\n            if let Err(err) = scheduler.run().await {\n                error!(err = err.to_string(), \"error while running scheduler\");\n            }\n        });\n    }\n\n    if !no_banner {\n        print_banner(&boot, &server_config);\n    }\n\n    let BootResult {\n        router,\n        worker,\n        run_scheduler: _,\n        app_context,\n    } = boot;\n\n    match (router, worker) {\n        (Some(router), None) => {\n            H::serve(router, &app_context, &server_config).await?;\n        }\n        (Some(router), Some(tags)) => {\n            let handle = if app_context.config.workers.mode == WorkerMode::BackgroundQueue {\n                Some(start_queue_worker(&app_context, tags)?)\n            } else {\n                None\n            };\n\n            H::serve(router, &app_context, &server_config).await?;\n\n            if let Some(handle) = handle {\n                shutdown_and_await_queue_worker(&app_context, handle).await?;\n            }\n        }\n        (None, Some(tags)) => {\n            let handle = if app_context.config.workers.mode == WorkerMode::BackgroundQueue {\n                Some(start_queue_worker(&app_context, tags)?)\n            } else {\n                None\n            };\n\n            shutdown_signal().await;\n\n            if let Some(handle) = handle {\n                shutdown_and_await_queue_worker(&app_context, handle).await?;\n            }\n        }\n        _ => {}\n    }\n    Ok(())\n}\n\nfn start_queue_worker(app_context: &AppContext, tags: Vec<String>) -> Result<JoinHandle<()>> {\n    debug!(\"note: worker is run in-process (tokio spawn)\");\n\n    if let Some(queue) = &app_context.queue_provider {\n        let cloned_queue = queue.clone();\n        let handle = tokio::spawn(async move {\n            if let Err(err) = cloned_queue.run(tags).await {\n                error!(err = err.to_string(), \"error while running worker\");\n            }\n        });\n        return Ok(handle);\n    }\n\n    Err(Error::QueueProviderMissing)\n}\n\nasync fn shutdown_and_await_queue_worker(\n    app_context: &AppContext,\n    handle: JoinHandle<()>,\n) -> Result<(), Error> {\n    if let Some(queue) = &app_context.queue_provider {\n        queue.shutdown()?;\n    }\n\n    println!(\"press ctrl-c again to force quit\");\n    select! {\n        _ = handle => {}\n        () = shutdown_signal() => {}\n    }\n    Ok(())\n}\n\n/// Run task\n///\n/// # Errors\n///\n/// When running could not run the task.\npub async fn run_task<H: Hooks>(\n    app_context: &AppContext,\n    task: Option<&String>,\n    vars: &task::Vars,\n) -> Result<()> {\n    let mut tasks = Tasks::default();\n    H::register_tasks(&mut tasks);\n\n    if let Some(task) = task {\n        let task_span = tracing::span!(tracing::Level::DEBUG, \"task\", task,);\n        let _guard = task_span.enter();\n        tasks.run(app_context, task, vars).await?;\n    } else {\n        let list = tasks.list();\n        for item in &list {\n            println!(\"{:<30}[{}]\", item.name, item.detail);\n        }\n    }\n    Ok(())\n}\n\n/// Initializes a new scheduler instance based on the provided configuration and context.\nfn scheduler<H: Hooks>(\n    app_context: &AppContext,\n    config: Option<&PathBuf>,\n    name: Option<String>,\n    tag: Option<String>,\n) -> Result<Scheduler> {\n    let env_config_path = env::var(env_vars::SCHEDULER_CONFIG).ok();\n\n    let config_path: Option<&Path> = config.map_or_else(\n        || env_config_path.as_deref().map(Path::new),\n        |path| Some(path.as_path()),\n    );\n\n    let scheduler = match config_path {\n        Some(path) => Scheduler::from_config::<H>(path, &app_context.environment)?,\n        None => {\n            if let Some(config) = &app_context.config.scheduler {\n                Scheduler::new::<H>(config, &app_context.environment)?\n            } else {\n                return Err(Error::Scheduler(scheduler::Error::Empty));\n            }\n        }\n    };\n\n    Ok(scheduler.by_spec(&scheduler::Spec { name, tag }))\n}\n\n/// Runs the scheduler with the given configuration and context. in case if list\n/// args is true prints scheduler job configuration\n///\n/// This function initializes the scheduler, registers tasks through the\n/// provided [`Hooks`], and executes the scheduler based on the specified\n/// configuration or context. The scheduler continuously runs, managing and\n/// executing scheduled tasks until a signal is received to shut down.\n/// Upon receiving this signal, the function gracefully shuts down all running\n/// tasks and exits safely.\n///\n/// # Errors\n///\n/// When running could not run the scheduler.\npub async fn run_scheduler<H: Hooks>(\n    app_context: &AppContext,\n    config: Option<&PathBuf>,\n    name: Option<String>,\n    tag: Option<String>,\n    list: bool,\n) -> Result<()> {\n    let task_span = tracing::span!(tracing::Level::DEBUG, \"scheduler_jobs\");\n    let _guard = task_span.enter();\n\n    let scheduler = scheduler::<H>(app_context, config, name, tag)?;\n    if list {\n        println!(\"{scheduler}\");\n        Ok(())\n    } else {\n        Ok(scheduler.run().await?)\n    }\n}\n\n/// Represents commands for handling database-related operations.\n#[derive(Debug)]\npub enum RunDbCommand {\n    /// Apply pending migrations.\n    Migrate,\n    /// Run one or more down migrations.\n    Down(u32),\n    /// Drop all tables, then reapply all migrations.\n    Reset,\n    /// Check the status of all migrations.\n    Status,\n    /// Generate entity.\n    Entities,\n    /// Truncate tables, by executing the implementation in [`Hooks::seed`]\n    /// (without dropping).\n    Truncate,\n    /// Seed database.\n    Seed {\n        reset: bool,\n        from: PathBuf,\n        dump: bool,\n        dump_tables: Option<Vec<String>>,\n    },\n    /// Dump database schema\n    Schema,\n}\n\n#[cfg(feature = \"with-db\")]\n/// Handles database commands.\n///\n/// # Errors\n///\n/// Return an error when the given command fails. mostly return\n/// [`sea_orm::DbErr`]\n#[allow(clippy::cognitive_complexity)]\npub async fn run_db<H: Hooks, M: MigratorTrait>(\n    app_context: &AppContext,\n    cmd: RunDbCommand,\n) -> Result<()> {\n    match cmd {\n        RunDbCommand::Migrate => {\n            tracing::warn!(\"migrate:\");\n            db::migrate::<M>(&app_context.db).await?;\n        }\n        RunDbCommand::Down(steps) => {\n            tracing::warn!(\"down:\");\n            db::down::<M>(&app_context.db, steps).await?;\n        }\n        RunDbCommand::Reset => {\n            tracing::warn!(\"reset:\");\n            db::reset::<M>(&app_context.db).await?;\n        }\n        RunDbCommand::Status => {\n            tracing::warn!(\"status:\");\n            db::status::<M>(&app_context.db).await?;\n        }\n        RunDbCommand::Entities => {\n            tracing::warn!(\"entities:\");\n\n            tracing::warn!(\"{}\", db::entities::<M>(app_context).await?);\n        }\n        RunDbCommand::Truncate => {\n            tracing::warn!(\"truncate:\");\n            H::truncate(app_context).await?;\n        }\n        RunDbCommand::Seed {\n            reset,\n            from,\n            dump,\n            dump_tables,\n        } => {\n            tracing::warn!(reset = reset, from = %from.display(), \"seed:\");\n\n            if dump || dump_tables.is_some() {\n                db::dump_tables(&app_context.db, from.as_path(), dump_tables).await?;\n            } else {\n                if reset {\n                    db::reset::<M>(&app_context.db).await?;\n                }\n                db::run_app_seed::<H>(app_context, &from).await?;\n            }\n        }\n        RunDbCommand::Schema => {\n            db::dump_schema(app_context, \"schema_dump.json\").await?;\n            println!(\"Database schema dumped to 'schema_dump.json'\");\n        }\n    }\n    Ok(())\n}\n\n/// Initializes the application context by loading configuration and\n/// establishing connections.\n///\n/// # Errors\n/// When has an error to create DB connection.\npub async fn create_context<H: Hooks>(\n    environment: &Environment,\n    config: Config,\n) -> Result<AppContext> {\n    if config.logger.pretty_backtrace {\n        std::env::set_var(\"RUST_BACKTRACE\", \"1\");\n        warn!(\n            \"pretty backtraces are enabled (this is great for development but has a runtime cost \\\n             for production. disable with `logger.pretty_backtrace` in your config yaml)\"\n        );\n    }\n    #[cfg(feature = \"with-db\")]\n    let db = db::connect(&config.database).await?;\n\n    let mailer = if let Some(cfg) = config.mailer.as_ref() {\n        create_mailer(cfg)?\n    } else {\n        None\n    };\n\n    let queue_provider = bgworker::create_queue_provider(&config).await?;\n    let ctx = AppContext {\n        environment: environment.clone(),\n        #[cfg(feature = \"with-db\")]\n        db,\n        queue_provider,\n        storage: Storage::single(storage::drivers::null::new()).into(),\n        cache: cache::create_cache_provider(&config).await?,\n        config,\n        mailer,\n        shared_store: Arc::new(crate::app::SharedStore::default()),\n    };\n\n    H::after_context(ctx).await\n}\n\n#[cfg(feature = \"with-db\")]\n/// Creates an application based on the specified mode and environment.\n///\n/// # Errors\n///\n/// When could not create the application\npub async fn create_app<H: Hooks, M: MigratorTrait>(\n    mode: StartMode,\n    environment: &Environment,\n    config: Config,\n) -> Result<BootResult> {\n    let app_context = create_context::<H>(environment, config).await?;\n    db::converge::<H, M>(&app_context, &app_context.config.database).await?;\n\n    if let (Some(queue), Some(config)) = (&app_context.queue_provider, &app_context.config.queue) {\n        bgworker::converge(queue, config).await?;\n    }\n\n    run_app::<H>(&mode, app_context).await\n}\n\n#[cfg(not(feature = \"with-db\"))]\npub async fn create_app<H: Hooks>(\n    mode: StartMode,\n    environment: &Environment,\n    config: Config,\n) -> Result<BootResult> {\n    let app_context = create_context::<H>(environment, config).await?;\n\n    if let (Some(queue), Some(config)) = (&app_context.queue_provider, &app_context.config.queue) {\n        bgworker::converge(queue, config).await?;\n    }\n\n    run_app::<H>(&mode, app_context).await\n}\n\n/// Run the application with the  given mode\n/// # Errors\n///\n/// When could not create the application\npub async fn run_app<H: Hooks>(mode: &StartMode, app_context: AppContext) -> Result<BootResult> {\n    H::before_run(&app_context).await?;\n    let initializers = H::initializers(&app_context).await?;\n\n    info!(\n        initializers = ?initializers.iter().map(|init| init.name()).collect::<Vec<_>>().join(\",\"),\n        \"initializers loaded\"\n    );\n\n    for initializer in &initializers {\n        initializer.before_run(&app_context).await?;\n    }\n\n    match mode {\n        StartMode::ServerOnly => {\n            let router = setup_routes::<H>(&app_context, &initializers).await?;\n            Ok(BootResult {\n                app_context,\n                router: Some(router),\n                worker: None,\n                run_scheduler: false,\n            })\n        }\n        StartMode::ServerAndWorker => {\n            register_workers::<H>(&app_context).await?;\n            let router = setup_routes::<H>(&app_context, &initializers).await?;\n            Ok(BootResult {\n                app_context,\n                router: Some(router),\n                worker: Some(vec![]),\n                run_scheduler: false,\n            })\n        }\n        StartMode::All => {\n            register_workers::<H>(&app_context).await?;\n            let router = setup_routes::<H>(&app_context, &initializers).await?;\n            Ok(BootResult {\n                app_context,\n                router: Some(router),\n                worker: Some(vec![]),\n                run_scheduler: true,\n            })\n        }\n        StartMode::WorkerOnly { tags } => {\n            register_workers::<H>(&app_context).await?;\n            Ok(BootResult {\n                app_context,\n                router: None,\n                worker: Some(tags.clone()),\n                run_scheduler: false,\n            })\n        }\n    }\n}\n\n/// Sets up the application's routes based on the provided initializers and hooks.\nasync fn setup_routes<H: Hooks>(\n    app_context: &AppContext,\n    initializers: &[Box<dyn Initializer>],\n) -> Result<Router> {\n    let app = H::before_routes(app_context).await?;\n    let app = H::routes(app_context).to_router::<H>(app_context.clone(), app)?;\n    let mut router = H::after_routes(app, app_context).await?;\n\n    for initializer in initializers {\n        router = initializer.after_routes(router, app_context).await?;\n    }\n\n    Ok(router)\n}\n\nasync fn register_workers<H: Hooks>(app_context: &AppContext) -> Result<()> {\n    if app_context.config.workers.mode == WorkerMode::BackgroundQueue {\n        if let Some(queue) = &app_context.queue_provider {\n            queue.register(MailerWorker::build(app_context)).await?;\n            H::connect_workers(app_context, queue).await?;\n        } else {\n            return Err(Error::QueueProviderMissing);\n        }\n\n        debug!(\"done registering workers and queues\");\n    }\n    Ok(())\n}\n\n#[must_use]\npub fn list_endpoints<H: Hooks>(ctx: &AppContext) -> Vec<ListRoutes> {\n    H::routes(ctx).collect()\n}\n\n/// Waits for a shutdown signal, either via Ctrl+C or termination signal.\n///\n/// # Panics\n///\n/// This function will panic if it fails to install the signal handlers for\n/// Ctrl+C or the terminate signal on Unix-based systems.\npub async fn shutdown_signal() {\n    let ctrl_c = async {\n        signal::ctrl_c()\n            .await\n            .expect(\"failed to install Ctrl+C handler\");\n    };\n\n    #[cfg(unix)]\n    let terminate = async {\n        signal::unix::signal(signal::unix::SignalKind::terminate())\n            .expect(\"failed to install signal handler\")\n            .recv()\n            .await;\n    };\n\n    #[cfg(not(unix))]\n    let terminate = std::future::pending::<()>();\n\n    tokio::select! {\n        () = ctrl_c => {},\n        () = terminate => {},\n    }\n}\n\npub struct MiddlewareInfo {\n    pub id: String,\n    pub enabled: bool,\n    pub detail: String,\n}\n\n#[must_use]\npub fn list_middlewares<H: Hooks>(ctx: &AppContext) -> Vec<MiddlewareInfo> {\n    H::middlewares(ctx)\n        .iter()\n        .map(|m| MiddlewareInfo {\n            id: m.name().to_string(),\n            enabled: m.is_enabled(),\n            detail: m.config().unwrap_or_default().to_string(),\n        })\n        .collect::<Vec<_>>()\n}\n\n/// Initializes an [`EmailSender`] based on the mailer configuration settings\n/// ([`config::Mailer`]).\nfn create_mailer(config: &config::Mailer) -> Result<Option<EmailSender>> {\n    if config.stub {\n        return Ok(Some(EmailSender::stub()));\n    }\n    if let Some(smtp) = config.smtp.as_ref() {\n        if smtp.enable {\n            return Ok(Some(EmailSender::smtp(smtp)?));\n        }\n    }\n    Ok(None)\n}\n"
  },
  {
    "path": "src/cache/drivers/inmem.rs",
    "content": "//! # In-Memory Cache Driver\n//!\n//! This module implements a cache driver using an in-memory cache.\nuse std::{\n    sync::Arc,\n    time::{Duration, Instant},\n};\n\nuse async_trait::async_trait;\nuse moka::{sync::Cache, Expiry};\n\nuse super::CacheDriver;\nuse crate::cache::CacheResult;\nuse crate::config::InMemCacheConfig;\n\n/// Creates a new instance of the in-memory cache driver, with a default Loco\n/// configuration.\n///\n/// # Returns\n///\n/// A [`Cache`] instance.\n#[must_use]\npub fn new(config: &InMemCacheConfig) -> crate::cache::Cache {\n    let cache: Cache<String, (Expiration, String)> = Cache::builder()\n        .max_capacity(config.max_capacity)\n        .expire_after(InMemExpiry)\n        .build();\n    crate::cache::Cache::new(Inmem::from(cache))\n}\n\n/// Represents the in-memory cache driver.\n#[derive(Debug)]\npub struct Inmem {\n    cache: Cache<String, (Expiration, String)>,\n}\n\nimpl Inmem {\n    /// Constructs a new [`Inmem`] instance from a given cache.\n    ///\n    /// # Returns\n    ///\n    /// A boxed [`CacheDriver`] instance.\n    #[must_use]\n    pub fn from(cache: Cache<String, (Expiration, String)>) -> Box<dyn CacheDriver> {\n        Box::new(Self { cache })\n    }\n}\n\n#[async_trait]\nimpl CacheDriver for Inmem {\n    /// Pings the cache to check if it is reachable.\n    ///\n    /// # Errors\n    ///\n    /// Returns always error\n    async fn ping(&self) -> CacheResult<()> {\n        Ok(())\n    }\n\n    /// Checks if a key exists in the cache.\n    ///\n    /// # Errors\n    ///\n    /// Returns a `CacheError` if there is an error during the operation.\n    async fn contains_key(&self, key: &str) -> CacheResult<bool> {\n        Ok(self.cache.contains_key(key))\n    }\n\n    /// Retrieves a value from the cache based on the provided key.\n    ///\n    /// # Errors\n    ///\n    /// Returns a `CacheError` if there is an error during the operation.\n    async fn get(&self, key: &str) -> CacheResult<Option<String>> {\n        let result = self.cache.get(key);\n        match result {\n            None => Ok(None),\n            Some(v) => Ok(Some(v.1)),\n        }\n    }\n\n    /// Inserts a key-value pair into the cache.\n    ///\n    /// # Errors\n    ///\n    /// Returns a `CacheError` if there is an error during the operation.\n    async fn insert(&self, key: &str, value: &str) -> CacheResult<()> {\n        self.cache.insert(\n            key.to_string(),\n            (Expiration::Never, Arc::new(value).to_string()),\n        );\n        Ok(())\n    }\n\n    /// Inserts a key-value pair into the cache that expires after the specified\n    /// number of seconds.\n    ///\n    /// # Errors\n    ///\n    /// Returns a [`super::CacheError`] if there is an error during the\n    /// operation.\n    async fn insert_with_expiry(\n        &self,\n        key: &str,\n        value: &str,\n        duration: Duration,\n    ) -> CacheResult<()> {\n        self.cache.insert(\n            key.to_string(),\n            (\n                Expiration::AfterDuration(duration),\n                Arc::new(value).to_string(),\n            ),\n        );\n        Ok(())\n    }\n\n    /// Removes a key-value pair from the cache.\n    ///\n    /// # Errors\n    ///\n    /// Returns a `CacheError` if there is an error during the operation.\n    async fn remove(&self, key: &str) -> CacheResult<()> {\n        self.cache.remove(key);\n        Ok(())\n    }\n\n    /// Clears all key-value pairs from the cache.\n    ///\n    /// # Errors\n    ///\n    /// Returns a `CacheError` if there is an error during the operation.\n    async fn clear(&self) -> CacheResult<()> {\n        self.cache.invalidate_all();\n        Ok(())\n    }\n}\n\n#[derive(Clone, Copy, Debug, Eq, PartialEq)]\npub enum Expiration {\n    Never,\n    AfterDuration(Duration),\n}\n\nimpl Expiration {\n    #[must_use]\n    pub fn as_duration(&self) -> Option<Duration> {\n        match self {\n            Self::Never => None,\n            Self::AfterDuration(d) => Some(*d),\n        }\n    }\n}\n\npub struct InMemExpiry;\n\nimpl Expiry<String, (Expiration, String)> for InMemExpiry {\n    fn expire_after_create(\n        &self,\n        _key: &String,\n        value: &(Expiration, String),\n        _current_time: Instant,\n    ) -> Option<Duration> {\n        value.0.as_duration()\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use crate::config::InMemCacheConfig;\n\n    fn create_test_config() -> InMemCacheConfig {\n        InMemCacheConfig { max_capacity: 100 }\n    }\n\n    #[tokio::test]\n    async fn ping_returns_pong_when_cache_is_accessible() {\n        let config = create_test_config();\n        let mem = new(&config);\n        assert!(mem.ping().await.is_ok());\n    }\n\n    #[tokio::test]\n    async fn is_contains_key() {\n        let config = create_test_config();\n        let mem = new(&config);\n        assert!(!mem.contains_key(\"key\").await.unwrap());\n        assert!(mem.insert(\"key\", \"loco\").await.is_ok());\n        assert!(mem.contains_key(\"key\").await.unwrap());\n    }\n\n    #[tokio::test]\n    async fn can_get_key_value() {\n        let config = create_test_config();\n        let mem = new(&config);\n        assert!(mem.insert(\"key\", \"loco\").await.is_ok());\n        assert_eq!(\n            mem.get::<String>(\"key\").await.unwrap(),\n            Some(\"loco\".to_string())\n        );\n\n        //try getting key that not exists\n        assert_eq!(mem.get::<String>(\"not-found\").await.unwrap(), None);\n    }\n\n    #[tokio::test]\n    async fn can_remove_key() {\n        let config = create_test_config();\n        let mem = new(&config);\n        assert!(mem.insert(\"key\", \"loco\").await.is_ok());\n        assert!(mem.contains_key(\"key\").await.unwrap());\n        mem.remove(\"key\").await.unwrap();\n        assert!(!mem.contains_key(\"key\").await.unwrap());\n    }\n\n    #[tokio::test]\n    async fn can_clear() {\n        let config = create_test_config();\n        let mem = new(&config);\n\n        let keys = vec![\"key\", \"key2\", \"key3\"];\n        for key in &keys {\n            assert!(mem.insert(key, \"loco\").await.is_ok());\n        }\n        for key in &keys {\n            assert!(mem.contains_key(key).await.is_ok());\n        }\n        assert!(mem.clear().await.is_ok());\n        for key in &keys {\n            assert!(!mem.contains_key(key).await.unwrap());\n        }\n    }\n}\n"
  },
  {
    "path": "src/cache/drivers/mod.rs",
    "content": "//! # Cache Drivers Module\n//!\n//! This module defines traits and implementations for cache drivers.\nuse std::time::Duration;\n\nuse async_trait::async_trait;\n\nuse super::CacheResult;\n\n#[cfg(feature = \"cache_inmem\")]\npub mod inmem;\npub mod null;\n#[cfg(feature = \"cache_redis\")]\npub mod redis;\n\n/// Trait representing a cache driver.\n#[async_trait]\npub trait CacheDriver: Sync + Send {\n    /// Pings the cache to check if it is reachable.\n    ///\n    /// # Errors\n    ///\n    /// Returns a [`super::CacheError`] if there is an error during the\n    /// operation.\n    async fn ping(&self) -> CacheResult<()>;\n\n    /// Checks if a key exists in the cache.\n    ///\n    /// # Errors\n    ///\n    /// Returns a [`super::CacheError`] if there is an error during the\n    /// operation.\n    async fn contains_key(&self, key: &str) -> CacheResult<bool>;\n\n    /// Retrieves a value from the cache based on the provided key.\n    ///\n    /// # Errors\n    ///\n    /// Returns a [`super::CacheError`] if there is an error during the\n    /// operation.\n    async fn get(&self, key: &str) -> CacheResult<Option<String>>;\n\n    /// Inserts a key-value pair into the cache.\n    ///\n    /// # Errors\n    ///\n    /// Returns a [`super::CacheError`] if there is an error during the\n    /// operation.\n    async fn insert(&self, key: &str, value: &str) -> CacheResult<()>;\n\n    /// Inserts a key-value pair into the cache that expires after the\n    /// specified duration.\n    ///\n    /// # Errors\n    ///\n    /// Returns a [`super::CacheError`] if there is an error during the\n    /// operation.\n    async fn insert_with_expiry(\n        &self,\n        key: &str,\n        value: &str,\n        duration: Duration,\n    ) -> CacheResult<()>;\n\n    /// Removes a key-value pair from the cache.\n    ///\n    /// # Errors\n    ///\n    /// Returns a [`super::CacheError`] if there is an error during the\n    /// operation.\n    async fn remove(&self, key: &str) -> CacheResult<()>;\n\n    /// Clears all key-value pairs from the cache.\n    ///\n    /// # Errors\n    ///\n    /// Returns a [`super::CacheError`] if there is an error during the\n    /// operation.\n    async fn clear(&self) -> CacheResult<()>;\n}\n"
  },
  {
    "path": "src/cache/drivers/null.rs",
    "content": "//! # Null Cache Driver\n//!\n//! The Null Cache Driver is the default cache driver implemented when the Loco\n//! framework is initialized. The primary purpose of this driver is to simplify\n//! the user workflow by avoiding the need for feature flags or optional cache\n//! driver configurations.\nuse std::time::Duration;\n\nuse async_trait::async_trait;\n\nuse super::CacheDriver;\nuse crate::cache::{CacheError, CacheResult};\n\n/// Represents the in-memory cache driver.\n#[derive(Debug)]\npub struct Null {}\n\n/// Creates a new null cache instance\n///\n/// # Returns\n///\n/// A boxed [`CacheDriver`] instance.\n#[must_use]\npub fn new() -> Box<dyn CacheDriver> {\n    Box::new(Null {})\n}\n\n#[async_trait]\nimpl CacheDriver for Null {\n    /// Pings the cache to check if it is reachable.\n    ///\n    /// # Errors\n    ///\n    /// Returns always error\n    async fn ping(&self) -> CacheResult<()> {\n        Err(CacheError::Any(\n            \"Operation not supported by null cache\".into(),\n        ))\n    }\n\n    /// Checks if a key exists in the cache.\n    ///\n    /// # Errors\n    ///\n    /// Returns always error\n    async fn contains_key(&self, _key: &str) -> CacheResult<bool> {\n        Err(CacheError::Any(\n            \"Operation not supported by null cache\".into(),\n        ))\n    }\n\n    /// Retrieves a value from the cache based on the provided key.\n    ///\n    /// # Errors\n    ///\n    /// Returns always error\n    async fn get(&self, _key: &str) -> CacheResult<Option<String>> {\n        Ok(None)\n    }\n\n    /// Inserts a key-value pair into the cache.\n    ///\n    /// # Errors\n    ///\n    /// Returns always error\n    async fn insert(&self, _key: &str, _value: &str) -> CacheResult<()> {\n        Err(CacheError::Any(\n            \"Operation not supported by null cache\".into(),\n        ))\n    }\n\n    /// Inserts a key-value pair into the cache that expires after the\n    /// provided duration.\n    ///\n    /// # Errors\n    ///\n    /// Returns always error\n    async fn insert_with_expiry(\n        &self,\n        _key: &str,\n        _value: &str,\n        _duration: Duration,\n    ) -> CacheResult<()> {\n        Err(CacheError::Any(\n            \"Operation not supported by null cache\".into(),\n        ))\n    }\n\n    /// Removes a key-value pair from the cache.\n    ///\n    /// # Errors\n    ///\n    /// Returns always error\n    async fn remove(&self, _key: &str) -> CacheResult<()> {\n        Err(CacheError::Any(\n            \"Operation not supported by null cache\".into(),\n        ))\n    }\n\n    /// Clears all key-value pairs from the cache.\n    ///\n    /// # Errors\n    ///\n    /// Returns always error\n    async fn clear(&self) -> CacheResult<()> {\n        Err(CacheError::Any(\n            \"Operation not supported by null cache\".into(),\n        ))\n    }\n}\n"
  },
  {
    "path": "src/cache/drivers/redis.rs",
    "content": "//! # Redis Cache Driver\n//!\n//! This module implements a cache driver using Redis.\nuse std::time::Duration;\n\nuse async_trait::async_trait;\nuse bb8::Pool;\nuse bb8_redis::{\n    bb8,\n    redis::{cmd, AsyncCommands},\n    RedisConnectionManager,\n};\n\nuse super::CacheDriver;\nuse crate::cache::{CacheError, CacheResult};\nuse crate::config::RedisCacheConfig;\n\n/// Creates a new instance of the Redis cache driver with a default configuration.\n///\n/// # Returns\n///\n/// A [`Cache`] instance.\n///\n/// # Errors\n///\n/// Returns a `CacheError` if there is an error connecting to Redis.\npub async fn new(config: &RedisCacheConfig) -> CacheResult<crate::cache::Cache> {\n    let manager = RedisConnectionManager::new(config.uri.clone())?;\n    let pool = Pool::builder()\n        .max_size(config.max_size)\n        .build(manager)\n        .await?;\n\n    Ok(crate::cache::Cache::new(Redis::from(pool)))\n}\n\n/// Represents the Redis cache driver.\n#[derive(Clone, Debug)]\npub struct Redis {\n    pool: Pool<RedisConnectionManager>,\n}\n\nimpl Redis {\n    /// Constructs a new [`Redis`] instance from a given connection pool.\n    ///\n    /// # Returns\n    ///\n    /// A boxed [`CacheDriver`] instance.\n    #[must_use]\n    pub fn from(pool: Pool<RedisConnectionManager>) -> Box<dyn CacheDriver> {\n        Box::new(Self { pool })\n    }\n}\n\n#[async_trait]\nimpl CacheDriver for Redis {\n    /// Sends a ping to Redis to check if it is reachable.\n    ///\n    /// # Errors\n    ///\n    /// Returns a `CacheError` if there is an error during the operation.\n    async fn ping(&self) -> CacheResult<()> {\n        let mut conn = self.pool.get().await?;\n        match conn.ping::<Option<String>>().await? {\n            Some(_) => Ok(()),\n            None => Err(CacheError::Any(\"Redis ping failed\".into())),\n        }\n    }\n\n    /// Checks if a key exists in the cache.\n    ///\n    /// # Errors\n    ///\n    /// Returns a `CacheError` if there is an error during the operation.\n    async fn contains_key(&self, key: &str) -> CacheResult<bool> {\n        let mut connection = self.pool.get().await?;\n        Ok(connection.exists(key).await?)\n    }\n\n    /// Retrieves a value from the cache based on the provided key.\n    ///\n    /// # Errors\n    ///\n    /// Returns a `CacheError` if there is an error during the operation.\n    async fn get(&self, key: &str) -> CacheResult<Option<String>> {\n        let mut conn = self.pool.get().await?;\n        let result: Option<String> = conn.get(key).await?;\n        Ok(result)\n    }\n\n    /// Inserts a key-value pair into the cache.\n    ///\n    /// # Errors\n    ///\n    /// Returns a `CacheError` if there is an error during the operation.\n    async fn insert(&self, key: &str, value: &str) -> CacheResult<()> {\n        let mut conn = self.pool.get().await?;\n        conn.set::<_, _, ()>(key, value).await?;\n        Ok(())\n    }\n\n    /// Inserts a key-value pair into the cache that expires after the specified\n    /// number of seconds.\n    ///\n    /// # Errors\n    ///\n    /// Returns a [`super::CacheError`] if there is an error during the\n    /// operation.\n    async fn insert_with_expiry(\n        &self,\n        key: &str,\n        value: &str,\n        duration: Duration,\n    ) -> CacheResult<()> {\n        let mut conn = self.pool.get().await?;\n        // Redis expects the expiry in seconds as a u64\n        conn.set_ex::<_, _, ()>(key, value, duration.as_secs())\n            .await?;\n        Ok(())\n    }\n\n    /// Removes a key-value pair from the cache.\n    ///\n    /// # Errors\n    ///\n    /// Returns a `CacheError` if there is an error during the operation.\n    async fn remove(&self, key: &str) -> CacheResult<()> {\n        let mut conn = self.pool.get().await?;\n        conn.del::<_, ()>(key).await?;\n        Ok(())\n    }\n\n    /// Clears all key-value pairs from the cache.\n    ///\n    /// # Errors\n    ///\n    /// Returns a `CacheError` if there is an error during the operation.\n    async fn clear(&self) -> CacheResult<()> {\n        let mut conn = self.pool.get().await?;\n        cmd(\"FLUSHDB\").query_async::<()>(&mut *conn).await?;\n        Ok(())\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use crate::tests_cfg::redis::setup_redis_container;\n    use std::time::Duration;\n    use testcontainers::{ContainerAsync, GenericImage};\n\n    use super::*;\n\n    async fn setup_redis_driver() -> (Box<dyn CacheDriver>, ContainerAsync<GenericImage>) {\n        let (redis_url, container) = setup_redis_container().await;\n\n        let redis_config = crate::config::RedisCacheConfig {\n            uri: redis_url,\n            max_size: 10,\n        };\n\n        let cache = new(&redis_config)\n            .await\n            .expect(\"Failed to create Redis driver\");\n\n        // Extract the driver from the Cache\n        let driver = cache.driver;\n\n        (driver, container)\n    }\n\n    #[tokio::test]\n    async fn ping_returns_pong_when_redis_is_reachable() {\n        let (redis, _container) = setup_redis_driver().await;\n\n        assert!(redis.ping().await.is_ok());\n    }\n\n    #[tokio::test]\n    async fn test_contains_key() {\n        let (redis, _container) = setup_redis_driver().await;\n\n        assert!(!redis\n            .contains_key(\"test_key\")\n            .await\n            .expect(\"Failed to check if key exists\"));\n\n        redis\n            .insert(\"test_key\", \"test_value\")\n            .await\n            .expect(\"Failed to insert key\");\n\n        assert!(redis\n            .contains_key(\"test_key\")\n            .await\n            .expect(\"Failed to check if key exists after insertion\"));\n    }\n\n    #[tokio::test]\n    async fn test_get_key_value() {\n        let (redis, _container) = setup_redis_driver().await;\n\n        redis\n            .insert(\"test_key\", \"test_value\")\n            .await\n            .expect(\"Failed to insert key\");\n\n        assert_eq!(\n            redis\n                .get(\"test_key\")\n                .await\n                .expect(\"Failed to get value for key\"),\n            Some(\"test_value\".to_string())\n        );\n\n        assert_eq!(\n            redis\n                .get(\"non_existent_key\")\n                .await\n                .expect(\"Failed to get value for non-existent key\"),\n            None\n        );\n    }\n\n    #[tokio::test]\n    async fn test_remove_key() {\n        let (redis, _container) = setup_redis_driver().await;\n\n        redis\n            .insert(\"test_key\", \"test_value\")\n            .await\n            .expect(\"Failed to insert key\");\n\n        assert!(redis\n            .contains_key(\"test_key\")\n            .await\n            .expect(\"Failed to check if key exists\"));\n\n        redis\n            .remove(\"test_key\")\n            .await\n            .expect(\"Failed to remove key\");\n\n        assert!(!redis\n            .contains_key(\"test_key\")\n            .await\n            .expect(\"Failed to check if key exists after removal\"));\n    }\n\n    #[tokio::test]\n    async fn test_clear() {\n        let (redis, _container) = setup_redis_driver().await;\n\n        let keys = vec![\"key1\", \"key2\", \"key3\"];\n        for key in &keys {\n            redis\n                .insert(key, \"test_value\")\n                .await\n                .expect(\"Failed to insert key\");\n        }\n\n        for key in &keys {\n            assert!(redis\n                .contains_key(key)\n                .await\n                .expect(\"Failed to check if key exists\"));\n        }\n\n        redis.clear().await.expect(\"Failed to clear cache\");\n\n        for key in &keys {\n            assert!(!redis\n                .contains_key(key)\n                .await\n                .expect(\"Failed to check if key exists after clear\"));\n        }\n    }\n\n    #[tokio::test]\n    async fn test_expiry() {\n        let (redis, _container) = setup_redis_driver().await;\n\n        redis\n            .insert_with_expiry(\"expiring_key\", \"test_value\", Duration::from_secs(1))\n            .await\n            .expect(\"Failed to insert key with expiry\");\n\n        assert!(redis\n            .contains_key(\"expiring_key\")\n            .await\n            .expect(\"Failed to check if key exists\"));\n\n        tokio::time::sleep(Duration::from_secs(2)).await;\n\n        assert!(!redis\n            .contains_key(\"expiring_key\")\n            .await\n            .expect(\"Failed to check if key exists after expiry\"));\n    }\n}\n"
  },
  {
    "path": "src/cache/mod.rs",
    "content": "//! # Cache Module\n//!\n//! This module provides a generic cache interface for various cache drivers.\npub mod drivers;\n\nuse std::{future::Future, time::Duration};\n\nuse serde::{de::DeserializeOwned, Serialize};\n\npub use self::drivers::CacheDriver;\nuse crate::config;\nuse crate::Result as LocoResult;\nuse std::sync::Arc;\n\n/// Errors related to cache operations\n#[derive(thiserror::Error, Debug)]\n#[allow(clippy::module_name_repetitions)]\npub enum CacheError {\n    #[error(transparent)]\n    Any(#[from] Box<dyn std::error::Error + Send + Sync>),\n\n    #[error(\"Serialization error: {0}\")]\n    Serialization(String),\n\n    #[error(\"Deserialization error: {0}\")]\n    Deserialization(String),\n\n    #[cfg(feature = \"cache_redis\")]\n    #[error(transparent)]\n    Redis(#[from] bb8_redis::redis::RedisError),\n\n    #[cfg(feature = \"cache_redis\")]\n    #[error(transparent)]\n    RedisConnectionError(#[from] bb8_redis::bb8::RunError<bb8_redis::redis::RedisError>),\n}\n\npub type CacheResult<T> = std::result::Result<T, CacheError>;\n\n/// Create a provider\n///\n/// # Errors\n///\n/// This function will return an error if fails to build\n#[allow(clippy::unused_async)]\npub async fn create_cache_provider(config: &config::Config) -> crate::Result<Arc<Cache>> {\n    match &config.cache {\n        #[cfg(feature = \"cache_redis\")]\n        config::CacheConfig::Redis(config) => {\n            let cache = crate::cache::drivers::redis::new(config).await?;\n            Ok(Arc::new(cache))\n        }\n        #[cfg(feature = \"cache_inmem\")]\n        config::CacheConfig::InMem(config) => {\n            let cache = crate::cache::drivers::inmem::new(config);\n            Ok(Arc::new(cache))\n        }\n        config::CacheConfig::Null => {\n            let driver = crate::cache::drivers::null::new();\n            Ok(Arc::new(Cache::new(driver)))\n        }\n    }\n}\n\n/// Represents a cache instance\npub struct Cache {\n    /// The cache driver used for underlying operations\n    pub driver: Box<dyn CacheDriver>,\n}\n\nimpl Cache {\n    /// Creates a new cache instance with the specified cache driver.\n    #[must_use]\n    pub fn new(driver: Box<dyn CacheDriver>) -> Self {\n        Self { driver }\n    }\n\n    /// Pings the cache to check if it is reachable.\n    ///\n    /// # Example\n    /// ```\n    /// use loco_rs::cache::{self, CacheResult};\n    /// use loco_rs::config::InMemCacheConfig;\n    ///\n    /// pub async fn ping() -> CacheResult<()> {\n    ///     let config = InMemCacheConfig { max_capacity: 100 };\n    ///     let cache = cache::Cache::new(cache::drivers::inmem::new(&config).driver);\n    ///     cache.ping().await\n    /// }\n    /// ```\n    ///\n    /// # Errors\n    /// A [`CacheResult`] indicating whether the cache is reachable.\n    pub async fn ping(&self) -> CacheResult<()> {\n        self.driver.ping().await\n    }\n\n    /// Checks if a key exists in the cache.\n    ///\n    /// # Example\n    /// ```\n    /// use loco_rs::cache::{self, CacheResult};\n    /// use loco_rs::config::InMemCacheConfig;\n    ///\n    /// pub async fn contains_key() -> CacheResult<bool> {\n    ///     let config = InMemCacheConfig { max_capacity: 100 };\n    ///     let cache = cache::Cache::new(cache::drivers::inmem::new(&config).driver);\n    ///     cache.contains_key(\"key\").await\n    /// }\n    /// ```\n    ///\n    /// # Errors\n    /// A [`CacheResult`] indicating whether the key exists in the cache.\n    pub async fn contains_key(&self, key: &str) -> CacheResult<bool> {\n        self.driver.contains_key(key).await\n    }\n\n    /// Retrieves a value from the cache based on the provided key and deserializes it.\n    ///\n    /// # Example\n    /// ```\n    /// use loco_rs::cache::{self, CacheResult};\n    /// use loco_rs::config::InMemCacheConfig;\n    /// use serde::Deserialize;\n    ///\n    /// #[derive(Deserialize)]\n    /// struct User {\n    ///     name: String,\n    ///     age: u32,\n    /// }\n    ///\n    /// pub async fn get_user() -> CacheResult<Option<User>> {\n    ///     let config = InMemCacheConfig { max_capacity: 100 };\n    ///     let cache = cache::Cache::new(cache::drivers::inmem::new(&config).driver);\n    ///     cache.get::<User>(\"user:1\").await\n    /// }\n    /// ```\n    ///\n    /// # Example with String\n    /// ```\n    /// use loco_rs::cache::{self, CacheResult};\n    /// use loco_rs::config::InMemCacheConfig;\n    ///\n    /// pub async fn get_string() -> CacheResult<Option<String>> {\n    ///     let config = InMemCacheConfig { max_capacity: 100 };\n    ///     let cache = cache::Cache::new(cache::drivers::inmem::new(&config).driver);\n    ///     cache.get::<String>(\"key\").await\n    /// }\n    /// ```\n    ///\n    /// # Errors\n    /// A [`CacheResult`] containing an `Option` representing the retrieved\n    /// and deserialized value.\n    pub async fn get<T: DeserializeOwned>(&self, key: &str) -> CacheResult<Option<T>> {\n        let result = self.driver.get(key).await?;\n        if let Some(value) = result {\n            let deserialized = serde_json::from_str::<T>(&value)\n                .map_err(|e| CacheError::Deserialization(e.to_string()))?;\n            Ok(Some(deserialized))\n        } else {\n            Ok(None)\n        }\n    }\n\n    /// Inserts a serializable value into the cache with the provided key.\n    ///\n    /// # Example\n    /// ```\n    /// use loco_rs::cache::{self, CacheResult};\n    /// use loco_rs::config::InMemCacheConfig;\n    /// use serde::Serialize;\n    ///\n    /// #[derive(Serialize)]\n    /// struct User {\n    ///     name: String,\n    ///     age: u32,\n    /// }\n    ///\n    /// pub async fn insert() -> CacheResult<()> {\n    ///     let config = InMemCacheConfig { max_capacity: 100 };\n    ///     let cache = cache::Cache::new(cache::drivers::inmem::new(&config).driver);\n    ///     let user = User { name: \"Alice\".to_string(), age: 30 };\n    ///     cache.insert(\"user:1\", &user).await\n    /// }\n    /// ```\n    ///\n    /// # Example with String\n    /// ```\n    /// use loco_rs::cache::{self, CacheResult};\n    /// use loco_rs::config::InMemCacheConfig;\n    ///\n    /// pub async fn insert_string() -> CacheResult<()> {\n    ///     let config = InMemCacheConfig { max_capacity: 100 };\n    ///     let cache = cache::Cache::new(cache::drivers::inmem::new(&config).driver);\n    ///     cache.insert(\"key\", &\"value\".to_string()).await\n    /// }\n    /// ```\n    ///\n    /// # Errors\n    ///\n    /// A [`CacheResult`] indicating the success of the operation.\n    pub async fn insert<T: Serialize + Sync + ?Sized>(\n        &self,\n        key: &str,\n        value: &T,\n    ) -> CacheResult<()> {\n        let serialized =\n            serde_json::to_string(value).map_err(|e| CacheError::Serialization(e.to_string()))?;\n        self.driver.insert(key, &serialized).await\n    }\n\n    /// Inserts a serializable value into the cache with the provided key and expiry duration.\n    ///\n    /// # Example\n    /// ```\n    /// use std::time::Duration;\n    /// use loco_rs::cache::{self, CacheResult};\n    /// use loco_rs::config::InMemCacheConfig;\n    /// use serde::Serialize;\n    ///\n    /// #[derive(Serialize)]\n    /// struct User {\n    ///     name: String,\n    ///     age: u32,\n    /// }\n    ///\n    /// pub async fn insert() -> CacheResult<()> {\n    ///     let config = InMemCacheConfig { max_capacity: 100 };\n    ///     let cache = cache::Cache::new(cache::drivers::inmem::new(&config).driver);\n    ///     let user = User { name: \"Alice\".to_string(), age: 30 };\n    ///     cache.insert_with_expiry(\"user:1\", &user, Duration::from_secs(300)).await\n    /// }\n    /// ```\n    ///\n    /// # Example with String\n    /// ```\n    /// use std::time::Duration;\n    /// use loco_rs::cache::{self, CacheResult};\n    /// use loco_rs::config::InMemCacheConfig;\n    ///\n    /// pub async fn insert_string() -> CacheResult<()> {\n    ///     let config = InMemCacheConfig { max_capacity: 100 };\n    ///     let cache = cache::Cache::new(cache::drivers::inmem::new(&config).driver);\n    ///     cache.insert_with_expiry(\"key\", &\"value\".to_string(), Duration::from_secs(300)).await\n    /// }\n    /// ```\n    ///\n    /// # Errors\n    ///\n    /// A [`CacheResult`] indicating the success of the operation.\n    pub async fn insert_with_expiry<T: Serialize + Sync + ?Sized>(\n        &self,\n        key: &str,\n        value: &T,\n        duration: Duration,\n    ) -> CacheResult<()> {\n        let serialized =\n            serde_json::to_string(value).map_err(|e| CacheError::Serialization(e.to_string()))?;\n        self.driver\n            .insert_with_expiry(key, &serialized, duration)\n            .await\n    }\n\n    /// Retrieves and deserializes the value associated with the given key from the cache,\n    /// or inserts it if it does not exist, using the provided closure to\n    /// generate the value.\n    ///\n    /// # Example\n    /// ```\n    /// use loco_rs::{app::AppContext};\n    /// use loco_rs::tests_cfg::app::*;\n    /// use serde::{Serialize, Deserialize};\n    ///\n    /// #[derive(Serialize, Deserialize, PartialEq, Debug)]\n    /// struct User {\n    ///     name: String,\n    ///     age: u32,\n    /// }\n    ///\n    /// pub async fn get_or_insert(){\n    ///    let app_ctx = get_app_context().await;\n    ///    let user = app_ctx.cache.get_or_insert::<User, _>(\"user:1\", async {\n    ///            Ok(User { name: \"Alice\".to_string(), age: 30 })\n    ///     }).await.unwrap();\n    ///    assert_eq!(user.name, \"Alice\");\n    /// }\n    /// ```\n    ///\n    /// # Example with String\n    /// ```\n    /// use loco_rs::{app::AppContext};\n    /// use loco_rs::tests_cfg::app::*;\n    ///\n    /// pub async fn get_or_insert_string(){\n    ///    let app_ctx = get_app_context().await;\n    ///    let res = app_ctx.cache.get_or_insert::<String, _>(\"key\", async {\n    ///            Ok(\"value\".to_string())\n    ///     }).await.unwrap();\n    ///    assert_eq!(res, \"value\");\n    /// }\n    /// ```\n    ///\n    /// # Errors\n    ///\n    /// A [`LocoResult`] indicating the success of the operation.\n    pub async fn get_or_insert<T, F>(&self, key: &str, f: F) -> LocoResult<T>\n    where\n        T: Serialize + DeserializeOwned + Send + Sync,\n        F: Future<Output = LocoResult<T>> + Send,\n    {\n        if let Some(value) = self.get::<T>(key).await? {\n            Ok(value)\n        } else {\n            let value = f.await?;\n            self.insert(key, &value).await?;\n            Ok(value)\n        }\n    }\n\n    /// Retrieves and deserializes the value associated with the given key from the cache,\n    /// or inserts it (with expiry after provided duration) if it does not\n    /// exist, using the provided closure to generate the value.\n    ///\n    /// # Example\n    /// ```\n    /// use std::time::Duration;\n    /// use loco_rs::{app::AppContext};\n    /// use loco_rs::tests_cfg::app::*;\n    /// use serde::{Serialize, Deserialize};\n    ///\n    /// #[derive(Serialize, Deserialize, PartialEq, Debug)]\n    /// struct User {\n    ///     name: String,\n    ///     age: u32,\n    /// }\n    ///\n    /// pub async fn get_or_insert(){\n    ///    let app_ctx = get_app_context().await;\n    ///    let user = app_ctx.cache.get_or_insert_with_expiry::<User, _>(\"user:1\", Duration::from_secs(300), async {\n    ///            Ok(User { name: \"Alice\".to_string(), age: 30 })\n    ///     }).await.unwrap();\n    ///    assert_eq!(user.name, \"Alice\");\n    /// }\n    /// ```\n    ///\n    /// # Example with String\n    /// ```\n    /// use std::time::Duration;\n    /// use loco_rs::{app::AppContext};\n    /// use loco_rs::tests_cfg::app::*;\n    ///\n    /// pub async fn get_or_insert_string(){\n    ///    let app_ctx = get_app_context().await;\n    ///    let res = app_ctx.cache.get_or_insert_with_expiry::<String, _>(\"key\", Duration::from_secs(300), async {\n    ///            Ok(\"value\".to_string())\n    ///     }).await.unwrap();\n    ///    assert_eq!(res, \"value\");\n    /// }\n    /// ```\n    ///\n    /// # Errors\n    ///\n    /// A [`LocoResult`] indicating the success of the operation.\n    pub async fn get_or_insert_with_expiry<T, F>(\n        &self,\n        key: &str,\n        duration: Duration,\n        f: F,\n    ) -> LocoResult<T>\n    where\n        T: Serialize + DeserializeOwned + Send + Sync,\n        F: Future<Output = LocoResult<T>> + Send,\n    {\n        if let Some(value) = self.get::<T>(key).await? {\n            Ok(value)\n        } else {\n            let value = f.await?;\n            self.insert_with_expiry(key, &value, duration).await?;\n            Ok(value)\n        }\n    }\n\n    /// Removes a key-value pair from the cache.\n    ///\n    /// # Example\n    /// ```\n    /// use loco_rs::cache::{self, CacheResult};\n    /// use loco_rs::config::InMemCacheConfig;\n    ///\n    /// pub async fn remove() -> CacheResult<()> {\n    ///     let config = InMemCacheConfig { max_capacity: 100 };\n    ///     let cache = cache::Cache::new(cache::drivers::inmem::new(&config).driver);\n    ///     cache.remove(\"key\").await\n    /// }\n    /// ```\n    ///\n    /// # Errors\n    ///\n    /// A [`CacheResult`] indicating the success of the operation.\n    pub async fn remove(&self, key: &str) -> CacheResult<()> {\n        self.driver.remove(key).await\n    }\n\n    /// Clears all key-value pairs from the cache.\n    ///\n    /// # Example\n    /// ```\n    /// use loco_rs::cache::{self, CacheResult};\n    /// use loco_rs::config::InMemCacheConfig;\n    ///\n    /// pub async fn clear() -> CacheResult<()> {\n    ///     let config = InMemCacheConfig { max_capacity: 100 };\n    ///     let cache = cache::Cache::new(cache::drivers::inmem::new(&config).driver);\n    ///     cache.clear().await\n    /// }\n    /// ```\n    ///\n    /// # Errors\n    ///\n    /// A [`CacheResult`] indicating the success of the operation.\n    pub async fn clear(&self) -> CacheResult<()> {\n        self.driver.clear().await\n    }\n}\n\n#[cfg(test)]\nmod tests {\n\n    use crate::tests_cfg;\n    use serde::{Deserialize, Serialize};\n\n    #[tokio::test]\n    async fn can_get_or_insert() {\n        let app_ctx = tests_cfg::app::get_app_context().await;\n        let get_key = \"loco\";\n\n        assert_eq!(app_ctx.cache.get::<String>(get_key).await.unwrap(), None);\n\n        let result = app_ctx\n            .cache\n            .get_or_insert::<String, _>(get_key, async { Ok(\"loco-cache-value\".to_string()) })\n            .await\n            .unwrap();\n\n        assert_eq!(result, \"loco-cache-value\".to_string());\n        assert_eq!(\n            app_ctx.cache.get::<String>(get_key).await.unwrap(),\n            Some(\"loco-cache-value\".to_string())\n        );\n    }\n\n    #[derive(Debug, Serialize, Deserialize, PartialEq)]\n    struct TestUser {\n        name: String,\n        age: u32,\n    }\n\n    #[tokio::test]\n    async fn can_serialize_deserialize() {\n        let app_ctx = tests_cfg::app::get_app_context().await;\n        let key = \"user:test\";\n\n        // Test user data\n        let user = TestUser {\n            name: \"Test User\".to_string(),\n            age: 42,\n        };\n\n        // Insert serialized user\n        app_ctx.cache.insert(key, &user).await.unwrap();\n\n        // Retrieve and deserialize user\n        let retrieved: Option<TestUser> = app_ctx.cache.get(key).await.unwrap();\n        assert!(retrieved.is_some());\n        assert_eq!(retrieved.unwrap(), user);\n    }\n\n    #[tokio::test]\n    async fn can_get_or_insert_generic() {\n        let app_ctx = tests_cfg::app::get_app_context().await;\n        let key = \"user:get_or_insert\";\n\n        // The key should not exist initially\n        let no_user: Option<TestUser> = app_ctx.cache.get(key).await.unwrap();\n        assert!(no_user.is_none());\n\n        // Get or insert should create the user\n        let user = app_ctx\n            .cache\n            .get_or_insert::<TestUser, _>(key, async {\n                Ok(TestUser {\n                    name: \"Alice\".to_string(),\n                    age: 30,\n                })\n            })\n            .await\n            .unwrap();\n\n        assert_eq!(user.name, \"Alice\");\n        assert_eq!(user.age, 30);\n\n        // Verify the user was stored in the cache\n        let retrieved: TestUser = app_ctx\n            .cache\n            .get_or_insert::<TestUser, _>(key, async {\n                // This should not be called\n                Ok(TestUser {\n                    name: \"Bob\".to_string(),\n                    age: 25,\n                })\n            })\n            .await\n            .unwrap();\n\n        // Should retrieve Alice, not Bob\n        assert_eq!(retrieved.name, \"Alice\");\n        assert_eq!(retrieved.age, 30);\n    }\n}\n"
  },
  {
    "path": "src/cargo_config.rs",
    "content": "//! Cargo.toml configuration reader module\n//!\n//! This module provides functionality to read and parse Cargo.toml files,\n//! with specific support for accessing database entity configuration\n//! under the `[package.metadata.db.entity]` section.\n\nuse crate::errors::Error;\nuse crate::Result as AppResult;\nuse std::path::Path;\nuse toml::Table;\n\n/// Represents a parsed Cargo.toml configuration\n///\n/// This struct holds the parsed TOML data from a Cargo.toml file\n/// and provides methods to access specific sections of the configuration.\npub struct CargoConfig {\n    toml: Table,\n}\n\nimpl CargoConfig {\n    /// Creates a new [`CargoConfig`] by reading the Cargo.toml file from the current directory\n    ///\n    /// # Errors\n    /// * If the Cargo.toml file cannot be read\n    /// * If the file contains invalid TOML\n    pub fn from_current_dir() -> AppResult<Self> {\n        Self::from_path(\"Cargo.toml\")\n    }\n\n    /// Creates a new [`CargoConfig`] by reading the Cargo.lock file from the current directory\n    ///\n    /// # Errors\n    /// * If the Cargo.lock file cannot be read\n    /// * If the file contains invalid TOML\n    pub fn lock_from_current_dir() -> AppResult<Self> {\n        Self::from_path(\"Cargo.lock\")\n    }\n\n    /// Creates a new [`CargoConfig`] by reading and parsing a TOML file from the specified path\n    ///\n    /// # Errors\n    /// * If the file cannot be read\n    /// * If the file contains invalid TOML\n    pub fn from_path(path: impl AsRef<Path>) -> AppResult<Self> {\n        let content = std::fs::read_to_string(path)\n            .map_err(|e| Error::Message(format!(\"Failed to read Cargo.toml: {e}\")))?;\n\n        let toml = content\n            .parse::<Table>()\n            .map_err(|e| Error::Message(format!(\"Failed to parse Cargo.toml: {e}\")))?;\n\n        Ok(Self { toml })\n    }\n\n    /// Retrieves the database entity configuration from the Cargo.toml\n    ///\n    /// Looks for configuration under the `[package.metadata.db.entity]` section.\n    #[must_use]\n    pub fn get_db_entities(&self) -> Option<&Table> {\n        self.toml\n            .get(\"package\")\n            .and_then(|p| p.as_table())\n            .and_then(|p| p.get(\"metadata\"))\n            .and_then(|m| m.as_table())\n            .and_then(|m| m.get(\"db\"))\n            .and_then(|d| d.as_table())\n            .and_then(|d| d.get(\"entity\"))\n            .and_then(|e| e.as_table())\n    }\n\n    /// Gets the package array from Cargo.lock\n    ///\n    /// # Errors\n    /// Returns an error if the package array is missing or invalid\n    pub fn get_package_array(&self) -> AppResult<&[toml::Value]> {\n        self.toml\n            .get(\"package\")\n            .and_then(|v| v.as_array())\n            .map(std::vec::Vec::as_slice)\n            .ok_or_else(|| Error::Message(\"Missing package array in Cargo.lock\".to_string()))\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use std::path::{Path, PathBuf};\n\n    struct CwdGuard(PathBuf);\n\n    impl CwdGuard {\n        fn set_to(path: &Path) -> Self {\n            let previous = std::env::current_dir().expect(\"Failed to get current directory\");\n            std::env::set_current_dir(path).expect(\"Failed to change directory\");\n            Self(previous)\n        }\n    }\n\n    impl Drop for CwdGuard {\n        fn drop(&mut self) {\n            let _ = std::env::set_current_dir(&self.0);\n        }\n    }\n\n    const TEST_CARGO_TOML: &str = r#\"\n[package]\nname = \"test-app\"\nversion = \"0.1.0\"\n\n[package.metadata.db.entity]\nwith-serde = \"serialize\"\ncompact-format = true\n\"#;\n\n    const TEST_CARGO_LOCK: &str = r#\"\n[[package]]\nname = \"test-app\"\nversion = \"0.1.0\"\ndependencies = [\n \"serde\",\n \"tokio\",\n]\n\n[[package]]\nname = \"serde\"\nversion = \"1.0.130\"\n\n[[package]]\nname = \"tokio\"\nversion = \"1.0.0\"\n\"#;\n\n    fn setup_test_dir(cargo_toml: Option<&str>) -> tree_fs::Tree {\n        tree_fs::TreeBuilder::default()\n            .add_file(\"Cargo.toml\", cargo_toml.unwrap_or(TEST_CARGO_TOML))\n            .create()\n            .expect(\"Failed to create test directory structure\")\n    }\n\n    fn setup_test_dir_with_lock(\n        cargo_toml: Option<&str>,\n        cargo_lock: Option<&str>,\n    ) -> tree_fs::Tree {\n        tree_fs::TreeBuilder::default()\n            .add_file(\"Cargo.toml\", cargo_toml.unwrap_or(TEST_CARGO_TOML))\n            .add_file(\"Cargo.lock\", cargo_lock.unwrap_or(TEST_CARGO_LOCK))\n            .create()\n            .expect(\"Failed to create test directory structure\")\n    }\n\n    #[test]\n    fn test_from_path_valid_toml() {\n        let tree = setup_test_dir(None);\n        let config = CargoConfig::from_path(tree.root.join(\"Cargo.toml\"))\n            .expect(\"Failed to read Cargo.toml\");\n\n        assert_eq!(config.toml[\"package\"][\"name\"].as_str(), Some(\"test-app\"));\n        assert_eq!(config.toml[\"package\"][\"version\"].as_str(), Some(\"0.1.0\"));\n    }\n\n    #[test]\n    fn test_from_current_dir() {\n        let tree = setup_test_dir(None);\n        let _cwd_guard = CwdGuard::set_to(&tree.root);\n\n        let config = CargoConfig::from_current_dir().expect(\"Failed to read from current dir\");\n        assert_eq!(config.toml[\"package\"][\"name\"].as_str(), Some(\"test-app\"));\n    }\n\n    #[test]\n    fn test_lock_from_current_dir() {\n        let tree = setup_test_dir_with_lock(None, None);\n        let _cwd_guard = CwdGuard::set_to(&tree.root);\n\n        let config = CargoConfig::lock_from_current_dir().expect(\"Failed to read Cargo.lock\");\n        let packages = config\n            .get_package_array()\n            .expect(\"Failed to get package array\");\n        assert_eq!(packages.len(), 3);\n        assert_eq!(\n            packages[0].as_table().unwrap()[\"name\"].as_str(),\n            Some(\"test-app\")\n        );\n    }\n\n    #[test]\n    fn test_get_package_array() {\n        let tree = setup_test_dir_with_lock(None, None);\n        let config = CargoConfig::from_path(tree.root.join(\"Cargo.lock\"))\n            .expect(\"Failed to read Cargo.lock\");\n\n        let packages = config\n            .get_package_array()\n            .expect(\"Failed to get package array\");\n        assert_eq!(packages.len(), 3);\n        assert_eq!(\n            packages[1].as_table().unwrap()[\"name\"].as_str(),\n            Some(\"serde\")\n        );\n    }\n\n    #[test]\n    fn test_get_db_entities() {\n        let tree = setup_test_dir(None);\n        let config = CargoConfig::from_path(tree.root.join(\"Cargo.toml\"))\n            .expect(\"Failed to read Cargo.toml\");\n\n        let entities = config\n            .get_db_entities()\n            .expect(\"No db entities found in Cargo.toml\");\n        assert_eq!(entities[\"with-serde\"].as_str(), Some(\"serialize\"));\n        assert!(entities[\"compact-format\"].as_bool().unwrap());\n    }\n\n    #[test]\n    fn test_get_db_entities_no_config() {\n        let tree = setup_test_dir(Some(\n            r#\"\n[package]\nname = \"test-app\"\nversion = \"0.1.0\"\n\"#,\n        ));\n\n        let config = CargoConfig::from_path(tree.root.join(\"Cargo.toml\"))\n            .expect(\"Failed to read Cargo.toml\");\n\n        let entities = config.get_db_entities();\n        assert!(entities.is_none());\n    }\n\n    #[test]\n    fn test_invalid_toml() {\n        let tree = setup_test_dir(Some(\n            r\"\n[package\ninvalid toml content\n\",\n        ));\n\n        let result = CargoConfig::from_path(tree.root.join(\"Cargo.toml\"));\n        assert!(result.is_err());\n    }\n\n    #[test]\n    fn test_file_not_found() {\n        let result = CargoConfig::from_path(\"/non/existent/path/Cargo.toml\");\n        assert!(result.is_err());\n    }\n}\n"
  },
  {
    "path": "src/cli.rs",
    "content": "//! command-line interface for running various tasks and commands\n//! related to the application. It allows developers to interact with the\n//! application via the command line.\n//!\n//! # Example\n//!\n//! ```rust,ignore\n//! use myapp::app::App;\n//! use loco_rs::cli;\n//! use migration::Migrator;\n//!\n//! #[tokio::main]\n//! async fn main() {\n//!     cli::main::<App, Migrator>().await\n//! }\n//! ```\n#[cfg(feature = \"with-db\")]\nuse {crate::boot::run_db, crate::db, sea_orm_migration::MigratorTrait};\n\nuse clap::{ArgAction, ArgGroup, Parser, Subcommand, ValueHint};\nuse colored::Colorize;\nuse duct::cmd;\nuse std::fmt::Write;\nuse std::process::exit;\nuse std::{collections::BTreeMap, path::PathBuf};\n\n#[cfg(any(feature = \"bg_redis\", feature = \"bg_pg\", feature = \"bg_sqlt\"))]\nuse crate::bgworker::JobStatus;\n#[cfg(debug_assertions)]\nuse crate::controller;\nuse crate::{\n    app::{AppContext, Hooks},\n    boot::{\n        create_app, create_context, list_endpoints, list_middlewares, run_scheduler, run_task,\n        start, RunDbCommand, ServeParams, StartMode,\n    },\n    config::Config,\n    doctor,\n    environment::{resolve_from_env, Environment, DEFAULT_ENVIRONMENT},\n    logger, task, Error,\n};\n\n#[derive(Parser)]\n#[command(author, version, about, long_about = None)]\n#[command(propagate_version = true)]\nstruct Playground {\n    /// Specify the environment\n    #[arg(short, long, global = true, help = &format!(\"Specify the environment [default: {}]\", DEFAULT_ENVIRONMENT))]\n    environment: Option<String>,\n}\n\n#[derive(Parser)]\n#[command(author, version, about, long_about = None)]\n#[command(propagate_version = true)]\nstruct Cli {\n    #[command(subcommand)]\n    command: Commands,\n\n    /// Specify the environment\n    #[arg(short, long, global = true, help = &format!(\"Specify the environment [default: {}]\", DEFAULT_ENVIRONMENT))]\n    environment: Option<String>,\n}\n\n#[derive(Subcommand)]\nenum Commands {\n    /// Start an app\n    #[command(group(ArgGroup::new(\"start_mode\").args(&[\"worker\", \"server_and_worker\", \"all\"])))]\n    #[clap(alias(\"s\"))]\n    Start {\n        /// Start worker. Optionally provide tags to run specific jobs (e.g. --worker=tag1,tag2)\n        #[arg(short, long, action, value_delimiter = ',', num_args = 0.., conflicts_with_all = &[\"server_and_worker\", \"all\"])]\n        worker: Option<Vec<String>>,\n        /// Start the server and worker in the same process\n        #[arg(short, long, action, conflicts_with_all = &[\"worker\", \"all\"])]\n        server_and_worker: bool,\n        /// Start the server, worker, and scheduler in the same process\n        #[arg(short, long, action, conflicts_with_all = &[\"worker\", \"server_and_worker\"])]\n        all: bool,\n        /// server bind address\n        #[arg(short, long, action)]\n        binding: Option<String>,\n        /// server port address\n        #[arg(short, long, action)]\n        port: Option<i32>,\n        /// disable the banner display\n        #[arg(short, long, action = ArgAction::SetTrue)]\n        no_banner: bool,\n    },\n    #[cfg(feature = \"with-db\")]\n    /// Perform DB operations\n    Db {\n        #[command(subcommand)]\n        command: DbCommands,\n    },\n    /// Describe all application endpoints\n    Routes {},\n    /// Describe all application middlewares\n    Middleware {\n        // print out the middleware configurations.\n        #[arg(short = 'c', long = \"config\", action)]\n        show_config: bool,\n    },\n    /// Run a custom task\n    #[clap(alias(\"t\"))]\n    Task {\n        /// Task name (identifier)\n        name: Option<String>,\n        /// Task params (e.g. <`my_task`> foo:bar baz:qux)\n        #[clap(value_parser = parse_key_val::<String,String>)]\n        params: Vec<(String, String)>,\n    },\n    #[cfg(any(feature = \"bg_redis\", feature = \"bg_pg\", feature = \"bg_sqlt\"))]\n    /// Managing jobs queue.\n    Jobs {\n        #[command(subcommand)]\n        command: JobsCommands,\n    },\n    /// Run the scheduler\n    Scheduler {\n        /// Run a specific job by its name.\n        #[arg(short, long, action)]\n        name: Option<String>,\n        /// Run jobs that are associated with a specific tag.\n        #[arg(short, long, action)]\n        tag: Option<String>,\n        /// Specify a path to a dedicated scheduler configuration file. by\n        /// default load schedulers job setting from environment config.\n        #[clap(value_parser)]\n        #[arg(short = 'c', long = \"config\", action, value_hint = ValueHint::FilePath)]\n        config_path: Option<PathBuf>,\n        /// Show all configured jobs\n        #[arg(short, long, action)]\n        list: bool,\n    },\n    /// code generation creates a set of files and code templates based on a\n    /// predefined set of rules.\n    #[cfg(debug_assertions)]\n    #[clap(alias(\"g\"))]\n    Generate {\n        /// What to generate\n        #[command(subcommand)]\n        component: ComponentArg,\n    },\n    /// Validate and diagnose configurations.\n    Doctor {\n        /// print out the current configurations.\n        #[arg(short, long, action)]\n        config: bool,\n        #[arg(short, long, action)]\n        production: bool,\n    },\n    /// Display the app version\n    Version {},\n\n    /// Watch and restart the app\n    #[clap(alias(\"w\"))]\n    Watch {\n        /// start worker\n        #[arg(short, long, action, value_delimiter = ',', num_args = 0..)]\n        worker: Option<Vec<String>>,\n        /// start same-process server and worker\n        #[arg(short, long, action)]\n        server_and_worker: bool,\n    },\n}\n\n#[cfg(debug_assertions)]\n#[derive(Subcommand)]\nenum ComponentArg {\n    #[cfg(feature = \"with-db\")]\n    /// Generates a new model file for defining the data structure of your\n    /// application, and test file logic.\n    #[command(after_help = format!(\n    \"{}  \n  - Generate empty model:\n      $ cargo loco g model posts\n\n  - Generate model with fields:\n      $ cargo loco g model posts title:string! content:text\n\n  - Generate model with references:\n      $ cargo loco g model movies long_title:string director:references award:references:prize_id\n      # 'director:references' references the 'directors' table with 'director_id' on 'movies'\n      # 'award:references:prize_id' references the 'awards' table with 'prize_id' on 'movies'\n\n  - Generate model without timestamps:\n      $ cargo loco g model posts title:string content:text --without-tz\n\",\n    \"Examples:\".bold().underline()\n))]\n    Model {\n        /// Name of the thing to generate\n        name: String,\n\n        /// Generate model without timestamps (`created_at`, `updated_at` columns)\n        #[arg(long, action)]\n        without_tz: bool,\n\n        /// Model fields, eg. title:string hits:int\n        #[clap(value_parser = parse_key_val::<String,String>)]\n        fields: Vec<(String, String)>,\n    },\n    #[cfg(feature = \"with-db\")]\n    /// Generates a new migration file\n    #[command(after_help = format!(\"{}\n  - Create a new table:\n      $ cargo loco g migration CreatePosts title:string\n      # Creates a migration to add a 'posts' table with a 'title' column of type string.\n\n  - Add columns to an existing table:\n      $ cargo loco g migration AddNameAndAgeToUsers name:string age:int\n      # Adds 'name' (string) and 'age' (integer) columns to the 'users' table.\n\n  - Remove columns from a table:\n      $ cargo loco g migration RemoveNameAndAgeFromUsers name:string age:int\n      # Removes 'name' and 'age' columns from the 'users' table.\n\n  - Add a foreign key reference:\n      $ cargo loco g migration AddUserRefToPosts user:references\n      # Adds a reference to the 'users' table in the 'posts' table.\n\n  - Create a join table:\n      $ cargo loco g migration CreateJoinTableUsersAndGroups count:int\n      # Creates a join table 'users_groups' with an additional 'count' column.\n\n  - Create an empty migration:\n      $ cargo loco g migration FixUsersTable\n      # Creates a blank migration file for custom edits to the 'users' table.\n\n  - Create migration without timestamps:\n      $ cargo loco g migration CreatePosts title:string --without-tz\n      # Creates a migration without timestamp columns\n\n  - Create join table without timestamps:\n      $ cargo loco g migration CreateJoinTableUsersAndGroups count:int --without-tz\n      # Creates a join table without timestamp columns\n\nAfter running the migration, follow these steps to complete the process:\n  - Apply the migration:\n    $ cargo loco db migrate\n  - Generate the model entities:\n    $ cargo loco db entities\n\", \"Examples:\".bold().underline()))]\n    Migration {\n        /// Name of the migration to generate\n        name: String,\n\n        /// Generate migration without timestamps (`created_at`, `updated_at` columns)\n        #[arg(long, action)]\n        without_tz: bool,\n\n        /// Table fields, eg. title:string hits:int\n        #[clap(value_parser = parse_key_val::<String,String>, )]\n        fields: Vec<(String, String)>,\n    },\n    #[cfg(feature = \"with-db\")]\n    /// Generates a CRUD scaffold, model and controller\n    #[command(after_help = format!(\"{}\n $ cargo loco g model posts title:string! user:references --api\n\n $ cargo loco g scaffold posts title:string! user:references --api --without-tz\", \"Examples:\".bold().underline()))]\n    Scaffold {\n        /// Name of the thing to generate\n        name: String,\n\n        /// Generate scaffold without timestamps (`created_at`, `updated_at` columns)\n        #[arg(long, action)]\n        without_tz: bool,\n\n        /// Model fields, eg. title:string hits:int\n        #[clap(value_parser = parse_key_val::<String,String>)]\n        fields: Vec<(String, String)>,\n\n        /// The kind of scaffold to generate\n        #[clap(short, long, value_enum, group = \"scaffold_kind_group\")]\n        kind: Option<loco_gen::ScaffoldKind>,\n\n        /// Use HTMX scaffold\n        #[clap(long, group = \"scaffold_kind_group\")]\n        htmx: bool,\n\n        /// Use HTML scaffold\n        #[clap(long, group = \"scaffold_kind_group\")]\n        html: bool,\n\n        /// Use API scaffold\n        #[clap(long, group = \"scaffold_kind_group\")]\n        api: bool,\n    },\n    /// Generate a new controller with the given controller name, and test file.\n    #[command(after_help = format!(\n    \"{}  \n  - Generate an empty controller:\n      $ cargo loco generate controller posts --api\n\n  - Generate a controller with actions:\n      $ cargo loco generate controller posts --api list remove update\n\",\n    \"Examples:\".bold().underline()\n))]\n    Controller {\n        /// Name of the thing to generate\n        name: String,\n\n        /// Actions\n        actions: Vec<String>,\n\n        /// The kind of controller actions to generate\n        #[clap(short, long, value_enum, group = \"scaffold_kind_group\")]\n        kind: Option<loco_gen::ScaffoldKind>,\n\n        /// Use HTMX controller actions\n        #[clap(long, group = \"scaffold_kind_group\")]\n        htmx: bool,\n\n        /// Use HTML controller actions\n        #[clap(long, group = \"scaffold_kind_group\")]\n        html: bool,\n\n        /// Use API controller actions\n        #[clap(long, group = \"scaffold_kind_group\")]\n        api: bool,\n    },\n    /// Generate a Task based on the given name\n    Task {\n        /// Name of the thing to generate\n        name: String,\n    },\n    /// Generate a scheduler jobs configuration template\n    Scheduler {},\n    /// Generate worker\n    Worker {\n        /// Name of the thing to generate\n        name: String,\n    },\n    /// Generate mailer\n    Mailer {\n        /// Name of the thing to generate\n        name: String,\n    },\n    /// Generate data loader\n    Data {\n        /// Name of the thing to generate\n        name: String,\n    },\n    /// Generate a deployment infrastructure\n    Deployment {\n        /// The type of deployment to generate\n        #[clap(value_enum)]\n        kind: DeploymentKind,\n    },\n\n    /// Override templates and allows you to take control of them. You can\n    /// always go back when deleting the local template.\n    #[command(after_help = format!(\"{}\n  - Override a Specific File:\n      * cargo loco generate override scaffold/api/controller.t\n      * cargo loco generate override migration/add_columns.t\n\n  - Override All Files in a Folder:\n      * cargo loco generate override scaffold/htmx\n      * cargo loco generate override task\n\n  - Override All templates:\n      * cargo loco generate override .\n\", \"Examples:\".bold().underline()))]\n    Override {\n        /// The path to a specific template or directory to copy.\n        template_path: Option<String>,\n\n        /// Show available templates to copy under the specified directory\n        /// without actually coping them.\n        #[arg(long, action)]\n        info: bool,\n    },\n}\n\n#[cfg(debug_assertions)]\nimpl ComponentArg {\n    fn into_gen_component(self, config: &Config) -> crate::Result<loco_gen::Component> {\n        match self {\n            #[cfg(feature = \"with-db\")]\n            Self::Model {\n                name,\n                without_tz,\n                fields,\n            } => Ok(loco_gen::Component::Model {\n                name,\n                with_tz: !without_tz,\n                fields,\n            }),\n            #[cfg(feature = \"with-db\")]\n            Self::Migration {\n                name,\n                without_tz,\n                fields,\n            } => Ok(loco_gen::Component::Migration {\n                name,\n                with_tz: !without_tz,\n                fields,\n            }),\n            #[cfg(feature = \"with-db\")]\n            Self::Scaffold {\n                name,\n                without_tz,\n                fields,\n                kind,\n                htmx,\n                html,\n                api,\n            } => {\n                let kind = if let Some(kind) = kind {\n                    kind\n                } else if htmx {\n                    loco_gen::ScaffoldKind::Htmx\n                } else if html {\n                    loco_gen::ScaffoldKind::Html\n                } else if api {\n                    loco_gen::ScaffoldKind::Api\n                } else {\n                    return Err(crate::Error::string(\n                        \"Error: generating this component requires one of `--kind`, `--htmx`, `--html`, or `--api` to be specified. Run with `--help` for more information.\",\n                    ));\n                };\n\n                Ok(loco_gen::Component::Scaffold {\n                    name,\n                    with_tz: !without_tz,\n                    fields,\n                    kind,\n                })\n            }\n            Self::Controller {\n                name,\n                actions,\n                kind,\n                htmx,\n                html,\n                api,\n            } => {\n                let kind = if let Some(kind) = kind {\n                    kind\n                } else if htmx {\n                    loco_gen::ScaffoldKind::Htmx\n                } else if html {\n                    loco_gen::ScaffoldKind::Html\n                } else if api {\n                    loco_gen::ScaffoldKind::Api\n                } else {\n                    return Err(crate::Error::string(\n                        \"Error: One of `kind`, `htmx`, `html`, or `api` must be specified.\",\n                    ));\n                };\n\n                Ok(loco_gen::Component::Controller {\n                    name,\n                    actions,\n                    kind,\n                })\n            }\n            Self::Task { name } => Ok(loco_gen::Component::Task { name }),\n            Self::Scheduler {} => Ok(loco_gen::Component::Scheduler {}),\n            Self::Worker { name } => Ok(loco_gen::Component::Worker { name }),\n            Self::Mailer { name } => Ok(loco_gen::Component::Mailer { name }),\n            Self::Data { name } => Ok(loco_gen::Component::Data { name }),\n            Self::Deployment { kind } => Ok(kind.to_generator_component(config)),\n            Self::Override {\n                template_path: _,\n                info: _,\n            } => Err(crate::Error::string(\n                \"Error: Override could not be generated.\",\n            )),\n        }\n    }\n}\n\n#[derive(Subcommand)]\nenum DbCommands {\n    /// Create schema\n    Create,\n    /// Migrate schema (up)\n    Migrate,\n    /// Run one down migration, or add a number to run multiple down migrations\n    /// (i.e. `down 2`)\n    Down {\n        /// The number of migrations to rollback\n        #[arg(default_value_t = 1)]\n        steps: u32,\n    },\n    /// Drop all tables, then reapply all migrations\n    Reset,\n    /// Migration status\n    Status,\n    /// Generate entity .rs files from database schema\n    #[cfg(debug_assertions)]\n    Entities,\n    /// Truncate data in tables (without dropping)\n    Truncate,\n    /// Seed your database with initial data or dump tables to files.\n    Seed {\n        /// Clears all data in the database before seeding.\n        #[arg(short, long)]\n        reset: bool,\n        /// Dumps all database tables to files.\n        #[arg(short, long)]\n        dump: bool,\n        /// Specifies specific tables to dump.\n        #[arg(long, value_delimiter = ',')]\n        dump_tables: Option<Vec<String>>,\n        /// Specifies the folder containing seed files (defaults to\n        /// 'src/fixtures').\n        #[arg(long, default_value = \"src/fixtures\")]\n        from: PathBuf,\n    },\n    /// Dump database schema\n    Schema,\n}\n\nimpl From<DbCommands> for RunDbCommand {\n    fn from(value: DbCommands) -> Self {\n        match value {\n            DbCommands::Migrate => Self::Migrate,\n            DbCommands::Down { steps } => Self::Down(steps),\n            DbCommands::Reset => Self::Reset,\n            DbCommands::Status => Self::Status,\n            #[cfg(debug_assertions)]\n            DbCommands::Entities => Self::Entities,\n            DbCommands::Truncate => Self::Truncate,\n            DbCommands::Seed {\n                reset,\n                from,\n                dump,\n                dump_tables,\n            } => Self::Seed {\n                reset,\n                from,\n                dump,\n                dump_tables,\n            },\n            DbCommands::Create => {\n                unreachable!(\"Create db should't handled in the global db commands\")\n            }\n            DbCommands::Schema => Self::Schema,\n        }\n    }\n}\n\n#[derive(clap::ValueEnum, Clone)]\npub enum DeploymentKind {\n    Docker,\n    Nginx,\n}\n\nimpl DeploymentKind {\n    #[cfg(debug_assertions)]\n    fn to_generator_component(&self, config: &Config) -> loco_gen::Component {\n        let kind = match self {\n            Self::Docker => {\n                let mut copy_paths = vec![];\n\n                if let Some(static_assets) = &config.server.middlewares.static_assets {\n                    let asset_folder =\n                        PathBuf::from(controller::views::engines::DEFAULT_ASSET_FOLDER);\n                    if asset_folder.exists() {\n                        copy_paths.push(asset_folder.clone());\n                    }\n                    if !static_assets.folder.path.starts_with(&asset_folder) {\n                        copy_paths.push(PathBuf::from(&static_assets.folder.path));\n                    }\n                    if !static_assets.fallback.starts_with(asset_folder) {\n                        copy_paths.push(PathBuf::from(&static_assets.fallback));\n                    }\n                }\n\n                let is_client_side_rendering =\n                    PathBuf::from(\"frontend\").join(\"package.json\").exists();\n\n                loco_gen::DeploymentKind::Docker {\n                    copy_paths,\n                    is_client_side_rendering,\n                }\n            }\n            Self::Nginx => loco_gen::DeploymentKind::Nginx {\n                host: config.server.host.clone(),\n                port: config.server.port,\n            },\n        };\n        loco_gen::Component::Deployment { kind }\n    }\n}\n\n#[cfg(any(feature = \"bg_redis\", feature = \"bg_pg\", feature = \"bg_sqlt\"))]\n#[derive(Subcommand)]\nenum JobsCommands {\n    /// Cancels jobs with the specified names, setting their status to\n    /// `cancelled`.\n    Cancel {\n        /// Names of jobs to cancel.\n        #[arg(long)]\n        name: String,\n    },\n    /// Deletes jobs that are either completed or cancelled.\n    Tidy {},\n    /// Deletes jobs based on their age in days.\n    Purge {\n        /// Deletes jobs with errors or cancelled, older than the specified\n        /// maximum age in days.\n        #[arg(long, default_value_t = 90)]\n        max_age: i64,\n        /// Limits the jobs being saved to those with specific criteria like\n        /// completed or queued.\n        #[arg(long, use_value_delimiter = true)]\n        status: Option<Vec<JobStatus>>,\n        /// Saves the details of jobs into a file before deleting them.\n        #[arg(long)]\n        dump: Option<PathBuf>,\n    },\n    /// Saves the details of all jobs to files in the specified folder.\n    Dump {\n        /// Limits the jobs being saved to those with specific criteria like\n        /// completed or queued.\n        #[arg(long, use_value_delimiter = true)]\n        status: Option<Vec<JobStatus>>,\n        /// Folder to save the job files (default: current directory).\n        #[arg(short, long, default_value = \".\")]\n        folder: PathBuf,\n    },\n    /// Imports jobs from a file.\n    Import {\n        /// Path to the file containing job details to import.\n        #[arg(short, long)]\n        file: PathBuf,\n    },\n    /// Change `processing` status to `queue`.\n    Requeue {\n        /// Change `processing` jobs older than the specified\n        /// maximum age in minutes.\n        #[arg(long, default_value_t = 0)]\n        from_age: i64,\n    },\n}\n\n/// Parse a single key-value pair\nfn parse_key_val<T, U>(\n    s: &str,\n) -> std::result::Result<(T, U), Box<dyn std::error::Error + Send + Sync>>\nwhere\n    T: std::str::FromStr,\n    T::Err: std::error::Error + Send + Sync + 'static,\n    U: std::str::FromStr,\n    U::Err: std::error::Error + Send + Sync + 'static,\n{\n    let pos = s\n        .find(':')\n        .ok_or_else(|| format!(\"invalid KEY=value: no `:` found in `{s}`\"))?;\n    Ok((s[..pos].parse()?, s[pos + 1..].parse()?))\n}\n\n#[cfg(feature = \"with-db\")]\n/// run playgroup code\n///\n/// # Errors\n///\n/// When could not create app context\npub async fn playground<H: Hooks>() -> crate::Result<AppContext> {\n    let cli = Playground::parse();\n    let environment: Environment = cli.environment.unwrap_or_else(resolve_from_env).into();\n\n    let config = H::load_config(&environment).await?;\n    let app_context = create_context::<H>(&environment, config).await?;\n\n    if !H::init_logger(&app_context)? {\n        logger::init::<H>(&app_context.config.logger)?;\n    }\n\n    Ok(app_context)\n}\n\n/// # Main CLI Function\n///\n/// The `main` function is the entry point for the command-line interface (CLI)\n/// of the application. It parses command-line arguments, interprets the\n/// specified commands, and performs corresponding actions. This function is\n/// generic over `H` and `M`, where `H` represents the application hooks and `M`\n/// represents the migrator trait for handling database migrations.\n///\n/// # Errors\n///\n/// Returns an any error indicating success or failure during the CLI execution.\n///\n/// # Example\n///\n/// ```rust,ignore\n/// use myapp::app::App;\n/// use loco_rs::cli;\n/// use migration::Migrator;\n///\n/// #[tokio::main]\n/// async fn main()  {\n///     cli::main::<App, Migrator>().await\n/// }\n/// ```\n#[cfg(feature = \"with-db\")]\n#[allow(clippy::too_many_lines)]\n#[allow(clippy::cognitive_complexity)]\npub async fn main<H: Hooks, M: MigratorTrait>() -> crate::Result<()> {\n    let cli: Cli = Cli::parse();\n    let environment: Environment = cli.environment.unwrap_or_else(resolve_from_env).into();\n\n    let config = H::load_config(&environment).await?;\n    let app_context = create_context::<H>(&environment, config).await?;\n\n    if !H::init_logger(&app_context)? {\n        logger::init::<H>(&app_context.config.logger)?;\n    }\n\n    let task_span = create_root_span(&environment);\n    let _guard = task_span.enter();\n\n    match cli.command {\n        Commands::Start {\n            worker,\n            server_and_worker,\n            all,\n            binding,\n            port,\n            no_banner,\n        } => {\n            let start_mode = worker.map_or(\n                if server_and_worker {\n                    StartMode::ServerAndWorker\n                } else if all {\n                    StartMode::All\n                } else {\n                    StartMode::ServerOnly\n                },\n                |tags| StartMode::WorkerOnly { tags },\n            );\n\n            let boot_result =\n                create_app::<H, M>(start_mode, &environment, app_context.config).await?;\n            let serve_params = ServeParams {\n                port: port.map_or(boot_result.app_context.config.server.port, |p| p),\n                binding: binding\n                    .unwrap_or_else(|| boot_result.app_context.config.server.binding.clone()),\n            };\n            start::<H>(boot_result, serve_params, no_banner).await?;\n        }\n        #[cfg(feature = \"with-db\")]\n        Commands::Db { command } => {\n            if matches!(command, DbCommands::Create) {\n                db::create(&app_context.config.database.uri).await?;\n            } else {\n                run_db::<H, M>(&app_context, command.into()).await?;\n            }\n        }\n        #[cfg(any(feature = \"bg_redis\", feature = \"bg_pg\", feature = \"bg_sqlt\"))]\n        Commands::Jobs { command } => {\n            handle_job_command::<H>(command, &environment, app_context.config).await?;\n        }\n        Commands::Routes {} => {\n            let app_context = create_context::<H>(&environment, app_context.config).await?;\n            show_list_endpoints::<H>(&app_context);\n        }\n        Commands::Middleware { show_config } => {\n            let app_context = create_context::<H>(&environment, app_context.config).await?;\n            let middlewares = list_middlewares::<H>(&app_context);\n            for middleware in middlewares.iter().filter(|m| m.enabled) {\n                println!(\n                    \"{:<22} {}\",\n                    middleware.id.bold(),\n                    if show_config {\n                        middleware.detail.as_str()\n                    } else {\n                        \"\"\n                    }\n                );\n            }\n            println!(\"\\n\");\n            for middleware in middlewares.iter().filter(|m| !m.enabled) {\n                println!(\"{:<22} (disabled)\", middleware.id.bold().dimmed(),);\n            }\n        }\n        Commands::Task { name, params } => {\n            let vars = task::Vars::from_cli_args(params);\n            let app_context = create_context::<H>(&environment, app_context.config).await?;\n            run_task::<H>(&app_context, name.as_ref(), &vars).await?;\n        }\n        Commands::Scheduler {\n            name,\n            config_path,\n            tag,\n            list,\n        } => {\n            let app_context = create_context::<H>(&environment, app_context.config).await?;\n            run_scheduler::<H>(&app_context, config_path.as_ref(), name, tag, list).await?;\n        }\n        #[cfg(debug_assertions)]\n        Commands::Generate { component } => {\n            handle_generate_command::<H>(component, &app_context.config)?;\n        }\n        Commands::Doctor {\n            config: config_arg,\n            production,\n        } => {\n            if config_arg {\n                println!(\"{}\", &app_context.config);\n                println!(\"Environment: {}\", &environment);\n            } else {\n                let mut should_exit = false;\n                for (_, check) in doctor::run_all::<H>(&app_context, production).await? {\n                    if !should_exit && !check.valid() {\n                        should_exit = true;\n                    }\n                    println!(\"{check}\");\n                }\n                if should_exit {\n                    exit(1);\n                }\n            }\n        }\n        Commands::Version {} => {\n            println!(\"{}\", H::app_version(),);\n        }\n\n        Commands::Watch {\n            worker,\n            server_and_worker,\n        } => {\n            // cargo-watch  -s 'cargo loco start'\n            let mut cmd_str = String::from(\"cargo loco start\");\n\n            if let Some(worker_tags) = worker {\n                if worker_tags.is_empty() {\n                    cmd_str.push_str(\" --worker\");\n                } else {\n                    write!(cmd_str, \" --worker={}\", worker_tags.join(\",\"))\n                        .expect(\"Failed to write to string\");\n                }\n            } else if server_and_worker {\n                cmd_str.push_str(\" --server-and-worker\");\n            }\n\n            cmd(\"cargo-watch\", &[\"-s\", &cmd_str]).run().map_err(|err| {\n                Error::Message(format!(\n                    \"failed to start with `cargo-watch`. Did you `cargo install \\\n                         cargo-watch`?. error details: `{err}`\",\n                ))\n            })?;\n        }\n    }\n    Ok(())\n}\n\n#[cfg(not(feature = \"with-db\"))]\npub async fn main<H: Hooks>() -> crate::Result<()> {\n    let cli = Cli::parse();\n    let environment: Environment = cli.environment.unwrap_or_else(resolve_from_env).into();\n\n    let config = H::load_config(&environment).await?;\n    let app_context = create_context::<H>(&environment, config).await?;\n\n    if !H::init_logger(&app_context)? {\n        logger::init::<H>(&app_context.config.logger)?;\n    }\n\n    let task_span = create_root_span(&environment);\n    let _guard = task_span.enter();\n\n    match cli.command {\n        Commands::Start {\n            worker,\n            server_and_worker,\n            all,\n            binding,\n            port,\n            no_banner,\n        } => {\n            let start_mode = worker.map_or(\n                if server_and_worker {\n                    StartMode::ServerAndWorker\n                } else if all {\n                    StartMode::All\n                } else {\n                    StartMode::ServerOnly\n                },\n                |tags| StartMode::WorkerOnly { tags },\n            );\n\n            let boot_result = create_app::<H>(start_mode, &environment, app_context.config).await?;\n            let serve_params = ServeParams {\n                port: port.map_or(boot_result.app_context.config.server.port, |p| p),\n                binding: binding.map_or(\n                    boot_result.app_context.config.server.binding.to_string(),\n                    |b| b,\n                ),\n            };\n            start::<H>(boot_result, serve_params, no_banner).await?;\n        }\n        Commands::Routes {} => show_list_endpoints::<H>(&app_context),\n        Commands::Middleware { show_config } => {\n            let middlewares = list_middlewares::<H>(&app_context);\n            for middleware in middlewares.iter().filter(|m| m.enabled) {\n                println!(\n                    \"{:<22} {}\",\n                    middleware.id.bold(),\n                    if show_config {\n                        middleware.detail.as_str()\n                    } else {\n                        \"\"\n                    }\n                );\n            }\n            println!(\"\\n\");\n            for middleware in middlewares.iter().filter(|m| !m.enabled) {\n                println!(\"{:<22} (disabled)\", middleware.id.bold().dimmed(),);\n            }\n        }\n        Commands::Task { name, params } => {\n            let vars = task::Vars::from_cli_args(params);\n            run_task::<H>(&app_context, name.as_ref(), &vars).await?;\n        }\n        #[cfg(any(feature = \"bg_redis\", feature = \"bg_pg\", feature = \"bg_sqlt\"))]\n        Commands::Jobs { command } => {\n            handle_job_command::<H>(command, &environment, app_context.config).await?\n        }\n        Commands::Scheduler {\n            name,\n            config_path,\n            tag,\n            list,\n        } => {\n            run_scheduler::<H>(&app_context, config_path.as_ref(), name, tag, list).await?;\n        }\n        #[cfg(debug_assertions)]\n        Commands::Generate { component } => {\n            handle_generate_command::<H>(component, &app_context.config)?;\n        }\n        Commands::Doctor {\n            config: config_arg,\n            production,\n        } => {\n            if config_arg {\n                println!(\"{}\", &app_context.config);\n                println!(\"Environment: {}\", &environment);\n            } else {\n                let mut should_exit = false;\n                for (_, check) in doctor::run_all::<H>(&app_context, production).await? {\n                    if !should_exit && !check.valid() {\n                        should_exit = true;\n                    }\n                    println!(\"{check}\");\n                }\n                if should_exit {\n                    exit(1);\n                }\n            }\n        }\n        Commands::Version {} => {\n            println!(\"{}\", H::app_version(),);\n        }\n        Commands::Watch {\n            worker,\n            server_and_worker,\n        } => {\n            // cargo-watch  -s 'cargo loco start'\n            let mut cmd_str = String::from(\"cargo loco start\");\n\n            if let Some(worker_tags) = worker {\n                if worker_tags.is_empty() {\n                    cmd_str.push_str(\" --worker\");\n                } else {\n                    write!(cmd_str, \" --worker={}\", worker_tags.join(\",\"))\n                        .expect(\"Failed to write to string\");\n                }\n            } else if server_and_worker {\n                cmd_str.push_str(\" --server-and-worker\");\n            }\n\n            cmd(\"cargo-watch\", &[\"-s\", &cmd_str]).run().map_err(|err| {\n                Error::Message(format!(\n                    \"failed to start with `cargo-watch`. Did you `cargo install \\\n                         cargo-watch`?. error details: `{err}`\",\n                ))\n            })?;\n        }\n    }\n    Ok(())\n}\n\n// Define route node structure with enhanced methods\n#[derive(Default)]\nstruct RouteNode {\n    children: BTreeMap<String, Self>,\n    endpoints: Vec<(String, String)>,\n}\n\nimpl RouteNode {\n    fn is_leaf(&self) -> bool {\n        self.endpoints.len() == 1 && self.children.is_empty()\n    }\n\n    fn is_collapsible(&self) -> bool {\n        self.endpoints.is_empty()\n            && self.children.len() == 1\n            && self.children.values().next().is_some_and(Self::is_leaf)\n    }\n\n    fn method(&self) -> &str {\n        self.endpoints\n            .first()\n            .map_or(\"\", |(method, _)| method.as_str())\n    }\n\n    fn print(&self, prefix: &str, segment: &str, is_last: bool, is_root: bool, current_path: &str) {\n        match (is_root, self.is_leaf(), self.is_collapsible()) {\n            // Root level special cases\n            (true, true, _) => {\n                Self::print_with_format(\n                    &format!(\"/{segment}\"),\n                    &color_method(self.method()),\n                    &Self::build_path(&[current_path, segment]),\n                );\n            }\n            (true, _, true) => {\n                let Some((child_segment, child_node)) = self.children.iter().next() else {\n                    return;\n                };\n                Self::print_with_format(\n                    &format!(\"/{segment}/{child_segment}\"),\n                    &color_method(child_node.method()),\n                    &Self::build_path(&[current_path, segment, child_segment]),\n                );\n            }\n\n            // Non root level special cases\n            (false, true, _) => {\n                let prefix_str = Self::format_prefix(prefix, is_last, true);\n\n                Self::print_with_format(\n                    &format!(\"{prefix_str}{segment}\"),\n                    &color_method(self.method()),\n                    &Self::build_path(&[current_path, segment]),\n                );\n            }\n            (false, _, true) => {\n                let prefix_str = Self::format_prefix(prefix, is_last, true);\n                let Some((child_segment, child_node)) = self.children.iter().next() else {\n                    return;\n                };\n                Self::print_with_format(\n                    &format!(\"{prefix_str}{segment}/{child_segment}\"),\n                    &color_method(child_node.method()),\n                    &Self::build_path(&[current_path, segment, child_segment]),\n                );\n            }\n\n            // Standard branch node handling\n            _ => {\n                if is_root {\n                    println!(\"/{segment}\");\n                } else if !segment.is_empty() {\n                    println!(\"{}{}\", Self::format_prefix(prefix, is_last, true), segment);\n                }\n\n                // Print endpoints and children\n                let next_prefix = Self::format_next_prefix(prefix, is_last);\n                self.print_endpoints(\n                    &next_prefix,\n                    self.children.is_empty(),\n                    &Self::build_path(&[current_path, segment]),\n                );\n                self.print_children(&next_prefix, &Self::build_path(&[current_path, segment]));\n            }\n        }\n    }\n\n    fn print_endpoints(&self, prefix: &str, is_last_group: bool, current_path: &str) {\n        for (i, (method, _)) in self.endpoints.iter().enumerate() {\n            let is_last_entry = i == self.endpoints.len() - 1 && is_last_group;\n            let marker = if is_last_entry { \"└─\" } else { \"├─\" };\n            Self::print_with_format(\n                &format!(\"{prefix}{marker}\"),\n                &color_method(method),\n                current_path,\n            );\n        }\n    }\n\n    fn print_children(&self, prefix: &str, current_path: &str) {\n        let children = self.children.iter().collect::<Vec<_>>();\n        for (i, (child_segment, child_node)) in children.iter().enumerate() {\n            let is_last_child = i == children.len() - 1;\n\n            if child_node.is_leaf() {\n                let marker = if is_last_child { \"└─\" } else { \"├─\" };\n                Self::print_with_format(\n                    &format!(\"{prefix}{marker} /{child_segment}\"),\n                    &color_method(child_node.method()),\n                    &Self::build_path(&[current_path, child_segment]),\n                );\n            } else {\n                child_node.print(prefix, child_segment, is_last_child, false, current_path);\n            }\n        }\n    }\n\n    fn format_prefix(prefix: &str, is_last: bool, with_slash: bool) -> String {\n        let marker = if is_last { \"└─\" } else { \"├─\" };\n        if with_slash {\n            format!(\"{prefix}{marker} /\")\n        } else {\n            format!(\"{prefix}{marker} \")\n        }\n    }\n\n    fn format_next_prefix(prefix: &str, is_last: bool) -> String {\n        if is_last {\n            format!(\"{prefix}   \")\n        } else {\n            format!(\"{prefix}│  \")\n        }\n    }\n\n    fn build_path(segments: &[&str]) -> String {\n        segments.iter().fold(String::new(), |mut acc, &segment| {\n            if !segment.is_empty() {\n                acc.push('/');\n                acc.push_str(segment);\n            }\n            acc.replace(\"//\", \"/\")\n        })\n    }\n\n    fn print_with_format(tree: &str, method: &str, full_path: &str) {\n        println!(\"{:<50} {}\", format!(\"{tree} {method}\"), full_path);\n    }\n}\n\nfn show_list_endpoints<H: Hooks>(ctx: &AppContext) {\n    // Get and sort routes\n    let mut routes = list_endpoints::<H>(ctx);\n    routes.sort_by(|a, b| {\n        let method_priority = |actions: &[_]| match actions\n            .first()\n            .map(ToString::to_string)\n            .unwrap_or_default()\n            .as_str()\n        {\n            \"GET\" => 0,\n            \"POST\" => 1,\n            \"PUT\" => 2,\n            \"PATCH\" => 3,\n            \"DELETE\" => 4,\n            _ => 5,\n        };\n        a.uri\n            .cmp(&b.uri)\n            .then(method_priority(&a.actions).cmp(&method_priority(&b.actions)))\n    });\n\n    // Build route tree\n    let mut route_tree = RouteNode::default();\n    for router in routes {\n        let path = router.uri.trim_start_matches('/');\n        let segments: Vec<&str> = path.split('/').collect();\n        if segments.is_empty() {\n            continue;\n        }\n\n        // Insert the route into the tree\n        let mut current_node = &mut route_tree;\n        for segment in &segments {\n            current_node = current_node\n                .children\n                .entry((*segment).to_string())\n                .or_default();\n        }\n\n        // Store the endpoint at this node\n        current_node.endpoints.push((\n            router\n                .actions\n                .iter()\n                .map(ToString::to_string)\n                .collect::<Vec<_>>()\n                .join(\",\"),\n            router.uri.clone(),\n        ));\n    }\n\n    // Print the route tree\n    for (i, (segment, node)) in route_tree.children.iter().enumerate() {\n        node.print(\"\", segment, i == route_tree.children.len() - 1, true, \"\");\n    }\n}\n\nfn color_method(method: &str) -> String {\n    match method {\n        \"GET\" => method.green().to_string(),\n        \"POST\" => method.blue().to_string(),\n        \"PUT\" => method.yellow().to_string(),\n        \"PATCH\" => method.magenta().to_string(),\n        \"DELETE\" => method.red().to_string(),\n        _ => method.to_string(),\n    }\n}\n\nfn create_root_span(environment: &Environment) -> tracing::Span {\n    tracing::span!(tracing::Level::DEBUG, \"app\", environment = %environment)\n}\n\n#[cfg(any(feature = \"bg_redis\", feature = \"bg_pg\", feature = \"bg_sqlt\"))]\nasync fn handle_job_command<H: Hooks>(\n    command: JobsCommands,\n    environment: &Environment,\n    config: Config,\n) -> crate::Result<()> {\n    let app_context = create_context::<H>(environment, config).await?;\n    let queue = app_context.queue_provider.unwrap_or_else(|| {\n        println!(\"queue not configured\");\n        exit(1);\n    });\n\n    match &command {\n        JobsCommands::Cancel { name } => queue.cancel_jobs(name).await,\n        JobsCommands::Tidy {} => {\n            queue\n                .clear_by_status(vec![JobStatus::Completed, JobStatus::Cancelled])\n                .await\n        }\n        JobsCommands::Purge {\n            max_age,\n            status,\n            dump,\n        } => {\n            let status = status.as_ref().map_or_else(\n                || {\n                    vec![\n                        JobStatus::Failed,\n                        JobStatus::Cancelled,\n                        JobStatus::Queued,\n                        JobStatus::Completed,\n                    ]\n                },\n                std::clone::Clone::clone,\n            );\n\n            if let Some(path) = dump {\n                let dump_path = queue\n                    .dump(path.as_path(), Some(&status), Some(*max_age))\n                    .await?;\n\n                println!(\"Jobs successfully dumped to: {}\", dump_path.display());\n            }\n\n            queue.clear_jobs_older_than(*max_age, &status).await\n        }\n        JobsCommands::Dump { status, folder } => {\n            let dump_path = queue.dump(folder.as_path(), status.as_ref(), None).await?;\n            println!(\"Jobs successfully dumped to: {}\", dump_path.display());\n            Ok(())\n        }\n        JobsCommands::Import { file } => queue.import(file.as_path()).await,\n        JobsCommands::Requeue { from_age } => queue.requeue(from_age).await,\n    }\n}\n\n#[cfg(debug_assertions)]\nfn handle_generate_command<H: Hooks>(\n    component: ComponentArg,\n    config: &Config,\n) -> crate::Result<()> {\n    use std::path::Path;\n    if let ComponentArg::Override {\n        template_path,\n        info,\n    } = component\n    {\n        match (template_path, info) {\n            // If no template path is provided, display the available templates,\n            // ignoring the `--info` flag.\n            (None, true | false) => {\n                let templates = loco_gen::template::collect();\n                println!(\"{}\", format_templates_as_tree(templates));\n            }\n            // If a template path is provided and `--info` is enabled,\n            // display the templates from the specified path.\n            (Some(path), true) => {\n                let templates = loco_gen::template::collect_files_path(Path::new(&path)).unwrap();\n                println!(\"{}\", format_templates_as_tree(templates));\n            }\n            // If a template path is provided and `--info` is disabled,\n            // copy the template to the default local template path.\n            (Some(path), false) => {\n                let copied_files = loco_gen::copy_template(\n                    Path::new(&path),\n                    Path::new(loco_gen::template::DEFAULT_LOCAL_TEMPLATE),\n                )?;\n                if copied_files.is_empty() {\n                    println!(\"{}\", \"No templates were found to copy.\".red());\n                } else {\n                    println!(\n                        \"{}\",\n                        \"The following templates were successfully copied:\".green()\n                    );\n                    for f in copied_files {\n                        println!(\" * {}\", f.display());\n                    }\n                }\n            }\n        }\n    } else {\n        let get_result = loco_gen::generate(\n            &loco_gen::new_generator(),\n            component.into_gen_component(config)?,\n            &loco_gen::AppInfo {\n                app_name: H::app_name().to_string(),\n            },\n        )?;\n        let messages = loco_gen::collect_messages(&get_result);\n        println!(\"{messages}\");\n    }\n    Ok(())\n}\n\n#[must_use]\npub fn format_templates_as_tree(paths: Vec<PathBuf>) -> String {\n    let mut categories: BTreeMap<String, BTreeMap<String, Vec<PathBuf>>> = BTreeMap::new();\n\n    for path in paths {\n        if let Some(parent) = path.parent() {\n            let parent_str = parent.to_string_lossy().to_string();\n            let mut components = parent_str.split('/');\n            if let Some(top_level) = components.next() {\n                let top_key = top_level.to_string();\n                let sub_key = components.next().unwrap_or(\"\").to_string();\n\n                categories\n                    .entry(top_key)\n                    .or_default()\n                    .entry(sub_key)\n                    .or_default()\n                    .push(path);\n            }\n        }\n    }\n\n    let mut output = \"Available templates and directories to copy:\".to_string();\n    let _ = writeln!(output);\n    let _ = writeln!(output);\n\n    for (top_level, sub_categories) in &categories {\n        let _ = writeln!(output, \"{}\", top_level.clone().yellow());\n\n        for (sub_category, paths) in sub_categories {\n            if !sub_category.is_empty() {\n                let _ = writeln!(output, \"{}\", format!(\" └── {sub_category}\").yellow());\n            }\n\n            for path in paths {\n                let _ = writeln!(\n                    output,\n                    \"   └── {}\",\n                    path.file_name().unwrap_or_default().to_string_lossy()\n                );\n            }\n        }\n    }\n\n    let _ = writeln!(output);\n    let _ = writeln!(output);\n    let _ = writeln!(output, \"{}\", \"Usage Examples:\".bold().green());\n    let _ = writeln!(output);\n    let _ = writeln!(output, \"{}\", \"Override a Specific File:\".bold());\n\n    let _ = writeln!(\n        output,\n        \" * cargo loco generate override {}\",\n        \"scaffold/api/controller.t\".yellow()\n    );\n    let _ = writeln!(\n        output,\n        \" * cargo loco generate override {}\",\n        \"migration/add_columns.t\".yellow()\n    );\n    let _ = writeln!(output);\n    let _ = writeln!(output, \"{}\", \"Override All Files in a Folder:\".bold());\n    let _ = writeln!(\n        output,\n        \" * cargo loco generate override {}\",\n        \"scaffold/htmx\".yellow()\n    );\n\n    let _ = writeln!(\n        output,\n        \" * cargo loco generate override {}\",\n        \"task\".yellow()\n    );\n    let _ = writeln!(output);\n    let _ = writeln!(output, \"{}\", \"Override All templates:\".bold());\n    let _ = writeln!(output, \" * cargo loco generate override {}\", \".\".yellow());\n\n    output\n}\n"
  },
  {
    "path": "src/config.rs",
    "content": "//! # Configuration Management\n//!\n//! This module defines the configuration structures and functions to manage and\n//! load configuration settings for the application.\n\n/***\n=============\nCONTRIBUTORS:\n=============\n\nHere's a check list when adding configuration values:\n\n* Add the new configuration piece\n* Document each field with the appropriate rustdoc comment\n* Go to `starters/`, evaluate which starter needs a configuration update, and update as needed.\n  apply a YAML comment above the new field or section with explanation and possible values.\n\nNotes:\n* Configuration is feature-dependent: with and without database\n* Configuration is \"stage\" dependent: development, test, production\n* We typically provide best practice values for development and test, but by-design we do not provide default values for production\n\n***/\nuse std::{\n    collections::BTreeMap,\n    fs,\n    path::{Path, PathBuf},\n    sync::OnceLock,\n};\n\nuse serde::{Deserialize, Serialize};\nuse serde_json::json;\nuse tracing::info;\n\nuse crate::{controller::middleware, environment::Environment, logger, scheduler, Error, Result};\n\nstatic DEFAULT_FOLDER: OnceLock<PathBuf> = OnceLock::new();\n\nfn get_default_folder() -> &'static PathBuf {\n    DEFAULT_FOLDER.get_or_init(|| PathBuf::from(\"config\"))\n}\n/// Main application configuration structure.\n///\n/// This struct encapsulates various configuration settings. The configuration\n/// can be customized through YAML files for different environments.\n#[derive(Debug, Clone, Deserialize, Serialize)]\npub struct Config {\n    pub logger: Logger,\n    pub server: Server,\n    #[cfg(feature = \"with-db\")]\n    pub database: Database,\n    #[serde(default)]\n    pub cache: CacheConfig,\n    pub queue: Option<QueueConfig>,\n    pub auth: Option<Auth>,\n    #[serde(default)]\n    pub workers: Workers,\n    pub mailer: Option<Mailer>,\n    pub initializers: Option<Initializers>,\n\n    /// Custom app settings\n    ///\n    /// Example:\n    /// ```yaml\n    /// settings:\n    ///   allow_list:\n    ///     - google.com\n    ///     - apple.com\n    /// ```\n    /// And then optionally deserialize it to your own `Settings` type by\n    /// accessing `ctx.config.settings`.\n    #[serde(default)]\n    pub settings: Option<serde_json::Value>,\n\n    pub scheduler: Option<scheduler::Config>,\n}\n\n/// Logger configuration\n///\n/// The Loco logging stack is built on `tracing`, using a carefuly\n/// crafted stack of filters and subscribers. We filter out noise,\n/// apply a log level across your app, and sort out back traces for\n/// a great developer experience.\n///\n/// Example (development):\n/// ```yaml\n/// # config/development.yaml\n/// logger:\n///   enable: true\n///   pretty_backtrace: true\n///   level: debug\n///   format: compact\n/// ```\n#[derive(Debug, Clone, Deserialize, Serialize, Default)]\npub struct Logger {\n    /// Enable log write to stdout\n    pub enable: bool,\n\n    /// Enable nice display of backtraces, in development this should be on.\n    /// Turn it off in performance sensitive production deployments.\n    #[serde(default)]\n    pub pretty_backtrace: bool,\n\n    /// Set the logger level.\n    ///\n    /// * options: `trace` | `debug` | `info` | `warn` | `error`\n    pub level: logger::LogLevel,\n\n    /// Set the logger format.\n    ///\n    /// * options: `compact` | `pretty` | `json`\n    pub format: logger::Format,\n\n    /// Override our custom tracing filter.\n    ///\n    /// Set this to your own filter if you want to see traces from internal\n    /// libraries. See more [here](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives)\n    pub override_filter: Option<String>,\n\n    /// Set this if you want to write log to file\n    pub file_appender: Option<LoggerFileAppender>,\n}\n\n#[derive(Debug, Clone, Deserialize, Serialize, Default)]\npub struct LoggerFileAppender {\n    /// Enable logger file appender\n    pub enable: bool,\n\n    /// Enable write log to file non-blocking\n    #[serde(default)]\n    pub non_blocking: bool,\n\n    /// Set the logger file appender level.\n    ///\n    /// * options: `trace` | `debug` | `info` | `warn` | `error`\n    pub level: logger::LogLevel,\n\n    /// Set the logger file appender format.\n    ///\n    /// * options: `compact` | `pretty` | `json`\n    pub format: logger::Format,\n\n    /// Set the logger file appender rotation.\n    pub rotation: logger::Rotation,\n\n    /// Set the logger file appender dir\n    ///\n    /// default is `./logs`\n    pub dir: Option<String>,\n\n    /// Set log filename prefix\n    pub filename_prefix: Option<String>,\n\n    /// Set log filename suffix\n    pub filename_suffix: Option<String>,\n\n    /// Set the logger file appender keep max log files.\n    pub max_log_files: usize,\n}\n\n/// Database configuration\n///\n/// Configures the [SeaORM](https://www.sea-ql.org/SeaORM/) connection and pool, as well as Loco's additional DB\n/// management utils such as `auto_migrate`, `truncate` and `recreate`.\n///\n/// Example (development):\n/// ```yaml\n/// # config/development.yaml\n/// database:\n///   uri: {{ get_env(name=\"DATABASE_URL\", default=\"...\") }}\n///   enable_logging: true\n///   connect_timeout: 500\n///   idle_timeout: 500\n///   min_connections: 1\n///   max_connections: 1\n///   auto_migrate: true\n///   dangerously_truncate: false\n///   dangerously_recreate: false\n/// ```\n#[derive(Debug, Clone, Deserialize, Serialize)]\n#[allow(clippy::struct_excessive_bools)]\npub struct Database {\n    /// The URI for connecting to the database. For example:\n    /// * Postgres: `postgres://root:12341234@localhost:5432/myapp_development`\n    /// * Sqlite: `sqlite://db.sqlite?mode=rwc`\n    pub uri: String,\n\n    /// Enable `SQLx` statement logging\n    pub enable_logging: bool,\n\n    /// Minimum number of connections for a pool\n    pub min_connections: u32,\n\n    /// Maximum number of connections for a pool\n    pub max_connections: u32,\n\n    /// Set the timeout duration when acquiring a connection\n    pub connect_timeout: u64,\n\n    /// Set the idle duration before closing a connection\n    pub idle_timeout: u64,\n\n    /// Set the timeout for acquiring a connection\n    pub acquire_timeout: Option<u64>,\n\n    /// Run migration up when application loads. It is recommended to turn it on\n    /// in development. In production keep it off, and explicitly migrate your\n    /// database every time you need.\n    #[serde(default)]\n    pub auto_migrate: bool,\n\n    /// Truncate database when application loads. It will delete data from your\n    /// tables. Commonly used in `test`.\n    #[serde(default)]\n    pub dangerously_truncate: bool,\n\n    /// Recreate schema when application loads. Use it when you want to reset\n    /// your database *and* structure (drop), this also deletes all of the data.\n    /// Useful when you're just sketching out your project and trying out\n    /// various things in development.\n    #[serde(default)]\n    pub dangerously_recreate: bool,\n\n    // Execute query after initializing the DB\n    /// for e.g. this can be used to confiure PRAGMAs for `SQLite` where you can pass all values as a string.\n    /// Default values are:\n    ///\n    /// PRAGMA `foreign_keys` = ON;\n    ///\n    /// PRAGMA `journal_mode` = WAL;\n    ///\n    /// PRAGMA `synchronous` = NORMAL;\n    ///\n    /// PRAGMA `mmap_size` = 134217728;\n    ///\n    /// PRAGMA `journal_size_limit` = 67108864;\n    ///\n    /// PRAGMA `cache_size` = 2000;\n    ///\n    /// PRAGMA `busy_timeout` = 5000;\n    pub run_on_start: Option<String>,\n}\n\n/// Cache configurations for the application\n#[derive(Debug, Clone, Default, Deserialize, Serialize)]\n#[serde(tag = \"kind\")]\npub enum CacheConfig {\n    #[cfg(feature = \"cache_inmem\")]\n    /// In-memory cache\n    InMem(InMemCacheConfig),\n    #[cfg(feature = \"cache_redis\")]\n    /// Redis cache\n    Redis(RedisCacheConfig),\n    /// Null cache\n    #[default]\n    Null,\n}\n\n#[derive(Debug, Clone, Deserialize, Serialize)]\npub struct InMemCacheConfig {\n    #[serde(default = \"cache_in_mem_max_capacity\")]\n    pub max_capacity: u64,\n}\n\nfn cache_in_mem_max_capacity() -> u64 {\n    32 * 1024 * 1024\n}\n\n#[derive(Debug, Clone, Deserialize, Serialize)]\npub struct RedisCacheConfig {\n    pub uri: String,\n    /// Sets the maximum number of connections managed by the pool.\n    pub max_size: u32,\n}\n\n#[derive(Debug, Clone, Deserialize, Serialize)]\n#[serde(tag = \"kind\")]\npub enum QueueConfig {\n    /// Redis queue\n    Redis(RedisQueueConfig),\n    /// Postgres queue\n    Postgres(PostgresQueueConfig),\n    /// Sqlite queue\n    Sqlite(SqliteQueueConfig),\n}\n\n#[derive(Debug, Clone, Deserialize, Serialize)]\npub struct RedisQueueConfig {\n    pub uri: String,\n    #[serde(default)]\n    pub dangerously_flush: bool,\n\n    /// Custom queue names declaration. Useful to model priority queues.\n    /// First queue in list is more important.\n    pub queues: Option<Vec<String>>,\n\n    #[serde(default = \"num_workers\")]\n    pub num_workers: u32,\n}\n\n#[derive(Debug, Clone, Deserialize, Serialize)]\npub struct PostgresQueueConfig {\n    pub uri: String,\n\n    #[serde(default)]\n    pub dangerously_flush: bool,\n\n    #[serde(default)]\n    pub enable_logging: bool,\n\n    #[serde(default = \"db_max_conn\")]\n    pub max_connections: u32,\n\n    #[serde(default = \"db_min_conn\")]\n    pub min_connections: u32,\n\n    #[serde(default = \"db_connect_timeout\")]\n    pub connect_timeout: u64,\n\n    #[serde(default = \"db_idle_timeout\")]\n    pub idle_timeout: u64,\n\n    #[serde(default = \"pgq_poll_interval\")]\n    pub poll_interval_sec: u32,\n\n    #[serde(default = \"num_workers\")]\n    pub num_workers: u32,\n}\n\n#[derive(Debug, Clone, Deserialize, Serialize)]\npub struct SqliteQueueConfig {\n    pub uri: String,\n\n    #[serde(default)]\n    pub dangerously_flush: bool,\n\n    #[serde(default)]\n    pub enable_logging: bool,\n\n    #[serde(default = \"db_max_conn\")]\n    pub max_connections: u32,\n\n    #[serde(default = \"db_min_conn\")]\n    pub min_connections: u32,\n\n    #[serde(default = \"db_connect_timeout\")]\n    pub connect_timeout: u64,\n\n    #[serde(default = \"db_idle_timeout\")]\n    pub idle_timeout: u64,\n\n    #[serde(default = \"sqlt_poll_interval\")]\n    pub poll_interval_sec: u32,\n\n    #[serde(default = \"num_workers\")]\n    pub num_workers: u32,\n}\n\nfn db_min_conn() -> u32 {\n    1\n}\n\nfn db_max_conn() -> u32 {\n    20\n}\n\nfn db_connect_timeout() -> u64 {\n    500\n}\n\nfn db_idle_timeout() -> u64 {\n    500\n}\n\nfn pgq_poll_interval() -> u32 {\n    1\n}\n\nfn sqlt_poll_interval() -> u32 {\n    1\n}\n\nfn num_workers() -> u32 {\n    2\n}\n\n/// User authentication configuration.\n///\n/// Example (development):\n/// ```yaml\n/// # config/development.yaml\n/// auth:\n///   jwt:\n///     secret: <your secret>\n///     expiration: 604800 # 7 days\n/// ```\n#[derive(Debug, Clone, Deserialize, Serialize)]\npub struct Auth {\n    /// JWT authentication config\n    pub jwt: Option<JWT>,\n}\n\n/// JWT configuration structure.\n#[derive(Debug, Clone, Deserialize, Serialize)]\npub struct JWT {\n    /// The location(s) where JWT tokens are expected to be found during\n    /// authentication. Can be a single location or an array of locations.\n    pub location: Option<JWTLocationConfig>,\n    /// The secret key For JWT token\n    pub secret: String,\n    /// The expiration time for authentication tokens\n    pub expiration: u64,\n}\n\n/// Defines the authentication mechanism for middleware.\n///\n/// This enum represents various ways to authenticate using JSON Web Tokens\n/// (JWT) within middleware.\n#[derive(Debug, Clone, Deserialize, Serialize)]\n#[serde(tag = \"from\")]\npub enum JWTLocation {\n    /// Authenticate using a Bearer token.\n    Bearer,\n    /// Authenticate using a token passed as a query parameter.\n    Query { name: String },\n    /// Authenticate using a token stored in a cookie.\n    Cookie { name: String },\n}\n\n/// Configuration for JWT location(s) - supports both single location and multiple locations\n#[derive(Debug, Clone, Deserialize, Serialize)]\n#[serde(untagged)]\npub enum JWTLocationConfig {\n    /// Single authentication location\n    Single(JWTLocation),\n    /// Multiple authentication locations (tried in order)\n    Multiple(Vec<JWTLocation>),\n}\n\n/// Server configuration structure.\n///\n/// Example (development):\n/// ```yaml\n/// # config/development.yaml\n/// server:\n///   port: {{ get_env(name=\"NODE_PORT\", default=5150) }}\n///   host: http://localhost\n///   middlewares:\n///     limit_payload:\n///       enable: true\n///       body_limit: 5mb\n///     logger:\n///       enable: true\n///     catch_panic:\n///       enable: true\n///     timeout_request:\n///       enable: true\n///       timeout: 5000\n///     compression:\n///       enable: true\n///     cors:\n///       enable: true\n/// ```\n#[derive(Debug, Clone, Deserialize, Serialize)]\npub struct Server {\n    /// The address on which the server should listen on for incoming\n    /// connections.\n    #[serde(default = \"default_binding\")]\n    pub binding: String,\n    /// The port on which the server should listen for incoming connections.\n    pub port: i32,\n    /// The webserver host\n    pub host: String,\n    /// Identify via the `Server` header\n    pub ident: Option<String>,\n    /// Middleware configurations for the server, including payload limits,\n    /// logging, and error handling.\n    #[serde(default)]\n    pub middlewares: middleware::Config,\n}\n\nfn default_binding() -> String {\n    \"localhost\".to_string()\n}\n\nimpl Server {\n    #[must_use]\n    pub fn full_url(&self) -> String {\n        format!(\"{}:{}\", self.host, self.port)\n    }\n}\n/// Background worker configuration\n/// Example (development):\n/// ```yaml\n/// # config/development.yaml\n/// workers:\n///   mode: BackgroundQueue\n/// ```\n#[derive(Debug, Clone, Deserialize, Serialize, Default)]\npub struct Workers {\n    /// Toggle between different worker modes\n    pub mode: WorkerMode,\n}\n\n/// Worker mode configuration\n#[derive(Clone, Default, Serialize, Deserialize, Debug, PartialEq, Eq)]\npub enum WorkerMode {\n    /// Workers operate asynchronously in the background, processing queued\n    /// tasks. **Requires a Redis connection**.\n    #[default]\n    BackgroundQueue,\n    /// Workers operate in the foreground in the same process and block until\n    /// tasks are completed.\n    ForegroundBlocking,\n    /// Workers operate asynchronously in the background, processing tasks with\n    /// async capabilities in the same process.\n    BackgroundAsync,\n}\n\n/// Mailer configuration\n///\n/// Example (development), to capture mails with something like [mailcrab](https://github.com/tweedegolf/mailcrab):\n/// ```yaml\n/// # config/development.yaml\n/// mailer:\n///   smtp:\n///     enable: true\n///     host: localhost\n///     port: 1025\n///     secure: false\n/// ```\n#[derive(Debug, Clone, Deserialize, Serialize)]\npub struct Mailer {\n    pub smtp: Option<SmtpMailer>,\n\n    #[serde(default)]\n    pub stub: bool,\n}\n\n/// Initializers configuration\n///\n/// Example (development): To configure settings for oauth2 or custom view\n/// engine\n/// ```yaml\n/// # config/development.yaml\n/// initializers:\n///  oauth2:\n///   authorization_code: # Authorization code grant type\n///     - client_identifier: google # Identifier for the `OAuth2` provider.\n///       Replace 'google' with your provider's name if different, must be\n///       unique within the oauth2 config. ... # other fields\npub type Initializers = BTreeMap<String, serde_json::Value>;\n\n/// SMTP mailer configuration structure.\n#[derive(Debug, Clone, Deserialize, Serialize)]\npub struct SmtpMailer {\n    pub enable: bool,\n    /// SMTP host. for example: localhost, smtp.gmail.com etc.\n    pub host: String,\n    /// SMTP port/\n    pub port: u16,\n    /// Enable TLS\n    pub secure: bool,\n    /// Auth SMTP server\n    pub auth: Option<MailerAuth>,\n    /// Optional EHLO client ID instead of hostname\n    pub hello_name: Option<String>,\n}\n\n/// Authentication details for the mailer\n#[derive(Debug, Clone, Deserialize, Serialize)]\npub struct MailerAuth {\n    /// User\n    pub user: String,\n    /// Password\n    pub password: String,\n}\n\nimpl Config {\n    /// Creates a new configuration instance based on the specified environment.\n    ///\n    /// # Errors\n    ///\n    /// Returns error when could not convert the give path to\n    /// [`Config`] struct.\n    ///\n    /// # Example\n    ///\n    /// ```rust\n    /// use loco_rs::{\n    ///     config::Config,\n    ///     environment::Environment,\n    /// };\n    ///\n    /// #[tokio::main]\n    /// async fn load(environment: &Environment) -> Config {\n    ///     Config::new(environment).expect(\"configuration loading\")\n    /// }\n    pub fn new(env: &Environment) -> Result<Self> {\n        let config = Self::from_folder(env, get_default_folder().as_path())?;\n        Ok(config)\n    }\n\n    /// Loads configuration settings from a folder for the specified\n    /// environment.\n    ///\n    /// # Errors\n    /// Returns error when could not convert the give path to\n    /// [`Config`] struct.\n    ///\n    /// # Example\n    ///\n    /// ```rust\n    /// use loco_rs::{\n    ///     config::Config,\n    ///     environment::Environment,\n    /// };\n    /// use std::path::PathBuf;\n    ///\n    /// #[tokio::main]\n    /// async fn load(environment: &Environment) -> Config{\n    ///     Config::from_folder(environment, &PathBuf::from(\"config\")).expect(\"configuration loading\")\n    /// }\n    pub fn from_folder(env: &Environment, path: &Path) -> Result<Self> {\n        // by order of precedence\n        let files = [\n            path.join(format!(\"{env}.local.yaml\")),\n            path.join(format!(\"{env}.yaml\")),\n        ];\n\n        let selected_path = files.iter().find(|p| p.exists()).ok_or_else(|| {\n            Error::Message(format!(\n                \"no configuration file found in folder: {}\",\n                path.display()\n            ))\n        })?;\n\n        info!(selected_path =? selected_path, \"loading environment from\");\n\n        let content = fs::read_to_string(selected_path)?;\n        let rendered = crate::tera::render_string(&content, &json!({}))?;\n\n        serde_yaml::from_str(&rendered)\n            .map_err(|err| Error::YAMLFile(err, selected_path.to_string_lossy().to_string()))\n    }\n\n    /// Get a reference to the JWT configuration.\n    ///\n    /// # Errors\n    /// return an error when jwt token not configured\n    pub fn get_jwt_config(&self) -> Result<&JWT> {\n        self.auth\n            .as_ref()\n            .and_then(|auth| auth.jwt.as_ref())\n            .map_or_else(\n                || Err(Error::Any(\"no JWT config found\".to_string().into())),\n                Ok,\n            )\n    }\n}\n\nimpl std::fmt::Display for Config {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        let content = serde_yaml::to_string(self).unwrap_or_default();\n        write!(f, \"{content}\")\n    }\n}\n"
  },
  {
    "path": "src/controller/app_routes.rs",
    "content": "//! This module defines the [`AppRoutes`] struct that is responsible for\n//! configuring routes in an Axum application. It allows you to define route\n//! prefixes, add routes, and configure middlewares for the application.\n\nuse std::{fmt, sync::OnceLock};\n\nuse axum::Router as AXRouter;\nuse regex::Regex;\n\nuse crate::{\n    app::{AppContext, Hooks},\n    controller::{middleware::MiddlewareLayer, routes::Routes},\n    Result,\n};\n\nstatic NORMALIZE_URL: OnceLock<Regex> = OnceLock::new();\n\nfn get_normalize_url() -> &'static Regex {\n    NORMALIZE_URL.get_or_init(|| Regex::new(r\"/+\").unwrap())\n}\n\n/// Represents the routes of the application.\n#[derive(Clone)]\npub struct AppRoutes {\n    prefix: Option<String>,\n    routes: Vec<Routes>,\n}\n\n#[derive(Debug)]\npub struct ListRoutes {\n    pub uri: String,\n    pub actions: Vec<axum::http::Method>,\n    pub method: axum::routing::MethodRouter<AppContext>,\n}\n\nimpl fmt::Display for ListRoutes {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        let actions_str = self\n            .actions\n            .iter()\n            .map(ToString::to_string)\n            .collect::<Vec<_>>()\n            .join(\",\");\n\n        write!(f, \"[{}] {}\", actions_str, self.uri)\n    }\n}\n\nimpl AppRoutes {\n    /// Create a new instance with the default routes.\n    #[must_use]\n    pub fn with_default_routes() -> Self {\n        Self::empty().add_routes(vec![super::monitoring::routes()])\n    }\n\n    /// Create an empty instance.\n    #[must_use]\n    pub fn empty() -> Self {\n        Self {\n            prefix: None,\n            routes: vec![],\n        }\n    }\n\n    #[must_use]\n    pub fn collect(&self) -> Vec<ListRoutes> {\n        self.get_routes()\n            .iter()\n            .flat_map(|controller| {\n                let uri_parts = controller\n                    .prefix\n                    .as_ref()\n                    .map_or_else(Vec::new, |prefix| vec![prefix.clone()]);\n\n                controller.handlers.iter().map(move |handler| {\n                    let mut parts = uri_parts.clone();\n                    parts.push(handler.uri.clone());\n                    let joined_parts = parts.join(\"/\");\n\n                    let normalized = get_normalize_url().replace_all(&joined_parts, \"/\");\n                    let mut uri = if normalized == \"/\" {\n                        normalized.to_string()\n                    } else {\n                        normalized\n                            .strip_suffix('/')\n                            .map_or_else(|| normalized.to_string(), ToString::to_string)\n                    };\n\n                    if !uri.starts_with('/') {\n                        uri.insert(0, '/');\n                    }\n\n                    ListRoutes {\n                        uri,\n                        actions: handler.actions.clone(),\n                        method: handler.method.clone(),\n                    }\n                })\n            })\n            .collect()\n    }\n\n    /// Get the prefix of the routes.\n    #[must_use]\n    pub fn get_prefix(&self) -> Option<&String> {\n        self.prefix.as_ref()\n    }\n\n    /// Get the routes.\n    #[must_use]\n    pub fn get_routes(&self) -> &[Routes] {\n        self.routes.as_ref()\n    }\n\n    /// Set a prefix for the routes. this prefix will be a prefix for all the\n    /// routes.\n    ///\n    /// # Example\n    ///\n    /// In the following example you are adding api as a prefix for all routes\n    ///\n    /// ```rust\n    /// use loco_rs::controller::AppRoutes;\n    ///\n    /// AppRoutes::with_default_routes().prefix(\"api\");\n    /// ```\n    #[must_use]\n    pub fn prefix(mut self, prefix: &str) -> Self {\n        let mut prefix = prefix.to_owned();\n        if !prefix.ends_with('/') {\n            prefix.push('/');\n        }\n        if !prefix.starts_with('/') {\n            prefix.insert(0, '/');\n        }\n\n        self.prefix = Some(prefix);\n\n        self\n    }\n\n    /// Set a nested prefix for the routes. This prefix will be appended to any existing prefix.\n    ///\n    /// # Example\n    ///\n    /// In the following example, you are adding `api` as a prefix and then nesting `v1` within it:\n    ///\n    /// ```rust\n    /// use loco_rs::controller::AppRoutes;\n    /// use loco_rs::tests_cfg::*;\n    ///\n    /// let app_routes = AppRoutes::with_default_routes()\n    ///      .prefix(\"api\")\n    ///      .add_route(controllers::auth::routes())\n    ///      .nest_prefix(\"v1\")\n    ///      .add_route(controllers::home::routes());\n    ///\n    /// // This will result in routes like `/api/auth` and `/api/v1/home`\n    /// ```\n    #[must_use]\n    pub fn nest_prefix(mut self, prefix: &str) -> Self {\n        let prefix = self.prefix.as_ref().map_or_else(\n            || prefix.to_owned(),\n            |old_prefix| format!(\"{old_prefix}{prefix}\"),\n        );\n        self = self.prefix(&prefix);\n\n        self\n    }\n\n    /// Set a nested route with a prefix. This route will be added with the specified prefix.\n    /// The prefix will only be applied to the routes given in this function.\n    ///\n    /// # Example\n    ///\n    /// In the following example, you are adding `api` as a prefix and then nesting a route within it:\n    ///\n    /// ```rust\n    /// use axum::routing::get;\n    /// use loco_rs::controller::{AppRoutes, Routes};\n    ///\n    /// let route = Routes::new().add(\"/notes\", get(|| async { \"notes\" }));\n    /// let app_routes = AppRoutes::with_default_routes()\n    ///     .prefix(\"api\")\n    ///     .nest_route(\"v1\", route);\n    ///\n    /// // This will result in routes with the prefix `/api/v1/notes`\n    /// ```\n    #[must_use]\n    pub fn nest_route(mut self, prefix: &str, route: Routes) -> Self {\n        let old_prefix = self.prefix.clone();\n        self = self.nest_prefix(prefix);\n        self = self.add_route(route);\n        self.prefix = old_prefix;\n\n        self\n    }\n\n    /// Set multiple nested routes with a prefix. These routes will be added with the specified prefix.\n    /// The prefix will only be applied to the routes given in this function.\n    ///\n    /// # Example\n    ///\n    /// In the following example, you are adding `api` as a prefix and then nesting multiple routes within it:\n    ///\n    /// ```rust\n    /// use axum::routing::get;\n    /// use loco_rs::controller::{AppRoutes, Routes};\n    ///\n    /// let routes = vec![\n    ///     Routes::new().add(\"/notes\", get(|| async { \"notes\" })),\n    ///     Routes::new().add(\"/users\", get(|| async { \"users\" })),\n    /// ];\n    /// let app_routes = AppRoutes::with_default_routes()\n    ///     .prefix(\"api\")\n    ///     .nest_routes(\"v1\", routes);\n    ///\n    /// // This will result in routes with the prefix `/api/v1/notes` and `/api/v1/users`\n    /// ```\n    #[must_use]\n    pub fn nest_routes(mut self, prefix: &str, routes: Vec<Routes>) -> Self {\n        let old_prefix = self.prefix.clone();\n        self = self.nest_prefix(prefix);\n        self = self.add_routes(routes);\n        self.prefix = old_prefix;\n\n        self\n    }\n\n    /// Add a single route.\n    #[must_use]\n    pub fn add_route(mut self, mut route: Routes) -> Self {\n        let routes_prefix = {\n            if let Some(mut prefix) = self.prefix.clone() {\n                let routes_prefix = route.prefix.clone().unwrap_or_default();\n\n                prefix.push_str(routes_prefix.as_str());\n                Some(prefix)\n            } else {\n                route.prefix.clone()\n            }\n        };\n\n        if let Some(prefix) = routes_prefix {\n            route = route.prefix(prefix.as_str());\n        }\n\n        self.routes.push(route);\n\n        self\n    }\n\n    /// Add multiple routes.\n    #[must_use]\n    pub fn add_routes(mut self, mounts: Vec<Routes>) -> Self {\n        for mount in mounts {\n            self = self.add_route(mount);\n        }\n\n        self\n    }\n\n    #[must_use]\n    pub fn middlewares<H: Hooks>(&self, ctx: &AppContext) -> Vec<Box<dyn MiddlewareLayer>> {\n        H::middlewares(ctx)\n            .into_iter()\n            .filter(|m| m.is_enabled())\n            .collect::<Vec<Box<dyn MiddlewareLayer>>>()\n    }\n\n    /// Add the routes to an existing Axum Router, and set a list of middlewares\n    /// that configure in the [`config::Config`]\n    ///\n    /// # Errors\n    /// Return an [`Result`] when could not convert the router setup to\n    /// [`axum::Router`].\n    #[allow(clippy::cognitive_complexity)]\n    pub fn to_router<H: Hooks>(\n        &self,\n        ctx: AppContext,\n        mut app: AXRouter<AppContext>,\n    ) -> Result<AXRouter> {\n        // IMPORTANT: middleware ordering in this function is opposite to what you\n        // intuitively may think. when using `app.layer` to add individual middleware,\n        // the LAST middleware is the FIRST to meet the outside world (a user request\n        // starting), or \"LIFO\" order.\n        // We build the \"onion\" from the inside (start of this function),\n        // outwards (end of this function). This is why routes is first in coding order\n        // here (the core of the onion), and request ID is amongst the last\n        // (because every request is assigned with a unique ID, which starts its\n        // \"life\").\n        //\n        // NOTE: when using ServiceBuilder#layer the order is FIRST to LAST (but we\n        // don't use ServiceBuilder because it requires too complex generic typing for\n        // this function). ServiceBuilder is recommended to save compile times, but that\n        // may be a thing of the past as we don't notice any issues with compile times\n        // using the router directly, and ServiceBuilder has been reported to give\n        // issues in compile times itself (https://github.com/rust-lang/crates.io/pull/7443).\n        //\n        for router in self.collect() {\n            tracing::info!(\"{}\", router.to_string());\n            app = app.route(&router.uri, router.method);\n        }\n\n        let middlewares = self.middlewares::<H>(&ctx);\n        for mid in middlewares {\n            app = mid.apply(app)?;\n            tracing::info!(name = mid.name(), \"+middleware\");\n        }\n        let router = app.with_state(ctx);\n        Ok(router)\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use crate::{prelude::*, tests_cfg};\n    use axum::http::Method;\n    use insta::assert_debug_snapshot;\n    use rstest::rstest;\n    use std::vec;\n    use tower::ServiceExt;\n\n    async fn action() -> Result<Response> {\n        format::json(\"loco\")\n    }\n\n    #[test]\n    fn can_load_app_route_from_default() {\n        let routes = AppRoutes::with_default_routes().collect();\n\n        for route in routes {\n            assert_debug_snapshot!(\n                format!(\"[{}]\", route.uri.replace('/', \"[slash]\")),\n                format!(\"{:?} {}\", route.actions, route.uri)\n            );\n        }\n    }\n\n    #[test]\n    fn can_load_empty_app_routes() {\n        assert_eq!(AppRoutes::empty().collect().len(), 0);\n    }\n\n    #[test]\n    fn can_load_routes() {\n        let router_without_prefix = Routes::new().add(\"/\", get(action));\n        let normalizer = Routes::new()\n            .prefix(\"/normalizer\")\n            .add(\"no-slash\", get(action))\n            .add(\"/\", post(action))\n            .add(\"//loco///rs//\", delete(action))\n            .add(\"//////multiple-start\", head(action))\n            .add(\"multiple-end/////\", trace(action));\n\n        let app_router = AppRoutes::empty()\n            .add_route(router_without_prefix)\n            .add_route(normalizer)\n            .add_routes(vec![\n                Routes::new().add(\"multiple1\", put(action)),\n                Routes::new().add(\"multiple2\", options(action)),\n                Routes::new().add(\"multiple3\", patch(action)),\n            ]);\n\n        for route in app_router.collect() {\n            assert_debug_snapshot!(\n                format!(\"[{}]\", route.uri.replace('/', \"[slash]\")),\n                format!(\"{:?} {}\", route.actions, route.uri)\n            );\n        }\n    }\n\n    #[test]\n    fn can_load_routes_with_root_prefix() {\n        let router_without_prefix = Routes::new()\n            .add(\"/loco\", get(action))\n            .add(\"loco-rs\", get(action));\n\n        let app_router = AppRoutes::empty()\n            .prefix(\"api\")\n            .add_route(router_without_prefix);\n\n        for route in app_router.collect() {\n            assert_debug_snapshot!(\n                format!(\"[{}]\", route.uri.replace('/', \"[slash]\")),\n                format!(\"{:?} {}\", route.actions, route.uri)\n            );\n        }\n    }\n\n    #[test]\n    fn can_nest_prefix() {\n        let app_router = AppRoutes::empty().prefix(\"api\").nest_prefix(\"v1\");\n\n        assert_eq!(app_router.get_prefix().unwrap(), \"/api/v1/\");\n    }\n\n    #[test]\n    fn can_nest_route() {\n        let route = Routes::new().add(\"/notes\", get(action));\n        let app_router = AppRoutes::empty().prefix(\"api\").nest_route(\"v1\", route);\n\n        let routes = app_router.collect();\n        assert_eq!(routes.len(), 1);\n        assert_eq!(routes[0].uri, \"/api/v1/notes\");\n    }\n\n    #[test]\n    fn can_nest_routes() {\n        let routes = vec![\n            Routes::new().add(\"/notes\", get(action)),\n            Routes::new().add(\"/users\", get(action)),\n        ];\n        let app_router = AppRoutes::empty().prefix(\"api\").nest_routes(\"v1\", routes);\n\n        for route in app_router.collect() {\n            assert_debug_snapshot!(\n                format!(\"[{}]\", route.uri.replace('/', \"[slash]\")),\n                format!(\"{:?} {}\", route.actions, route.uri)\n            );\n        }\n    }\n\n    #[rstest]\n    #[case(Method::GET, get(action))]\n    #[case(Method::POST, post(action))]\n    #[case(Method::DELETE, delete(action))]\n    #[case(Method::HEAD, head(action))]\n    #[case(Method::OPTIONS, options(action))]\n    #[case(Method::PATCH, patch(action))]\n    #[case(Method::POST, post(action))]\n    #[case(Method::PUT, put(action))]\n    #[case(Method::TRACE, trace(action))]\n    #[tokio::test]\n    async fn can_request_method(\n        #[case] http_method: Method,\n        #[case] method: axum::routing::MethodRouter<AppContext>,\n    ) {\n        let router_without_prefix = Routes::new().add(\"/loco\", method);\n\n        let app_router = AppRoutes::empty().add_route(router_without_prefix);\n\n        let ctx = tests_cfg::app::get_app_context().await;\n        let router = app_router\n            .to_router::<tests_cfg::db::AppHook>(ctx, axum::Router::new())\n            .unwrap();\n\n        let req = axum::http::Request::builder()\n            .uri(\"/loco\")\n            .method(http_method)\n            .body(axum::body::Body::empty())\n            .unwrap();\n\n        let response = router.oneshot(req).await.unwrap();\n        assert!(response.status().is_success());\n    }\n}\n"
  },
  {
    "path": "src/controller/backtrace.rs",
    "content": "use std::sync::OnceLock;\n\nuse regex::Regex;\n\nuse crate::{Error, Result};\n\nstatic NAME_BLOCKLIST: OnceLock<Vec<Regex>> = OnceLock::new();\nstatic FILE_BLOCKLIST: OnceLock<Vec<Regex>> = OnceLock::new();\n\nfn get_name_blocklist() -> &'static Vec<Regex> {\n    NAME_BLOCKLIST.get_or_init(|| {\n        [\n            \"^___rust_try\",\n            \"^__pthread\",\n            \"^__clone\",\n            \"^<loco_rs::errors::Error as\",\n            \"^loco_rs::errors::Error::bt\",\n            /*\n            \"^<?tokio\",\n            \"^<?future\",\n            \"^<?tower\",\n            \"^<?futures\",\n            \"^<?hyper\",\n            \"^<?axum\",\n            \"<F as futures_core\",\n            \"^<F as axum::\",\n            \"^<?std::panic\",\n            \"^<?core::\",\n            \"^rust_panic\",\n            \"^rayon\",\n            \"^rust_begin_unwind\",\n            \"^start_thread\",\n            \"^call_once\",\n            \"^catch_unwind\",\n            */\n        ]\n        .iter()\n        .map(|s| Regex::new(s).unwrap())\n        .collect::<Vec<_>>()\n    })\n}\n\nfn get_file_blocklist() -> &'static Vec<Regex> {\n    FILE_BLOCKLIST.get_or_init(|| {\n        [\n            \"axum-.*$\",\n            \"tower-.*$\",\n            \"hyper-.*$\",\n            \"tokio-.*$\",\n            \"futures-.*$\",\n            \"^/rustc\",\n        ]\n        .iter()\n        .map(|s| Regex::new(s).unwrap())\n        .collect::<Vec<_>>()\n    })\n}\n\npub fn print_backtrace(bt: &std::backtrace::Backtrace) -> Result<()> {\n    backtrace_printer::print_backtrace(\n        &mut std::io::stdout(),\n        bt,\n        get_name_blocklist(),\n        get_file_blocklist(),\n    )\n    .map_err(Error::msg)\n}\n"
  },
  {
    "path": "src/controller/describe.rs",
    "content": "use std::sync::OnceLock;\n\nuse axum::{http, routing::MethodRouter};\nuse regex::Regex;\n\nuse crate::app::AppContext;\n\nstatic DESCRIBE_METHOD_ACTION: OnceLock<Regex> = OnceLock::new();\n\nfn get_describe_method_action() -> &'static Regex {\n    DESCRIBE_METHOD_ACTION.get_or_init(|| Regex::new(r\"\\b(\\w+):\\s*(BoxedHandler|Route)\\b\").unwrap())\n}\n\n/// Extract the allow list method actions from [`MethodRouter`].\n///\n/// Currently axum not exposed the action type of the router. for hold extra\n/// information about routers we need to convert the `method` to string and\n/// capture the details\npub fn method_action(method: &MethodRouter<AppContext>) -> Vec<http::Method> {\n    let method_str = format!(\"{method:?}\");\n\n    get_describe_method_action()\n        .captures(&method_str)\n        .and_then(|captures| captures.get(1).map(|m| m.as_str().to_lowercase()))\n        .and_then(|method_name| match method_name.as_str() {\n            \"get\" => Some(http::Method::GET),\n            \"post\" => Some(http::Method::POST),\n            \"put\" => Some(http::Method::PUT),\n            \"delete\" => Some(http::Method::DELETE),\n            \"head\" => Some(http::Method::HEAD),\n            \"options\" => Some(http::Method::OPTIONS),\n            \"connect\" => Some(http::Method::CONNECT),\n            \"patch\" => Some(http::Method::PATCH),\n            \"trace\" => Some(http::Method::TRACE),\n            _ => {\n                tracing::info!(\"Unknown method: {}\", method_name);\n                None\n            }\n        })\n        .into_iter()\n        .collect::<Vec<_>>()\n}\n"
  },
  {
    "path": "src/controller/extractor/auth.rs",
    "content": "//! Axum middleware for validating token header\n//!\n//! # Example:\n//!\n//! ```\n//! use loco_rs::prelude::*;\n//! use serde::Serialize;\n//! use axum::extract::State;\n//! use loco_rs::controller::extractor::auth;\n//!\n//! #[derive(Serialize)]\n//! pub struct TestResponse {\n//!     pub pid: String,\n//! }\n//!\n//! async fn current(\n//!     auth: auth::JWT,\n//!     State(ctx): State<AppContext>,\n//! ) -> Result<Response> {\n//!     format::json(TestResponse{ pid: auth.claims.pid})\n//! }\n//! ```\nuse std::collections::HashMap;\n\nuse axum::{\n    extract::{FromRef, FromRequestParts, Query},\n    http::{request::Parts, HeaderMap},\n};\nuse axum_extra::extract::cookie;\nuse serde::{Deserialize, Serialize};\nuse tracing;\n\nuse crate::{app::AppContext, auth, config::JWT as JWTConfig, errors::Error, Result as LocoResult};\n\n#[cfg(feature = \"with-db\")]\nuse crate::model::{Authenticable, ModelError};\n\n// ---------------------------------------\n//\n// JWT Auth extractor\n//\n// ---------------------------------------\n\n// Define constants for token prefix and authorization header\nconst TOKEN_PREFIX: &str = \"Bearer \";\nconst AUTH_HEADER: &str = \"authorization\";\n\n// Define a struct to represent user authentication information serialized\n// to/from JSON\n#[cfg(feature = \"with-db\")]\n#[derive(Debug, Deserialize, Serialize)]\npub struct JWTWithUser<T: Authenticable> {\n    pub claims: auth::jwt::UserClaims,\n    pub user: T,\n}\n\n// Implement the FromRequestParts trait for the Auth struct\n#[cfg(feature = \"with-db\")]\nimpl<S, T> FromRequestParts<S> for JWTWithUser<T>\nwhere\n    AppContext: FromRef<S>,\n    S: Send + Sync,\n    T: Authenticable,\n{\n    type Rejection = Error;\n\n    async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Error> {\n        let ctx: AppContext = AppContext::from_ref(state);\n\n        let token = extract_token(get_jwt_from_config(&ctx)?, parts)?;\n\n        let jwt_secret = ctx.config.get_jwt_config()?;\n\n        match auth::jwt::JWT::new(&jwt_secret.secret).validate(&token) {\n            Ok(claims) => {\n                let user = T::find_by_claims_key(&ctx.db, &claims.claims.pid)\n                    .await\n                    .map_err(|e| match e {\n                        ModelError::EntityNotFound => Error::Unauthorized(\"not found\".to_string()),\n                        ModelError::DbErr(db_err) => {\n                            tracing::error!(\"Database error during authentication: {}\", db_err);\n                            Error::InternalServerError\n                        }\n                        _ => {\n                            tracing::error!(\"Authentication error: {}\", e);\n                            Error::Unauthorized(\"could not authorize\".to_string())\n                        }\n                    })?;\n                Ok(Self {\n                    claims: claims.claims,\n                    user,\n                })\n            }\n            Err(err) => {\n                tracing::error!(\"JWT validation error: {}\", err);\n                Err(Error::Unauthorized(\"token is not valid\".to_string()))\n            }\n        }\n    }\n}\n\n// Define a struct to represent user authentication information serialized\n// to/from JSON\n#[derive(Debug, Deserialize, Serialize)]\npub struct JWT {\n    pub claims: auth::jwt::UserClaims,\n}\n\n// Implement the FromRequestParts trait for the Auth struct\nimpl<S> FromRequestParts<S> for JWT\nwhere\n    AppContext: FromRef<S>,\n    S: Send + Sync,\n{\n    type Rejection = Error;\n\n    async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Error> {\n        extract_jwt_from_request_parts(parts, state)\n    }\n}\n\n/// extract a [JWT] token from request parts, using a non-mutable reference to the [Parts]\n///\n/// # Errors\n/// Return an error when JWT token not configured or when the token is not valid\npub fn extract_jwt_from_request_parts<S>(parts: &Parts, state: &S) -> Result<JWT, Error>\nwhere\n    AppContext: FromRef<S>,\n    S: Send + Sync,\n{\n    let ctx: AppContext = AppContext::from_ref(state); // change to ctx\n\n    let token = extract_token(get_jwt_from_config(&ctx)?, parts)?;\n\n    let jwt_secret = ctx.config.get_jwt_config()?;\n\n    match auth::jwt::JWT::new(&jwt_secret.secret).validate(&token) {\n        Ok(claims) => Ok(JWT {\n            claims: claims.claims,\n        }),\n        Err(err) => {\n            tracing::error!(\"JWT validation error: {}\", err);\n            Err(Error::Unauthorized(\"token is not valid\".to_string()))\n        }\n    }\n}\n\n/// extract JWT token from context configuration\n///\n/// # Errors\n/// Return an error when JWT token not configured\npub fn get_jwt_from_config(ctx: &AppContext) -> LocoResult<&JWTConfig> {\n    ctx.config\n        .auth\n        .as_ref()\n        .ok_or_else(|| Error::string(\"auth not configured\"))?\n        .jwt\n        .as_ref()\n        .ok_or_else(|| Error::string(\"JWT token not configured\"))\n}\n/// extract token from the configured jwt location settings\n///\n/// # Errors\n///\n/// Returns an error when the token cannot be extracted from any of the configured locations,\n/// such as missing headers, invalid formats, or inaccessible request data.\npub fn extract_token(jwt_config: &JWTConfig, parts: &Parts) -> LocoResult<String> {\n    let locations = get_jwt_locations(jwt_config.location.as_ref());\n\n    for location in &locations {\n        if let Ok(token) = extract_token_from_location(location, parts) {\n            return Ok(token);\n        }\n    }\n\n    // If we get here, none of the locations worked\n    Err(Error::Unauthorized(\"Token not found in any of the configured JWT locations. Please check your auth.jwt.location configuration.\".to_string()))\n}\n\n/// Get the list of JWT locations to try, with Bearer as default\nfn get_jwt_locations(\n    config: Option<&crate::config::JWTLocationConfig>,\n) -> Vec<&crate::config::JWTLocation> {\n    match config {\n        Some(crate::config::JWTLocationConfig::Single(location)) => vec![location],\n        Some(crate::config::JWTLocationConfig::Multiple(locations)) => locations.iter().collect(),\n        None => vec![&crate::config::JWTLocation::Bearer],\n    }\n}\n\n/// Extract token from a specific location\nfn extract_token_from_location(\n    location: &crate::config::JWTLocation,\n    parts: &Parts,\n) -> LocoResult<String> {\n    match location {\n        crate::config::JWTLocation::Query { name } => extract_token_from_query(name, parts),\n        crate::config::JWTLocation::Cookie { name } => extract_token_from_cookie(name, parts),\n        crate::config::JWTLocation::Bearer => extract_token_from_header(&parts.headers),\n    }\n}\n\n/// Function to extract a token from the authorization header\n///\n/// # Errors\n///\n/// When token is not valid or not found\npub fn extract_token_from_header(headers: &HeaderMap) -> LocoResult<String> {\n    let token = headers\n        .get(AUTH_HEADER)\n        .ok_or_else(|| Error::Unauthorized(format!(\"header {AUTH_HEADER} token not found\")))?\n        .to_str()\n        .map_err(|err| Error::Unauthorized(err.to_string()))?\n        .strip_prefix(TOKEN_PREFIX)\n        .ok_or_else(|| Error::Unauthorized(format!(\"error strip {AUTH_HEADER} value\")))?;\n\n    Ok(token.to_string())\n}\n\n/// Extract a token value from cookie\n///\n/// # Errors\n/// when token value from cookie is not found\npub fn extract_token_from_cookie(name: &str, parts: &Parts) -> LocoResult<String> {\n    // LogoResult\n    let jar: cookie::CookieJar = cookie::CookieJar::from_headers(&parts.headers);\n    Ok(jar\n        .get(name)\n        .ok_or(Error::Unauthorized(\"token is not found\".to_string()))?\n        .to_string()\n        .strip_prefix(&format!(\"{name}=\"))\n        .ok_or_else(|| Error::Unauthorized(\"error strip value\".to_string()))?\n        .to_string())\n}\n/// Extract a token value from query\n///\n/// # Errors\n/// when token value from cookie is not found\npub fn extract_token_from_query(name: &str, parts: &Parts) -> LocoResult<String> {\n    // LogoResult\n    let parameters: Query<HashMap<String, String>> =\n        Query::try_from_uri(&parts.uri).map_err(|err| Error::Unauthorized(err.to_string()))?;\n    parameters\n        .get(name)\n        .cloned()\n        .ok_or_else(|| Error::Unauthorized(format!(\"`{name}` query parameter not found\")))\n}\n\n// ---------------------------------------\n//\n// API Token Auth / Extractor\n//\n// ---------------------------------------\n#[cfg(feature = \"with-db\")]\n#[derive(Debug, Deserialize, Serialize)]\n// Represents the data structure for the API token.\npub struct ApiToken<T: Authenticable> {\n    pub user: T,\n}\n\n// Implementing the `FromRequestParts` trait for `ApiToken` to enable extracting\n// it from the request.\n#[cfg(feature = \"with-db\")]\nimpl<S, T> FromRequestParts<S> for ApiToken<T>\nwhere\n    AppContext: FromRef<S>,\n    S: Send + Sync,\n    T: Authenticable,\n{\n    type Rejection = Error;\n\n    // Extracts `ApiToken` from the request parts.\n    async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Error> {\n        // Extract API key from the request header.\n        let api_key = extract_token_from_header(&parts.headers)?;\n\n        // Convert the state reference to the application context.\n        let state: AppContext = AppContext::from_ref(state);\n\n        // Retrieve user information based on the API key from the database.\n        let user = T::find_by_api_key(&state.db, &api_key)\n            .await\n            .map_err(|e| match e {\n                ModelError::EntityNotFound => Error::Unauthorized(\"not found\".to_string()),\n                ModelError::DbErr(db_err) => {\n                    tracing::error!(\"Database error during API key authentication: {}\", db_err);\n                    Error::InternalServerError\n                }\n                _ => {\n                    tracing::error!(\"API key authentication error: {}\", e);\n                    Error::Unauthorized(\"could not authorize\".to_string())\n                }\n            })?;\n\n        Ok(Self { user })\n    }\n}\n\n#[cfg(test)]\nmod tests {\n\n    use axum::http::{HeaderMap, HeaderValue};\n\n    use super::*;\n    use crate::config;\n\n    #[test]\n    fn test_extract_token_from_header_success() {\n        let mut headers = HeaderMap::new();\n        headers.insert(\n            AUTH_HEADER,\n            HeaderValue::from_str(\"Bearer valid_token_123\").unwrap(),\n        );\n\n        let result = extract_token_from_header(&headers);\n        assert!(result.is_ok());\n        assert_eq!(result.unwrap(), \"valid_token_123\");\n    }\n\n    #[test]\n    fn test_extract_token_from_header_with_spaces() {\n        let mut headers = HeaderMap::new();\n        headers.insert(\n            AUTH_HEADER,\n            HeaderValue::from_str(\"Bearer  token_with_spaces  \").unwrap(),\n        );\n\n        let result = extract_token_from_header(&headers);\n        assert!(result.is_ok());\n        assert_eq!(result.unwrap(), \" token_with_spaces  \");\n    }\n\n    #[test]\n    fn test_extract_token_from_header_special_chars() {\n        let mut headers = HeaderMap::new();\n        headers.insert(\n            AUTH_HEADER,\n            HeaderValue::from_str(\"Bearer token-with_special.chars\").unwrap(),\n        );\n\n        let result = extract_token_from_header(&headers);\n        assert!(result.is_ok());\n        assert_eq!(result.unwrap(), \"token-with_special.chars\");\n    }\n\n    #[test]\n    fn test_extract_token_from_header_missing_header() {\n        let headers = HeaderMap::new();\n        let result = extract_token_from_header(&headers);\n        assert!(result.is_err());\n        assert!(result\n            .unwrap_err()\n            .to_string()\n            .contains(\"authorization token not found\"));\n    }\n\n    #[test]\n    fn test_extract_token_from_header_missing_bearer_prefix() {\n        let mut headers = HeaderMap::new();\n        headers.insert(\n            AUTH_HEADER,\n            HeaderValue::from_str(\"InvalidPrefix token\").unwrap(),\n        );\n\n        let result = extract_token_from_header(&headers);\n        assert!(result.is_err());\n        assert!(result\n            .unwrap_err()\n            .to_string()\n            .contains(\"error strip authorization value\"));\n    }\n\n    #[test]\n    fn test_extract_token_from_header_empty_value() {\n        let mut headers = HeaderMap::new();\n        headers.insert(AUTH_HEADER, HeaderValue::from_str(\"Bearer\").unwrap());\n\n        let result = extract_token_from_header(&headers);\n        assert!(result.is_err());\n        assert!(result\n            .unwrap_err()\n            .to_string()\n            .contains(\"error strip authorization value\"));\n    }\n\n    #[test]\n    fn test_extract_token_from_cookie_success() {\n        let request = axum::http::Request::builder()\n            .header(\"Cookie\", \"test_cookie=cookie_value_123\")\n            .body(())\n            .unwrap();\n        let (parts, ()) = request.into_parts();\n\n        let result = extract_token_from_cookie(\"test_cookie\", &parts);\n        assert!(result.is_ok());\n        assert_eq!(result.unwrap(), \"cookie_value_123\");\n    }\n\n    #[test]\n    fn test_extract_token_from_cookie_special_chars() {\n        let request = axum::http::Request::builder()\n            .header(\"Cookie\", \"auth_token=token-with.special_chars\")\n            .body(())\n            .unwrap();\n        let (parts, ()) = request.into_parts();\n\n        let result = extract_token_from_cookie(\"auth_token\", &parts);\n        assert!(result.is_ok());\n        assert_eq!(result.unwrap(), \"token-with.special_chars\");\n    }\n\n    #[test]\n    fn test_extract_token_from_cookie_missing_cookie() {\n        let request = axum::http::Request::builder().body(()).unwrap();\n        let (parts, ()) = request.into_parts();\n\n        let result = extract_token_from_cookie(\"nonexistent\", &parts);\n        assert!(result.is_err());\n        assert!(result\n            .unwrap_err()\n            .to_string()\n            .contains(\"token is not found\"));\n    }\n\n    #[test]\n    fn test_extract_token_from_cookie_not_found() {\n        let request = axum::http::Request::builder()\n            .header(\"Cookie\", \"different_cookie=value\")\n            .body(())\n            .unwrap();\n        let (parts, ()) = request.into_parts();\n\n        let result = extract_token_from_cookie(\"nonexistent\", &parts);\n        assert!(result.is_err());\n        assert!(result\n            .unwrap_err()\n            .to_string()\n            .contains(\"token is not found\"));\n    }\n\n    #[test]\n    fn test_extract_token_from_query_success() {\n        let request = axum::http::Request::builder()\n            .uri(\"https://example.com?token=query_value_123&other=param\")\n            .body(())\n            .unwrap();\n        let (parts, ()) = request.into_parts();\n\n        let result = extract_token_from_query(\"token\", &parts);\n        assert!(result.is_ok());\n        assert_eq!(result.unwrap(), \"query_value_123\");\n    }\n\n    #[test]\n    fn test_extract_token_from_query_special_chars() {\n        let request = axum::http::Request::builder()\n            .uri(\"https://example.com?auth_token=token-with.special_chars\")\n            .body(())\n            .unwrap();\n        let (parts, ()) = request.into_parts();\n\n        let result = extract_token_from_query(\"auth_token\", &parts);\n        assert!(result.is_ok());\n        assert_eq!(result.unwrap(), \"token-with.special_chars\");\n    }\n\n    #[test]\n    fn test_extract_token_from_query_missing_param() {\n        let request = axum::http::Request::builder()\n            .uri(\"https://example.com?other=param\")\n            .body(())\n            .unwrap();\n        let (parts, ()) = request.into_parts();\n\n        let result = extract_token_from_query(\"nonexistent_param\", &parts);\n        assert!(result.is_err());\n        assert!(result\n            .unwrap_err()\n            .to_string()\n            .contains(\"query parameter not found\"));\n    }\n\n    #[test]\n    fn test_extract_token_from_query_invalid_uri() {\n        let request = axum::http::Request::builder()\n            .uri(\"not-a-valid-uri\")\n            .body(())\n            .unwrap();\n        let (parts, ()) = request.into_parts();\n\n        let result = extract_token_from_query(\"nonexistent_param\", &parts);\n        assert!(result.is_err());\n    }\n\n    #[test]\n    fn test_get_jwt_locations_default() {\n        let jwt_config = JWTConfig {\n            location: None,\n            secret: String::new(),\n            expiration: 1,\n        };\n\n        let locations = get_jwt_locations(jwt_config.location.as_ref());\n        assert_eq!(locations.len(), 1);\n        assert!(matches!(locations[0], config::JWTLocation::Bearer));\n    }\n\n    #[test]\n    fn test_get_jwt_locations_single_bearer() {\n        let jwt_config = JWTConfig {\n            location: Some(config::JWTLocationConfig::Single(\n                config::JWTLocation::Bearer,\n            )),\n            secret: String::new(),\n            expiration: 1,\n        };\n\n        let locations = get_jwt_locations(jwt_config.location.as_ref());\n        assert_eq!(locations.len(), 1);\n        assert!(matches!(locations[0], config::JWTLocation::Bearer));\n    }\n\n    #[test]\n    fn test_get_jwt_locations_single_cookie() {\n        let jwt_config = JWTConfig {\n            location: Some(config::JWTLocationConfig::Single(\n                config::JWTLocation::Cookie {\n                    name: \"auth_token\".to_string(),\n                },\n            )),\n            secret: String::new(),\n            expiration: 1,\n        };\n\n        let locations = get_jwt_locations(jwt_config.location.as_ref());\n        assert_eq!(locations.len(), 1);\n        assert!(matches!(locations[0], config::JWTLocation::Cookie { .. }));\n    }\n\n    #[test]\n    fn test_get_jwt_locations_single_query() {\n        let jwt_config = JWTConfig {\n            location: Some(config::JWTLocationConfig::Single(\n                config::JWTLocation::Query {\n                    name: \"token\".to_string(),\n                },\n            )),\n            secret: String::new(),\n            expiration: 1,\n        };\n\n        let locations = get_jwt_locations(jwt_config.location.as_ref());\n        assert_eq!(locations.len(), 1);\n        assert!(matches!(locations[0], config::JWTLocation::Query { .. }));\n    }\n\n    #[test]\n    fn test_get_jwt_locations_multiple() {\n        let jwt_config = JWTConfig {\n            location: Some(config::JWTLocationConfig::Multiple(vec![\n                config::JWTLocation::Cookie {\n                    name: \"auth\".to_string(),\n                },\n                config::JWTLocation::Query {\n                    name: \"token\".to_string(),\n                },\n                config::JWTLocation::Bearer,\n            ])),\n            secret: String::new(),\n            expiration: 1,\n        };\n\n        let locations = get_jwt_locations(jwt_config.location.as_ref());\n        assert_eq!(locations.len(), 3);\n        assert!(matches!(locations[0], config::JWTLocation::Cookie { .. }));\n        assert!(matches!(locations[1], config::JWTLocation::Query { .. }));\n        assert!(matches!(locations[2], config::JWTLocation::Bearer));\n    }\n\n    #[test]\n    fn test_extract_token_from_location_bearer() {\n        let request = axum::http::Request::builder()\n            .uri(\"https://example.com?token=query_value\")\n            .header(AUTH_HEADER, format!(\"{TOKEN_PREFIX} bearer_value\"))\n            .header(\"Cookie\", \"auth_token=cookie_value\")\n            .body(())\n            .unwrap();\n        let (parts, ()) = request.into_parts();\n\n        let result = extract_token_from_location(&config::JWTLocation::Bearer, &parts);\n        assert!(result.is_ok());\n        assert_eq!(result.unwrap(), \" bearer_value\");\n    }\n\n    #[test]\n    fn test_extract_token_from_location_cookie() {\n        let request = axum::http::Request::builder()\n            .uri(\"https://example.com?token=query_value\")\n            .header(AUTH_HEADER, format!(\"{TOKEN_PREFIX} bearer_value\"))\n            .header(\"Cookie\", \"auth_token=cookie_value\")\n            .body(())\n            .unwrap();\n        let (parts, ()) = request.into_parts();\n\n        let result = extract_token_from_location(\n            &config::JWTLocation::Cookie {\n                name: \"auth_token\".to_string(),\n            },\n            &parts,\n        );\n        assert!(result.is_ok());\n        assert_eq!(result.unwrap(), \"cookie_value\");\n    }\n\n    #[test]\n    fn test_extract_token_from_location_query() {\n        let request = axum::http::Request::builder()\n            .uri(\"https://example.com?token=query_value\")\n            .header(AUTH_HEADER, format!(\"{TOKEN_PREFIX} bearer_value\"))\n            .header(\"Cookie\", \"auth_token=cookie_value\")\n            .body(())\n            .unwrap();\n        let (parts, ()) = request.into_parts();\n\n        let result = extract_token_from_location(\n            &config::JWTLocation::Query {\n                name: \"token\".to_string(),\n            },\n            &parts,\n        );\n        assert!(result.is_ok());\n        assert_eq!(result.unwrap(), \"query_value\");\n    }\n\n    #[test]\n    fn test_extract_token_single_location_success() {\n        let jwt_config = JWTConfig {\n            location: Some(config::JWTLocationConfig::Single(\n                config::JWTLocation::Bearer,\n            )),\n            secret: String::new(),\n            expiration: 1,\n        };\n\n        let request = axum::http::Request::builder()\n            .uri(\"https://example.com\")\n            .header(AUTH_HEADER, format!(\"{TOKEN_PREFIX} valid_token\"))\n            .body(())\n            .unwrap();\n        let (parts, ()) = request.into_parts();\n\n        let result = extract_token(&jwt_config, &parts);\n        assert!(result.is_ok());\n        assert_eq!(result.unwrap(), \" valid_token\");\n    }\n\n    #[test]\n    fn test_extract_token_multiple_locations_fallback() {\n        let jwt_config = JWTConfig {\n            location: Some(config::JWTLocationConfig::Multiple(vec![\n                config::JWTLocation::Cookie {\n                    name: \"nonexistent\".to_string(),\n                },\n                config::JWTLocation::Query {\n                    name: \"token\".to_string(),\n                },\n            ])),\n            secret: String::new(),\n            expiration: 1,\n        };\n\n        let request = axum::http::Request::builder()\n            .uri(\"https://example.com?token=fallback_token\")\n            .body(())\n            .unwrap();\n        let (parts, ()) = request.into_parts();\n\n        let result = extract_token(&jwt_config, &parts);\n        assert!(result.is_ok());\n        assert_eq!(result.unwrap(), \"fallback_token\");\n    }\n\n    #[test]\n    fn test_extract_token_all_locations_fail() {\n        let jwt_config = JWTConfig {\n            location: Some(config::JWTLocationConfig::Multiple(vec![\n                config::JWTLocation::Cookie {\n                    name: \"nonexistent\".to_string(),\n                },\n                config::JWTLocation::Query {\n                    name: \"missing\".to_string(),\n                },\n            ])),\n            secret: String::new(),\n            expiration: 1,\n        };\n\n        let request = axum::http::Request::builder()\n            .uri(\"https://example.com\")\n            .body(())\n            .unwrap();\n        let (parts, ()) = request.into_parts();\n\n        let result = extract_token(&jwt_config, &parts);\n        assert!(result.is_err());\n        assert!(result\n            .unwrap_err()\n            .to_string()\n            .contains(\"Token not found in any of the configured JWT locations\"));\n    }\n\n    #[test]\n    fn test_extract_from_default() {\n        let jwt_config = JWTConfig {\n            location: None,\n            secret: String::new(),\n            expiration: 1,\n        };\n\n        let request = axum::http::Request::builder()\n            .uri(\"https://loco.rs\")\n            .header(AUTH_HEADER, format!(\"{TOKEN_PREFIX} bearer_token_value\"))\n            .header(\"Cookie\", \"loco_cookie_key=cookie_token_value\")\n            .body(())\n            .unwrap();\n        let (parts, ()) = request.into_parts();\n\n        let result = extract_token(&jwt_config, &parts);\n        assert!(result.is_ok());\n        assert_eq!(result.unwrap(), \" bearer_token_value\");\n    }\n\n    #[test]\n    fn test_extract_from_bearer() {\n        let jwt_config = JWTConfig {\n            location: Some(config::JWTLocationConfig::Single(\n                config::JWTLocation::Bearer,\n            )),\n            secret: String::new(),\n            expiration: 1,\n        };\n\n        let request = axum::http::Request::builder()\n            .uri(\"loco.rs\")\n            .header(AUTH_HEADER, format!(\"{TOKEN_PREFIX} bearer_token_value\"))\n            .header(\"Cookie\", \"loco_cookie_key=cookie_token_value\")\n            .body(())\n            .unwrap();\n        let (parts, ()) = request.into_parts();\n\n        let result = extract_token(&jwt_config, &parts);\n        assert!(result.is_ok());\n        assert_eq!(result.unwrap(), \" bearer_token_value\");\n    }\n\n    #[test]\n    fn test_extract_from_cookie() {\n        let jwt_config = JWTConfig {\n            location: Some(config::JWTLocationConfig::Single(\n                config::JWTLocation::Cookie {\n                    name: \"loco_cookie_key\".to_string(),\n                },\n            )),\n            secret: String::new(),\n            expiration: 1,\n        };\n\n        let request = axum::http::Request::builder()\n            .uri(\"https://loco.rs\")\n            .header(AUTH_HEADER, format!(\"{TOKEN_PREFIX} bearer_token_value\"))\n            .header(\"Cookie\", \"loco_cookie_key=cookie_token_value\")\n            .body(())\n            .unwrap();\n        let (parts, ()) = request.into_parts();\n\n        let result = extract_token(&jwt_config, &parts);\n        assert!(result.is_ok());\n        assert_eq!(result.unwrap(), \"cookie_token_value\");\n    }\n\n    #[test]\n    fn test_extract_from_query() {\n        let jwt_config = JWTConfig {\n            location: Some(config::JWTLocationConfig::Single(\n                config::JWTLocation::Query {\n                    name: \"query_token\".to_string(),\n                },\n            )),\n            secret: String::new(),\n            expiration: 1,\n        };\n\n        let request = axum::http::Request::builder()\n            .uri(\"https://loco.rs?query_token=query_token_value&test=loco\")\n            .header(AUTH_HEADER, format!(\"{TOKEN_PREFIX} bearer_token_value\"))\n            .header(\"Cookie\", \"loco_cookie_key=cookie_token_value\")\n            .body(())\n            .unwrap();\n        let (parts, ()) = request.into_parts();\n\n        let result = extract_token(&jwt_config, &parts);\n        assert!(result.is_ok());\n        assert_eq!(result.unwrap(), \"query_token_value\");\n    }\n\n    #[test]\n    fn test_extract_from_multiple_locations() {\n        let jwt_config = JWTConfig {\n            location: Some(config::JWTLocationConfig::Multiple(vec![\n                config::JWTLocation::Cookie {\n                    name: \"nonexistent\".to_string(),\n                },\n                config::JWTLocation::Query {\n                    name: \"query_token\".to_string(),\n                },\n            ])),\n            secret: String::new(),\n            expiration: 1,\n        };\n\n        let request = axum::http::Request::builder()\n            .uri(\"https://loco.rs?query_token=query_token_value&test=loco\")\n            .header(AUTH_HEADER, format!(\"{TOKEN_PREFIX} bearer_token_value\"))\n            .header(\"Cookie\", \"loco_cookie_key=cookie_token_value\")\n            .body(())\n            .unwrap();\n        let (parts, ()) = request.into_parts();\n\n        let result = extract_token(&jwt_config, &parts);\n        assert!(result.is_ok());\n        assert_eq!(result.unwrap(), \"query_token_value\");\n    }\n\n    #[test]\n    fn test_extract_token_error_message_for_missing_token() {\n        let jwt_config = JWTConfig {\n            location: Some(config::JWTLocationConfig::Multiple(vec![\n                config::JWTLocation::Cookie {\n                    name: \"nonexistent\".to_string(),\n                },\n                config::JWTLocation::Query {\n                    name: \"missing\".to_string(),\n                },\n            ])),\n            secret: String::new(),\n            expiration: 1,\n        };\n\n        let request = axum::http::Request::builder()\n            .uri(\"https://loco.rs\")\n            .body(())\n            .unwrap();\n        let (parts, ()) = request.into_parts();\n\n        let result = extract_token(&jwt_config, &parts);\n        assert!(result.is_err());\n        let error_msg = result.unwrap_err().to_string();\n        assert!(error_msg.contains(\"auth.jwt.location configuration\"));\n    }\n}\n"
  },
  {
    "path": "src/controller/extractor/mod.rs",
    "content": "#[cfg(feature = \"auth_jwt\")]\npub mod auth;\npub mod shared_store;\npub mod validate;\n"
  },
  {
    "path": "src/controller/extractor/shared_store.rs",
    "content": "use crate::{app::AppContext, Error};\nuse axum::{extract::FromRequestParts, http::request::Parts};\nuse std::any::Any;\n\n/// An extractor that streamlines the process of getting static Data from the `DiContainer`.\npub struct SharedStore<T>(pub T);\n\nimpl<T> FromRequestParts<AppContext> for SharedStore<T>\nwhere\n    T: Any + Clone + Send + Sync + 'static,\n{\n    type Rejection = Error;\n\n    async fn from_request_parts(\n        _: &mut Parts,\n        state: &AppContext,\n    ) -> Result<Self, Self::Rejection> {\n        let instance = state.shared_store.get::<T>().ok_or_else(|| {\n            let type_name = std::any::type_name::<T>();\n            tracing::error!(\n                \"Could not find service of type `{}` in shared store\",\n                type_name\n            );\n            Error::InternalServerError\n        })?;\n\n        Ok(Self(instance))\n    }\n}\n"
  },
  {
    "path": "src/controller/extractor/snapshots/loco_rs__controller__extractor__auth__tests__extract_from_bearer.snap",
    "content": "---\nsource: src/controller/extractor/auth.rs\nexpression: \"extract_token(&jwt_config, &parts)\"\nsnapshot_kind: text\n---\nOk(\n    \" bearer_token_value\",\n)\n"
  },
  {
    "path": "src/controller/extractor/snapshots/loco_rs__controller__extractor__auth__tests__extract_from_cookie.snap",
    "content": "---\nsource: src/controller/extractor/auth.rs\nexpression: \"extract_token(&jwt_config, &parts)\"\nsnapshot_kind: text\n---\nOk(\n    \"cookie_token_value\",\n)\n"
  },
  {
    "path": "src/controller/extractor/snapshots/loco_rs__controller__extractor__auth__tests__extract_from_default.snap",
    "content": "---\nsource: src/controller/extractor/auth.rs\nexpression: \"extract_token(&jwt_config, &parts)\"\nsnapshot_kind: text\n---\nOk(\n    \" bearer_token_value\",\n)\n"
  },
  {
    "path": "src/controller/extractor/snapshots/loco_rs__controller__extractor__auth__tests__extract_from_multiple_locations.snap",
    "content": "---\nsource: src/controller/extractor/auth.rs\nexpression: \"extract_token(&jwt_config, &parts)\"\nsnapshot_kind: text\n---\nOk(\n    \"query_token_value\",\n)\n"
  },
  {
    "path": "src/controller/extractor/snapshots/loco_rs__controller__extractor__auth__tests__extract_from_query.snap",
    "content": "---\nsource: src/controller/extractor/auth.rs\nexpression: \"extract_token(&jwt_config, &parts)\"\nsnapshot_kind: text\n---\nOk(\n    \"query_token_value\",\n)\n"
  },
  {
    "path": "src/controller/extractor/validate.rs",
    "content": "use crate::validation::ValidatorTrait;\nuse axum::extract::{Form, FromRequest, Json, Query, Request};\nuse serde::de::DeserializeOwned;\n\nuse crate::Error;\n\n/// Axum middleware for validating JSON request bodies\n///\n/// This module provides extractors for validating JSON request bodies, form\n/// data, path parameters, and query parameters using the `validator` crate.\n/// Each extractor supports both detailed validation error messages\n/// (`WithMessage` variants) and simplified error responses.\n///\n/// # Example:\n///\n/// ```\n/// use axum::{routing::post, Router};\n/// use serde::{Deserialize, Serialize};\n/// use loco_rs::controller::extractor::validate::JsonValidateWithMessage;\n/// use validator::Validate;\n///\n/// #[derive(Serialize, Deserialize, Validate)]\n/// struct User {\n///     #[validate(length(min = 3, message = \"username must be at least 3 characters\"))]\n///     username: String,\n///     #[validate(email(message = \"email must be valid\"))]\n///     email: String,\n/// }\n///\n/// async fn create_user(JsonValidateWithMessage(user): JsonValidateWithMessage<User>) -> String {\n///     format!(\"User created: {}, {}\", user.username, user.email)\n/// }\n///\n/// fn app() -> Router {\n///     Router::new()\n///         .route(\"/users\", post(create_user))\n/// }\n/// ```\n\n#[derive(Debug, Clone, Copy, Default)]\npub struct JsonValidateWithMessage<T>(pub T);\n\nimpl<T, S> FromRequest<S> for JsonValidateWithMessage<T>\nwhere\n    T: DeserializeOwned + ValidatorTrait,\n    S: Send + Sync,\n{\n    type Rejection = Error;\n\n    async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {\n        let Json(value) = Json::<T>::from_request(req, state).await?;\n        value.validate().map_err(Error::Validation)?;\n        Ok(Self(value))\n    }\n}\n\n/// Axum middleware for validating form data\n///\n/// # Example:\n///\n/// ```\n/// use axum::{routing::post, Router};\n/// use serde::{Deserialize, Serialize};\n/// use loco_rs::controller::extractor::validate::FormValidateWithMessage;\n/// use validator::Validate;\n///\n/// #[derive(Serialize, Deserialize, Validate)]\n/// struct User {\n///     #[validate(length(min = 3, message = \"username must be at least 3 characters\"))]\n///     username: String,\n///     #[validate(email(message = \"email must be valid\"))]\n///     email: String,\n/// }\n///\n/// async fn create_user(FormValidateWithMessage(user): FormValidateWithMessage<User>) -> String {\n///     format!(\"User created: {}, {}\", user.username, user.email)\n/// }\n///\n/// fn app() -> Router {\n///     Router::new()\n///         .route(\"/users\", post(create_user))\n/// }\n/// ```\n\n#[derive(Debug, Clone, Copy, Default)]\npub struct FormValidateWithMessage<T>(pub T);\n\nimpl<T, S> FromRequest<S> for FormValidateWithMessage<T>\nwhere\n    T: DeserializeOwned + ValidatorTrait,\n    S: Send + Sync,\n{\n    type Rejection = Error;\n\n    async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {\n        let Form(value) = Form::<T>::from_request(req, state).await?;\n        value.validate().map_err(Error::Validation)?;\n        Ok(Self(value))\n    }\n}\n\n/// Axum middleware for validating JSON request bodies with simplified error\n/// handling\n///\n/// # Example:\n///\n/// ```\n/// use axum::{routing::post, Router};\n/// use serde::{Deserialize, Serialize};\n/// use loco_rs::controller::extractor::validate::JsonValidate;\n/// use validator::Validate;\n///\n/// #[derive(Serialize, Deserialize, Validate)]\n/// struct User {\n///     #[validate(length(min = 3, message = \"username must be at least 3 characters\"))]\n///     username: String,\n///     #[validate(email(message = \"email must be valid\"))]\n///     email: String,\n/// }\n///\n/// async fn create_user(JsonValidate(user): JsonValidate<User>) -> String {\n///     format!(\"User created: {}, {}\", user.username, user.email)\n/// }\n///\n/// fn app() -> Router {\n///     Router::new()\n///         .route(\"/users\", post(create_user))\n/// }\n/// ```\n\n#[derive(Debug, Clone, Copy, Default)]\npub struct JsonValidate<T>(pub T);\n\nimpl<T, S> FromRequest<S> for JsonValidate<T>\nwhere\n    T: DeserializeOwned + ValidatorTrait,\n    S: Send + Sync,\n{\n    type Rejection = Error;\n\n    async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {\n        let Json(value) = Json::<T>::from_request(req, state).await?;\n        value.validate().map_err(|err| {\n            tracing::debug!(err = ?err, \"request validation error occurred\");\n            Error::BadRequest(String::new())\n        })?;\n        Ok(Self(value))\n    }\n}\n\n/// Axum middleware for validating form data with simplified error handling\n///\n/// # Example:\n///\n/// ```\n/// use axum::{routing::post, Router};\n/// use serde::{Deserialize, Serialize};\n/// use loco_rs::controller::extractor::validate::FormValidate;\n/// use validator::Validate;\n///\n/// #[derive(Serialize, Deserialize, Validate)]\n/// struct User {\n///     #[validate(length(min = 3, message = \"username must be at least 3 characters\"))]\n///     username: String,\n///     #[validate(email(message = \"email must be valid\"))]\n///     email: String,\n/// }\n///\n/// async fn create_user(FormValidate(user): FormValidate<User>) -> String {\n///     format!(\"User created: {}, {}\", user.username, user.email)\n/// }\n///\n/// fn app() -> Router {\n///     Router::new()\n///         .route(\"/users\", post(create_user))\n/// }\n/// ```\n\n#[derive(Debug, Clone, Copy, Default)]\npub struct FormValidate<T>(pub T);\n\nimpl<T, S> FromRequest<S> for FormValidate<T>\nwhere\n    T: DeserializeOwned + ValidatorTrait,\n    S: Send + Sync,\n{\n    type Rejection = Error;\n\n    async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {\n        let Form(value) = Form::<T>::from_request(req, state).await?;\n        value.validate().map_err(|err| {\n            tracing::debug!(err = ?err, \"request validation error occurred\");\n            Error::BadRequest(String::new())\n        })?;\n        Ok(Self(value))\n    }\n}\n\n/// Axum middleware for validating query parameters\n///\n/// # Example:\n///\n/// ```\n/// use axum::{routing::get, Router};\n/// use serde::{Deserialize, Serialize};\n/// use loco_rs::controller::extractor::validate::QueryValidateWithMessage;\n/// use validator::Validate;\n///\n/// #[derive(Serialize, Deserialize, Validate)]\n/// struct UserQuery {\n///     #[validate(length(min = 3, message = \"username must be at least 3 characters\"))]\n///     username: String,\n///     #[validate(email(message = \"email must be valid\"))]\n///     email: String,\n/// }\n///\n/// async fn get_user(QueryValidateWithMessage(params): QueryValidateWithMessage<UserQuery>) -> String {\n///     format!(\"User: {}, Email: {}\", params.username, params.email)\n/// }\n///\n/// fn app() -> Router {\n///     Router::new()\n///         .route(\"/users\", get(get_user))\n/// }\n/// ```\n\n#[derive(Debug, Clone, Copy, Default)]\npub struct QueryValidateWithMessage<T>(pub T);\n\nimpl<T, S> FromRequest<S> for QueryValidateWithMessage<T>\nwhere\n    T: DeserializeOwned + ValidatorTrait,\n    S: Send + Sync,\n{\n    type Rejection = Error;\n\n    async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {\n        let Query(value) = Query::<T>::from_request(req, state)\n            .await\n            .map_err(|rejection| Error::BadRequest(format!(\"Invalid query string: {rejection}\")))?;\n        value.validate().map_err(Error::Validation)?;\n        Ok(Self(value))\n    }\n}\n\n/// Axum middleware for validating query parameters with simplified error\n/// handling\n///\n/// # Example:\n///\n/// ```\n/// use axum::{routing::get, Router};\n/// use serde::{Deserialize, Serialize};\n/// use loco_rs::controller::extractor::validate::QueryValidate;\n/// use validator::Validate;\n///\n/// #[derive(Serialize, Deserialize, Validate)]\n/// struct UserQuery {\n///     #[validate(length(min = 3, message = \"username must be at least 3 characters\"))]\n///     username: String,\n///     #[validate(email(message = \"email must be valid\"))]\n///     email: String,\n/// }\n///\n/// async fn get_user(QueryValidate(params): QueryValidate<UserQuery>) -> String {\n///     format!(\"User: {}, Email: {}\", params.username, params.email)\n/// }\n///\n/// fn app() -> Router {\n///     Router::new()\n///         .route(\"/users\", get(get_user))\n/// }\n/// ```\n\n#[derive(Debug, Clone, Copy, Default)]\npub struct QueryValidate<T>(pub T);\n\nimpl<T, S> FromRequest<S> for QueryValidate<T>\nwhere\n    T: DeserializeOwned + ValidatorTrait,\n    S: Send + Sync,\n{\n    type Rejection = Error;\n\n    async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {\n        let Query(value) = Query::<T>::from_request(req, state)\n            .await\n            .map_err(|rejection| Error::BadRequest(format!(\"Invalid query string: {rejection}\")))?;\n        value.validate().map_err(|err| {\n            tracing::debug!(err = ?err, \"query validation error occurred\");\n            Error::BadRequest(String::new())\n        })?;\n        Ok(Self(value))\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use crate::validation::{ModelValidationErrors, ValidatorTrait};\n    use axum::{\n        body::{to_bytes, Body},\n        http::{self, Request as HttpRequest, StatusCode},\n        response::IntoResponse,\n    };\n    use serde::{Deserialize, Serialize};\n    use serde_json::{json, Value};\n    use validator::Validate;\n\n    use super::*;\n\n    #[derive(Debug, Serialize, Deserialize, Validate)]\n    struct TestUser {\n        #[validate(length(min = 3, message = \"username must be at least 3 characters\"))]\n        username: String,\n        #[validate(email(message = \"email must be valid\"))]\n        email: String,\n    }\n\n    #[derive(Debug, Serialize, Deserialize, Validate)]\n    struct TestQueryParams {\n        #[validate(length(min = 3, message = \"username must be at least 3 characters\"))]\n        username: String,\n        #[validate(email(message = \"email must be valid\"))]\n        email: String,\n    }\n\n    fn create_json_request(json: &str) -> HttpRequest<Body> {\n        HttpRequest::builder()\n            .method(http::Method::POST)\n            .uri(\"/test\")\n            .header(http::header::CONTENT_TYPE, \"application/json\")\n            .body(Body::from(json.to_string()))\n            .unwrap()\n    }\n\n    fn create_form_request(form_data: &str) -> HttpRequest<Body> {\n        HttpRequest::builder()\n            .method(http::Method::POST)\n            .uri(\"/test\")\n            .header(\n                http::header::CONTENT_TYPE,\n                \"application/x-www-form-urlencoded\",\n            )\n            .body(Body::from(form_data.to_string()))\n            .unwrap()\n    }\n\n    fn create_query_request(query: &str) -> HttpRequest<Body> {\n        HttpRequest::builder()\n            .method(http::Method::GET)\n            .uri(format!(\"/test?{}\", query))\n            .body(Body::empty())\n            .unwrap()\n    }\n\n    async fn assert_response_status_and_body(\n        err: Error,\n        expected_status: StatusCode,\n        expected_json: Value,\n    ) {\n        let response = err.into_response();\n        assert_eq!(response.status(), expected_status);\n\n        let body = to_bytes(response.into_body(), 1024 * 1024)\n            .await\n            .expect(\"Failed to read response body\");\n\n        let body_str = String::from_utf8(body.to_vec()).expect(\"Response body is not valid UTF-8\");\n\n        let actual_json =\n            serde_json::from_str::<Value>(&body_str).expect(\"Response body is not valid JSON\");\n\n        assert_eq!(actual_json, expected_json);\n    }\n\n    #[tokio::test]\n    async fn test_json_validate_with_message_valid() {\n        let valid_json = r#\"{\"username\": \"valid_user\", \"email\": \"test@example.com\"}\"#;\n        let request = create_json_request(valid_json);\n\n        let result = JsonValidateWithMessage::<TestUser>::from_request(request, &()).await;\n        assert!(result.is_ok());\n\n        let user = result.unwrap().0;\n        assert_eq!(user.username, \"valid_user\");\n        assert_eq!(user.email, \"test@example.com\");\n    }\n\n    #[tokio::test]\n    async fn test_json_validate_with_message_invalid() {\n        let invalid_json = r#\"{\"username\": \"ab\", \"email\": \"invalid-email\"}\"#;\n        let request = create_json_request(invalid_json);\n\n        let result = JsonValidateWithMessage::<TestUser>::from_request(request, &()).await;\n        assert!(result.is_err());\n\n        let expected = json!({\n            \"errors\": {\n                \"username\": [\n                    {\n                        \"code\": \"length\",\n                        \"message\": \"username must be at least 3 characters\",\n                        \"params\": {\n                            \"min\": 3,\n                            \"value\": \"ab\"\n                        }\n                    }\n                ],\n                \"email\": [\n                    {\n                        \"code\": \"email\",\n                        \"message\": \"email must be valid\",\n                        \"params\": {\n                            \"value\": \"invalid-email\"\n                        }\n                    }\n                ]\n            }\n        });\n\n        assert_response_status_and_body(result.unwrap_err(), StatusCode::BAD_REQUEST, expected)\n            .await;\n    }\n\n    #[tokio::test]\n    async fn test_form_validate_with_message_valid() {\n        let valid_form = \"username=valid_user&email=test@example.com\";\n        let request = create_form_request(valid_form);\n\n        let result = FormValidateWithMessage::<TestUser>::from_request(request, &()).await;\n        assert!(result.is_ok());\n\n        let user = result.unwrap().0;\n        assert_eq!(user.username, \"valid_user\");\n        assert_eq!(user.email, \"test@example.com\");\n    }\n\n    #[tokio::test]\n    async fn test_form_validate_with_message_invalid() {\n        let invalid_form = \"username=ab&email=invalid-email\";\n        let request = create_form_request(invalid_form);\n\n        let result = FormValidateWithMessage::<TestUser>::from_request(request, &()).await;\n        assert!(result.is_err());\n\n        let expected = json!({\n            \"errors\": {\n                \"username\": [\n                    {\n                        \"code\": \"length\",\n                        \"message\": \"username must be at least 3 characters\",\n                        \"params\": {\n                            \"min\": 3,\n                            \"value\": \"ab\"\n                        }\n                    }\n                ],\n                \"email\": [\n                    {\n                        \"code\": \"email\",\n                        \"message\": \"email must be valid\",\n                        \"params\": {\n                            \"value\": \"invalid-email\"\n                        }\n                    }\n                ]\n            }\n        });\n\n        assert_response_status_and_body(result.unwrap_err(), StatusCode::BAD_REQUEST, expected)\n            .await;\n    }\n\n    #[tokio::test]\n    async fn test_json_validate_valid() {\n        let valid_json = r#\"{\"username\": \"valid_user\", \"email\": \"test@example.com\"}\"#;\n        let request = create_json_request(valid_json);\n\n        let result = JsonValidate::<TestUser>::from_request(request, &()).await;\n        assert!(result.is_ok());\n\n        let user = result.unwrap().0;\n        assert_eq!(user.username, \"valid_user\");\n        assert_eq!(user.email, \"test@example.com\");\n    }\n\n    #[tokio::test]\n    async fn test_json_validate_invalid() {\n        let invalid_json = r#\"{\"username\": \"ab\", \"email\": \"invalid-email\"}\"#;\n        let request = create_json_request(invalid_json);\n\n        let result = JsonValidate::<TestUser>::from_request(request, &()).await;\n        assert!(result.is_err());\n\n        let err = result.unwrap_err();\n        if let Error::BadRequest(msg) = &err {\n            assert_eq!(msg, &String::new());\n        } else {\n            panic!(\"Expected BadRequest error\");\n        }\n\n        let expected = json!({\n            \"error\": \"Bad Request\",\n            // \"description\": \"\"\n        });\n\n        assert_response_status_and_body(err, StatusCode::BAD_REQUEST, expected).await;\n    }\n\n    #[tokio::test]\n    async fn test_form_validate_valid() {\n        let valid_form = \"username=valid_user&email=test@example.com\";\n        let request = create_form_request(valid_form);\n\n        let result = FormValidate::<TestUser>::from_request(request, &()).await;\n        assert!(result.is_ok());\n\n        let user = result.unwrap().0;\n        assert_eq!(user.username, \"valid_user\");\n        assert_eq!(user.email, \"test@example.com\");\n    }\n\n    #[tokio::test]\n    async fn test_form_validate_invalid() {\n        let invalid_form = \"username=ab&email=invalid-email\";\n        let request = create_form_request(invalid_form);\n\n        let result = FormValidate::<TestUser>::from_request(request, &()).await;\n        assert!(result.is_err());\n\n        let err = result.unwrap_err();\n        if let Error::BadRequest(msg) = &err {\n            assert_eq!(msg, &String::new());\n        } else {\n            panic!(\"Expected BadRequest error\");\n        }\n\n        let expected = json!({\n            \"error\": \"Bad Request\",\n            // \"description\": \"\"\n        });\n\n        assert_response_status_and_body(err, StatusCode::BAD_REQUEST, expected).await;\n    }\n\n    #[tokio::test]\n    async fn test_malformed_json() {\n        let invalid_json = r#\"{\"username\": \"valid_user\", \"email\": \"test@example.com\"#;\n        let request = create_json_request(invalid_json);\n\n        let result = JsonValidate::<TestUser>::from_request(request, &()).await;\n        assert!(result.is_err());\n\n        let expected = json!({\n            \"error\": \"Bad Request\",\n            // \"description\": \"invalid type: map, expected a string at line 1 column 47\"\n        });\n\n        assert_response_status_and_body(result.unwrap_err(), StatusCode::BAD_REQUEST, expected)\n            .await;\n    }\n\n    #[tokio::test]\n    async fn test_malformed_form() {\n        let invalid_form = \"username=valid_user&email%invalid_format\";\n        let request = create_form_request(invalid_form);\n\n        let result = FormValidate::<TestUser>::from_request(request, &()).await;\n        assert!(result.is_err());\n\n        let expected = json!({\n            \"error\": \"internal_server_error\",\n            \"description\": \"Internal Server Error\"\n        });\n\n        assert_response_status_and_body(\n            result.unwrap_err(),\n            StatusCode::INTERNAL_SERVER_ERROR,\n            expected,\n        )\n        .await;\n    }\n\n    // Custom validator that does not rely on the `validator` crate\n    #[derive(Debug, Serialize, Deserialize)]\n    struct CustomData {\n        username: String,\n        email: String,\n    }\n\n    impl ValidatorTrait for CustomData {\n        fn validate(&self) -> Result<(), ModelValidationErrors> {\n            use crate::validation::ValidationError;\n            use std::collections::{BTreeMap, HashMap};\n            if self.username.len() < 3 {\n                let mut errors: BTreeMap<String, Vec<ValidationError>> = BTreeMap::new();\n                errors.insert(\n                    \"username\".to_string(),\n                    vec![ValidationError {\n                        code: \"length\".to_string(),\n                        message: Some(\"username must be at least 3 characters\".to_string()),\n                        params: HashMap::new(),\n                    }],\n                );\n                return Err(ModelValidationErrors { errors });\n            }\n            if !self.email.contains('@') {\n                let mut errors: BTreeMap<String, Vec<ValidationError>> = BTreeMap::new();\n                errors.insert(\n                    \"email\".to_string(),\n                    vec![ValidationError {\n                        code: \"email\".to_string(),\n                        message: Some(\"email must be valid\".to_string()),\n                        params: HashMap::new(),\n                    }],\n                );\n                return Err(ModelValidationErrors { errors });\n            }\n            Ok(())\n        }\n    }\n\n    #[tokio::test]\n    async fn test_json_validate_with_message_custom_validator_invalid() {\n        let invalid_json = r#\"{\"username\": \"ab\", \"email\": \"invalid\"}\"#;\n        let request = create_json_request(invalid_json);\n\n        let result = JsonValidateWithMessage::<CustomData>::from_request(request, &()).await;\n        assert!(result.is_err());\n\n        let expected = json!({\n            \"errors\": {\n                \"username\": [\n                    { \"code\": \"length\", \"message\": \"username must be at least 3 characters\" }\n                ]\n            }\n        });\n\n        assert_response_status_and_body(result.unwrap_err(), StatusCode::BAD_REQUEST, expected)\n            .await;\n    }\n\n    #[tokio::test]\n    async fn test_json_validate_with_message_custom_validator_valid() {\n        let valid_json = r#\"{\"username\": \"valid_user\", \"email\": \"valid@example.com\"}\"#;\n        let request = create_json_request(valid_json);\n\n        let result = JsonValidateWithMessage::<CustomData>::from_request(request, &()).await;\n        assert!(result.is_ok());\n    }\n\n    #[tokio::test]\n    async fn test_query_validate_with_message_valid() {\n        let valid_query = \"username=valid_user&email=test@example.com\";\n        let request = create_query_request(valid_query);\n\n        let result = QueryValidateWithMessage::<TestQueryParams>::from_request(request, &()).await;\n        assert!(result.is_ok());\n\n        let params = result.unwrap().0;\n        assert_eq!(params.username, \"valid_user\");\n        assert_eq!(params.email, \"test@example.com\");\n    }\n\n    #[tokio::test]\n    async fn test_query_validate_with_message_invalid() {\n        let invalid_query = \"username=ab&email=invalid-email\";\n        let request = create_query_request(invalid_query);\n\n        let result = QueryValidateWithMessage::<TestQueryParams>::from_request(request, &()).await;\n        assert!(result.is_err());\n\n        let expected = json!({\n            \"errors\": {\n                \"username\": [\n                    {\n                        \"code\": \"length\",\n                        \"message\": \"username must be at least 3 characters\",\n                        \"params\": {\n                            \"min\": 3,\n                            \"value\": \"ab\"\n                        }\n                    }\n                ],\n                \"email\": [\n                    {\n                        \"code\": \"email\",\n                        \"message\": \"email must be valid\",\n                        \"params\": {\n                            \"value\": \"invalid-email\"\n                        }\n                    }\n                ]\n            }\n        });\n\n        assert_response_status_and_body(result.unwrap_err(), StatusCode::BAD_REQUEST, expected)\n            .await;\n    }\n\n    #[tokio::test]\n    async fn test_query_validate_valid() {\n        let valid_query = \"username=valid_user&email=test@example.com\";\n        let request = create_query_request(valid_query);\n\n        let result = QueryValidate::<TestQueryParams>::from_request(request, &()).await;\n        assert!(result.is_ok());\n\n        let params = result.unwrap().0;\n        assert_eq!(params.username, \"valid_user\");\n        assert_eq!(params.email, \"test@example.com\");\n    }\n\n    #[tokio::test]\n    async fn test_query_validate_invalid() {\n        let invalid_query = \"username=ab&email=invalid-email\";\n        let request = create_query_request(invalid_query);\n\n        let result = QueryValidate::<TestQueryParams>::from_request(request, &()).await;\n        assert!(result.is_err());\n\n        let err = result.unwrap_err();\n        if let Error::BadRequest(msg) = &err {\n            assert_eq!(msg, &String::new());\n        } else {\n            panic!(\"Expected BadRequest error\");\n        }\n\n        let expected = json!({\n            \"error\": \"Bad Request\",\n            // \"description\": \"\"\n        });\n\n        assert_response_status_and_body(err, StatusCode::BAD_REQUEST, expected).await;\n    }\n\n    #[tokio::test]\n    async fn test_malformed_query() {\n        let invalid_query = \"username=valid_user&email=invalid_format\";\n        let request = create_query_request(invalid_query);\n\n        let result = QueryValidate::<TestQueryParams>::from_request(request, &()).await;\n        assert!(result.is_err());\n\n        let expected = json!({\n            \"error\": \"Bad Request\",\n            // \"description\": \"Invalid query string: expected `=` after key\"\n        });\n\n        assert_response_status_and_body(result.unwrap_err(), StatusCode::BAD_REQUEST, expected)\n            .await;\n    }\n}\n"
  },
  {
    "path": "src/controller/format.rs",
    "content": "//! This module contains utility functions for generating HTTP responses that\n//! are commonly used in web applications. These functions simplify the process\n//! of creating responses with various data types.\n//!\n//! # Example:\n//!\n//! This example illustrates how to construct a JSON-formatted response using a\n//! Rust struct.\n//!\n//! ```rust\n//! use loco_rs::prelude::*;\n//! use serde::Serialize;\n//!\n//! #[derive(Serialize)]\n//! pub struct Health {\n//!     pub ok: bool,\n//! }\n//!\n//! async fn ping() -> Result<Response> {\n//!    format::json(Health { ok: true })\n//! }\n//! ```\nuse std::convert::TryInto;\n\nuse axum::{\n    body::Body,\n    http::{header, response::Builder, HeaderName, HeaderValue, StatusCode},\n    response::{Html, IntoResponse, Redirect, Response},\n};\nuse axum_extra::extract::cookie::Cookie;\nuse bytes::{BufMut, BytesMut};\nuse serde::Serialize;\nuse serde_json::json;\n\nuse crate::{\n    controller::{\n        views::{self, ViewRenderer},\n        Json,\n    },\n    Result,\n};\n\n/// Returns an empty response.\n///\n/// # Example:\n///\n/// This example illustrates how to return an empty response.\n/// ```rust\n/// use loco_rs::prelude::*;\n///\n/// async fn endpoint() -> Result<Response> {\n///    format::empty()\n/// }\n/// ```\n///\n/// # Errors\n///\n/// Currently this function doesn't return any error. this is for feature\n/// functionality\npub fn empty() -> Result<Response> {\n    Ok(().into_response())\n}\n\n/// Returns a response containing the provided text.\n///\n/// # Example:\n///\n/// This example illustrates how to return an text response.\n/// ```rust\n/// use loco_rs::prelude::*;\n///\n/// async fn endpoint() -> Result<Response> {\n///    format::text(\"MESSAGE-RESPONSE\")\n/// }\n/// ```\n///\n/// # Errors\n///\n/// Currently this function doesn't return any error. this is for feature\n/// functionality\npub fn text(t: &str) -> Result<Response> {\n    Ok(t.to_string().into_response())\n}\n\n/// Returns a JSON response containing the provided data.\n///\n/// # Example:\n///\n/// This example illustrates how to construct a JSON-formatted response using a\n/// Rust struct.\n///\n/// ```rust\n/// use loco_rs::prelude::*;\n/// use serde::Serialize;\n///\n/// #[derive(Serialize)]\n/// pub struct Health {\n///     pub ok: bool,\n/// }\n///\n/// async fn endpoint() -> Result<Response> {\n///    format::json(Health { ok: true })\n/// }\n/// ```\n///\n/// # Errors\n///\n/// Currently this function doesn't return any error. this is for feature\n/// functionality\npub fn json<T: Serialize>(t: T) -> Result<Response> {\n    Ok(Json(t).into_response())\n}\n\n/// Respond with empty json (`{}`)\n///\n/// # Errors\n///\n/// This function will return an error if serde fails\npub fn empty_json() -> Result<Response> {\n    json(json!({}))\n}\n\n/// Returns an HTML response\n///\n/// # Example:\n///\n/// ```rust\n/// use loco_rs::prelude::*;\n///\n/// async fn endpoint() -> Result<Response> {\n///    format::html(\"hello, world\")\n/// }\n/// ```\n///\n/// # Errors\n///\n/// Currently this function doesn't return any error. this is for feature\n/// functionality\npub fn html(content: &str) -> Result<Response> {\n    Ok(Html(content.to_string()).into_response())\n}\n\n/// Returns a YAML response\n///\n/// # Example:\n///\n/// ```rust\n/// use loco_rs::prelude::*;\n///\n/// pub async fn openapi_spec_yaml() -> Result<Response> {\n///     format::yaml(\"openapi: 3.1.0\\ninfo:\\n  title: Loco Demo\\n  \")\n/// }\n/// ```\n///\n/// # Errors\n///\n/// Currently this function doesn't return any error. this is for feature\n/// functionality\npub fn yaml(content: &str) -> Result<Response> {\n    Ok(Builder::new()\n        .header(header::CONTENT_TYPE, \"application/yaml\")\n        .body(Body::from(content.to_string()))?\n        .into_response())\n}\n\n/// Returns an redirect response\n///\n/// # Example:\n///\n/// ```rust\n/// use loco_rs::prelude::*;\n///\n/// async fn login() -> Result<Response> {\n///    format::redirect(\"/dashboard\")\n/// }\n/// ```\n///\n/// # Errors\n///\n/// Currently this function doesn't return any error. this is for feature\n/// functionality\npub fn redirect(to: &str) -> Result<Response> {\n    Ok(Redirect::to(to).into_response())\n}\n\n/// Render template located by `key`\n///\n/// # Errors\n///\n/// This function will return an error if rendering fails\npub fn view<V, S>(v: &V, key: &str, data: S) -> Result<Response>\nwhere\n    V: ViewRenderer,\n    S: Serialize,\n{\n    let res = v.render(key, data)?;\n    html(&res)\n}\n\n/// Render template from string\n///\n/// # Errors\n///\n/// This function will return an error if rendering fails\npub fn template<S>(template: &str, data: S) -> Result<Response>\nwhere\n    S: Serialize,\n{\n    html(&views::template(template, data)?)\n}\n\n#[derive(Debug)]\npub struct RenderBuilder {\n    response: Builder,\n}\n\nimpl RenderBuilder {\n    #[must_use]\n    pub fn new() -> Self {\n        Self {\n            response: Builder::new().status(StatusCode::OK),\n        }\n    }\n\n    /// Get an Axum response builder (escape hatch, leaving this builder)\n    #[must_use]\n    pub fn response(self) -> Builder {\n        self.response\n    }\n\n    /// Add a status code\n    #[must_use]\n    pub fn status<T>(self, status: T) -> Self\n    where\n        StatusCode: TryFrom<T>,\n        <StatusCode as TryFrom<T>>::Error: Into<axum::http::Error>,\n    {\n        Self {\n            response: self.response.status(status),\n        }\n    }\n\n    /// Add a single header\n    #[must_use]\n    pub fn header<K, V>(self, key: K, value: V) -> Self\n    where\n        HeaderName: TryFrom<K>,\n        <HeaderName as TryFrom<K>>::Error: Into<axum::http::Error>,\n        HeaderValue: TryFrom<V>,\n        <HeaderValue as TryFrom<V>>::Error: Into<axum::http::Error>,\n    {\n        Self {\n            response: self.response.header(key, value),\n        }\n    }\n\n    /// Add an etag\n    ///\n    /// # Errors\n    ///\n    /// This function will return an error if provided etag value is illegal\n    /// (not visible ASCII)\n    pub fn etag(self, etag: &str) -> Result<Self> {\n        Ok(Self {\n            response: self\n                .response\n                .header(header::ETAG, HeaderValue::from_str(etag)?),\n        })\n    }\n\n    /// Add a collection of cookies to the response\n    ///\n    /// # Errors\n    /// Returns error if cookie values are illegal\n    pub fn cookies(self, cookies: &[Cookie<'_>]) -> Result<Self> {\n        let mut res = self.response;\n        for cookie in cookies {\n            let header_value = cookie.encoded().to_string().parse::<HeaderValue>()?;\n            res = res.header(header::SET_COOKIE, header_value);\n        }\n        Ok(Self { response: res })\n    }\n\n    /// Finalize and return a text response\n    ///\n    /// # Errors\n    ///\n    /// This function will return an error if IO fails\n    pub fn text(self, content: &str) -> Result<Response> {\n        Ok(self\n            .response\n            .header(\n                header::CONTENT_TYPE,\n                HeaderValue::from_static(\"text/plain; charset=utf-8\"),\n            )\n            .body(Body::from(content.to_string()))?)\n    }\n\n    /// Finalize and return an empty response\n    ///\n    /// # Errors\n    ///\n    /// This function will return an error if IO fails\n    pub fn empty(self) -> Result<Response> {\n        Ok(self.response.body(Body::empty())?)\n    }\n\n    /// Render template located by `key`\n    ///\n    /// # Errors\n    ///\n    /// This function will return an error if rendering fails\n    pub fn view<V, S>(self, v: &V, key: &str, data: S) -> Result<Response>\n    where\n        V: ViewRenderer,\n        S: Serialize,\n    {\n        let content = v.render(key, data)?;\n        self.html(&content)\n    }\n\n    /// Render template located by `key`\n    ///\n    /// # Errors\n    ///\n    /// This function will return an error if rendering fails\n    pub fn template<S>(self, template: &str, data: S) -> Result<Response>\n    where\n        S: Serialize,\n    {\n        html(&views::template(template, data)?)\n    }\n\n    /// Finalize and return a HTML response\n    ///\n    /// # Errors\n    ///\n    /// This function will return an error if IO fails\n    pub fn html(self, content: &str) -> Result<Response> {\n        Ok(self\n            .response\n            .header(\n                header::CONTENT_TYPE,\n                HeaderValue::from_static(\"text/html; charset=utf-8\"),\n            )\n            .body(Body::from(content.to_string()))?)\n    }\n\n    /// Finalize and return a JSON response\n    ///\n    /// # Errors\n    ///\n    /// This function will return an error if IO fails\n    pub fn json<T>(self, item: T) -> Result<Response>\n    where\n        T: Serialize,\n    {\n        let mut buf = BytesMut::with_capacity(128).writer();\n        serde_json::to_writer(&mut buf, &item)?;\n        let body = Body::from(buf.into_inner().freeze());\n        Ok(self\n            .response\n            .header(\n                header::CONTENT_TYPE,\n                HeaderValue::from_static(\"application/json\"),\n            )\n            .body(body)?)\n    }\n\n    /// Finalize and redirect request\n    ///\n    /// # Errors\n    ///\n    /// This function will return an error if IO fails\n    pub fn redirect(self, to: &str) -> Result<Response> {\n        self.redirect_with_header_key(header::LOCATION, to)\n    }\n\n    /// Finalizes the HTTP response and redirects to a specified location using\n    /// a dynamic header key.\n    ///\n    /// # Errors\n    ///\n    /// This function will return an error if IO fails\n    pub fn redirect_with_header_key<K>(self, key: K, to: &str) -> Result<Response>\n    where\n        K: TryInto<HeaderName>,\n        <K as TryInto<HeaderName>>::Error: Into<axum::http::Error>,\n    {\n        Ok(self\n            .response\n            .status(StatusCode::SEE_OTHER)\n            .header(key, to)\n            .body(Body::empty())?)\n    }\n}\n\nimpl Default for RenderBuilder {\n    fn default() -> Self {\n        Self::new()\n    }\n}\n\n#[must_use]\npub fn render() -> RenderBuilder {\n    RenderBuilder::new()\n}\n\n#[cfg(test)]\nmod tests {\n\n    use axum::http::Response;\n    use insta::assert_debug_snapshot;\n\n    use super::*;\n    use crate::prelude::*;\n\n    async fn response_body_to_string(response: Response<Body>) -> String {\n        let bytes = axum::body::to_bytes(response.into_body(), 200)\n            .await\n            .unwrap();\n        std::str::from_utf8(&bytes).unwrap().to_string()\n    }\n\n    pub fn get_header_from_response(response: &Response<Body>, header: &str) -> Option<String> {\n        Some(response.headers().get(header)?.to_str().ok()?.to_string())\n    }\n\n    #[tokio::test]\n    async fn empty_response_format() {\n        let response: Response<Body> = empty().unwrap();\n\n        assert_debug_snapshot!(response);\n        assert_eq!(response_body_to_string(response).await, String::new());\n    }\n\n    #[tokio::test]\n    async fn text_response_format() {\n        let response_content = \"loco\";\n        let response = text(response_content).unwrap();\n\n        assert_debug_snapshot!(response);\n        assert_eq!(response_body_to_string(response).await, response_content);\n    }\n\n    #[tokio::test]\n    async fn json_response_format() {\n        let response_content = serde_json::json!({\"loco\": \"app\"});\n        let response = json(&response_content).unwrap();\n\n        assert_debug_snapshot!(response);\n        assert_eq!(\n            response_body_to_string(response).await,\n            response_content.to_string()\n        );\n    }\n\n    #[tokio::test]\n    async fn empty_json_response_format() {\n        let response = empty_json().unwrap();\n\n        assert_debug_snapshot!(response);\n        assert_eq!(\n            response_body_to_string(response).await,\n            serde_json::json!({}).to_string()\n        );\n    }\n\n    #[tokio::test]\n    async fn html_response_format() {\n        let response_content: &str = \"<h1>loco</h1>\";\n        let response = html(response_content).unwrap();\n\n        assert_debug_snapshot!(response);\n        assert_eq!(response_body_to_string(response).await, response_content);\n    }\n\n    #[tokio::test]\n    async fn yaml_response_format() {\n        let response_content: &str = \"openapi: 3.1.0\\ninfo:\\n  title: Loco Demo\\n  \";\n        let response = yaml(response_content).unwrap();\n\n        assert_debug_snapshot!(response);\n        assert_eq!(response_body_to_string(response).await, response_content);\n    }\n\n    #[tokio::test]\n    async fn redirect_response() {\n        let response = redirect(\"https://loco.rs\").unwrap();\n\n        assert_debug_snapshot!(response);\n        assert_eq!(response_body_to_string(response).await, String::new());\n    }\n\n    #[cfg(not(feature = \"embedded_assets\"))]\n    #[tokio::test]\n    async fn view_response() {\n        let tree_fs = tree_fs::TreeBuilder::default()\n            .add_file(\"template/test.html\", \"- {{foo}}\")\n            .create()\n            .unwrap();\n\n        let v = TeraView::from_custom_dir(&tree_fs.root, |_| Ok(())).unwrap();\n\n        assert_debug_snapshot!(view(&v, \"template/none.html\", serde_json::json!({})));\n        let response = view(&v, \"template/test.html\", serde_json::json!({\"foo\": \"loco\"})).unwrap();\n\n        assert_debug_snapshot!(response);\n        assert_eq!(&response_body_to_string(response).await, \"- loco\");\n    }\n\n    #[tokio::test]\n    async fn template_response() {\n        let response = template(\"- {{foo}}\", serde_json::json!({\"foo\": \"loco\"})).unwrap();\n\n        assert_debug_snapshot!(response);\n        assert_eq!(&response_body_to_string(response).await, \"- loco\");\n    }\n\n    #[tokio::test]\n    async fn builder_set_status_code_response() {\n        assert_eq!(render().empty().unwrap().status(), 200);\n        assert_eq!(render().status(202).empty().unwrap().status(), 202);\n    }\n\n    #[tokio::test]\n    async fn builder_set_headers_response() {\n        assert_eq!(render().empty().unwrap().headers().len(), 0);\n        let response = render()\n            .header(\"header-1\", \"loco\")\n            .header(\"header-2\", \"rs\")\n            .empty()\n            .unwrap();\n\n        assert_eq!(response.headers().len(), 2);\n        assert_eq!(\n            get_header_from_response(&response, \"header-1\"),\n            Some(\"loco\".to_string())\n        );\n        assert_eq!(\n            get_header_from_response(&response, \"header-2\"),\n            Some(\"rs\".to_string())\n        );\n    }\n\n    #[tokio::test]\n    async fn builder_etag_response() {\n        assert_eq!(render().empty().unwrap().headers().len(), 0);\n        let response = render().etag(\"foobar\").unwrap().empty().unwrap();\n\n        assert_eq!(response.headers().len(), 1);\n        assert_eq!(\n            get_header_from_response(&response, \"etag\"),\n            Some(\"foobar\".to_string())\n        );\n    }\n\n    #[tokio::test]\n    async fn builder_cookies_response() {\n        let response = render()\n            .cookies(&[\n                cookie::Cookie::new(\"foo\", \"bar\"),\n                cookie::Cookie::new(\"baz\", \"qux\"),\n            ])\n            .unwrap()\n            .empty()\n            .unwrap();\n\n        assert_debug_snapshot!(response.headers());\n    }\n\n    #[tokio::test]\n    async fn builder_text_response() {\n        let response = render().text(\"loco\").unwrap();\n\n        assert_debug_snapshot!(response);\n        assert_eq!(&response_body_to_string(response).await, \"loco\");\n    }\n\n    #[tokio::test]\n    async fn builder_empty_response() {\n        let response = render().empty().unwrap();\n\n        assert_debug_snapshot!(response);\n        assert_eq!(response_body_to_string(response).await, String::new());\n    }\n\n    #[cfg(not(feature = \"embedded_assets\"))]\n    #[tokio::test]\n    async fn builder_view_response() {\n        let tree_fs = tree_fs::TreeBuilder::default()\n            .add_file(\"template/test.html\", \"- {{foo}}\")\n            .create()\n            .unwrap();\n\n        let v = TeraView::from_custom_dir(&tree_fs.root, |_| Ok(())).unwrap();\n\n        assert_debug_snapshot!(view(&v, \"template/none.html\", serde_json::json!({})));\n        let response = render()\n            .view(&v, \"template/test.html\", serde_json::json!({\"foo\": \"loco\"}))\n            .unwrap();\n\n        assert_debug_snapshot!(response);\n        assert_eq!(&response_body_to_string(response).await, \"- loco\");\n    }\n\n    #[tokio::test]\n    async fn builder_template_response() {\n        let response = render()\n            .template(\"- {{foo}}\", serde_json::json!({\"foo\": \"loco\"}))\n            .unwrap();\n\n        assert_debug_snapshot!(response);\n        assert_eq!(&response_body_to_string(response).await, \"- loco\");\n    }\n\n    #[tokio::test]\n    async fn builder_html_response() {\n        let response_content = \"<h1>loco</h1>\";\n        let response = render().html(response_content).unwrap();\n\n        assert_debug_snapshot!(response);\n        assert_eq!(&response_body_to_string(response).await, response_content);\n    }\n\n    #[tokio::test]\n    async fn builder_json_response() {\n        let response_content = serde_json::json!({\"loco\": \"app\"});\n        let response = render().json(&response_content).unwrap();\n\n        assert_debug_snapshot!(response);\n        assert_eq!(\n            response_body_to_string(response).await,\n            response_content.to_string()\n        );\n    }\n\n    #[tokio::test]\n    async fn builder_redirect_response() {\n        let response = render().redirect(\"https://loco.rs\").unwrap();\n\n        assert_debug_snapshot!(response);\n        assert_eq!(response_body_to_string(response).await, String::new());\n    }\n\n    #[tokio::test]\n    async fn builder_redirect_with_custom_header_response() {\n        let response = render()\n            .redirect_with_header_key(\"HX-Redirect\", \"https://loco.rs\")\n            .unwrap();\n\n        assert_debug_snapshot!(response);\n        assert_eq!(response_body_to_string(response).await, String::new());\n    }\n}\n"
  },
  {
    "path": "src/controller/middleware/_archive/content_etag.rs",
    "content": "use std::{\n    pin::Pin,\n    task::{Context, Poll},\n};\n\nuse axum::{\n    body::{to_bytes, Body, Bytes},\n    extract::Request,\n    response::Response,\n    BoxError,\n};\nuse futures_util::Future;\nuse hyper::header::{ETAG, IF_NONE_MATCH};\nuse sha2::{Digest, Sha256};\nuse tower::{Layer, Service}; // Corrected import\n\n#[derive(Clone, Debug)]\npub struct EtagLayer;\n\nimpl EtagLayer {\n    pub fn new() -> Self {\n        Self {}\n    }\n}\nimpl<S> Layer<S> for EtagLayer {\n    type Service = EtagMiddleware<S>;\n\n    fn layer(&self, inner: S) -> Self::Service {\n        EtagMiddleware { inner }\n    }\n}\n\n#[derive(Clone, Debug)]\npub struct EtagMiddleware<S> {\n    inner: S,\n}\n\nimpl<S> Service<Request<Body>> for EtagMiddleware<S>\nwhere\n    S: Service<Request<Body>, Response = Response<Body>>,\n    S::Response: 'static,\n    S::Error: Into<BoxError> + 'static,\n    S::Future: Send + 'static,\n{\n    type Response = S::Response;\n    type Error = BoxError;\n    type Future =\n        Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send + 'static>>;\n\n    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {\n        self.inner.poll_ready(cx).map_err(Into::into)\n    }\n\n    fn call(&mut self, request: Request) -> Self::Future {\n        let ifnm = request.headers().get(IF_NONE_MATCH).cloned();\n        // TODO:\n        // handle case where headers already have etag header because some other\n        // middleware added it or someone added it manually, and short-circuit\n        // the comparison and bail\n        // then split this into 2 in config\n        //      <etag route - doesnt exist yet>\n        //      etag_response: true\n        //        regex for which routes to do this on\n        //\n        //      etag: true\n        //       looks for the etag header itself, has to appear in the end\n        //\n        let future = self.inner.call(request);\n        let res_fut = async move {\n            let response = future.await.map_err(Into::into)?;\n            let (parts, body) = response.into_parts();\n            to_bytes(body, 5_000_000)\n                .await\n                .and_then(|bytes| {\n                    let etag = calculate_etag(&bytes);\n                    let response = Response::from_parts(parts, Body::from(bytes));\n\n                    if let Some(etag_in_request) = ifnm {\n                        if etag_in_request == &etag {\n                            return Ok(Response::builder()\n                                .status(304)\n                                .body(Body::empty())\n                                .unwrap());\n                        }\n                    }\n\n                    let mut response_with_etag = response;\n                    response_with_etag\n                        .headers_mut()\n                        .insert(ETAG, etag.parse().unwrap());\n                    Ok(response_with_etag)\n                })\n                .map_err(|err| Box::new(err) as Box<dyn std::error::Error + Send + Sync>)\n        };\n        Box::pin(res_fut)\n    }\n}\n\nfn calculate_etag(bytes: &Bytes) -> String {\n    format!(\"{:x}\", Sha256::digest(bytes))\n}\n\n// Usage in Axum application setup remains the same\n"
  },
  {
    "path": "src/controller/middleware/catch_panic.rs",
    "content": "//! Catch Panic Middleware for Axum\n//!\n//! This middleware catches panics that occur during request handling in the\n//! application. When a panic occurs, it logs the error and returns an\n//! internal server error response. This middleware helps ensure that the\n//! application can gracefully handle unexpected errors without crashing the\n//! server.\nuse axum::Router as AXRouter;\nuse serde::{Deserialize, Serialize};\nuse tower_http::catch_panic::CatchPanicLayer;\n\nuse crate::{\n    app::AppContext,\n    controller::{middleware::MiddlewareLayer, IntoResponse},\n    errors, Result,\n};\n\n#[derive(Debug, Clone, Deserialize, Serialize)]\npub struct CatchPanic {\n    #[serde(default)]\n    pub enable: bool,\n}\n\n/// Handler function for the [`CatchPanicLayer`] middleware.\n///\n/// This function processes panics by extracting error messages, logging them,\n/// and returning an internal server error response.\n#[allow(clippy::needless_pass_by_value)]\nfn handle_panic(err: Box<dyn std::any::Any + Send + 'static>) -> axum::response::Response {\n    let err = err.downcast_ref::<String>().map_or_else(\n        || err.downcast_ref::<&str>().map_or(\"no error details\", |s| s),\n        |s| s.as_str(),\n    );\n\n    tracing::error!(err.msg = err, \"server_panic\");\n\n    errors::Error::InternalServerError.into_response()\n}\n\nimpl MiddlewareLayer for CatchPanic {\n    /// Returns the name of the middleware\n    fn name(&self) -> &'static str {\n        \"catch_panic\"\n    }\n\n    /// Returns whether the middleware is enabled or not\n    fn is_enabled(&self) -> bool {\n        self.enable\n    }\n\n    fn config(&self) -> serde_json::Result<serde_json::Value> {\n        serde_json::to_value(self)\n    }\n\n    /// Applies the Catch Panic middleware layer to the Axum router.\n    fn apply(&self, app: AXRouter<AppContext>) -> Result<AXRouter<AppContext>> {\n        Ok(app.layer(CatchPanicLayer::custom(handle_panic)))\n    }\n}\n\n#[cfg(test)]\nmod tests {\n\n    use axum::{\n        body::Body,\n        http::{Method, Request, StatusCode},\n        routing::get,\n        Router,\n    };\n    use tower::ServiceExt;\n\n    use super::*;\n    use crate::tests_cfg;\n\n    #[allow(dependency_on_unit_never_type_fallback)]\n    #[tokio::test]\n    async fn panic_enabled() {\n        let middleware = CatchPanic { enable: true };\n\n        let app = Router::new().route(\"/\", get(|| async { panic!(\"panic\") }));\n        let app = middleware\n            .apply(app)\n            .expect(\"apply middleware\")\n            .with_state(tests_cfg::app::get_app_context().await);\n\n        let req = Request::builder()\n            .uri(\"/\")\n            .method(Method::GET)\n            .body(Body::empty())\n            .expect(\"request\");\n\n        let response = app.oneshot(req).await.expect(\"valid response\");\n\n        assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);\n    }\n\n    #[test]\n    fn should_be_disabled() {\n        let middleware = CatchPanic { enable: false };\n        assert!(!middleware.is_enabled());\n    }\n}\n"
  },
  {
    "path": "src/controller/middleware/compression.rs",
    "content": "//! Compression Middleware for Axum\n//!\n//! This middleware applies compression to HTTP responses to reduce the size of\n//! the data being transmitted. This can improve performance by decreasing load\n//! times and reducing bandwidth usage. The middleware configuration allows for\n//! enabling or disabling compression based on the application settings.\n\nuse axum::Router as AXRouter;\nuse serde::{Deserialize, Serialize};\nuse tower_http::compression::CompressionLayer;\n\nuse crate::{app::AppContext, controller::middleware::MiddlewareLayer, Result};\n\n#[derive(Debug, Clone, Deserialize, Serialize)]\npub struct Compression {\n    #[serde(default)]\n    pub enable: bool,\n}\n\nimpl MiddlewareLayer for Compression {\n    /// Returns the name of the middleware\n    fn name(&self) -> &'static str {\n        \"compression\"\n    }\n\n    /// Returns whether the middleware is enabled or not\n    fn is_enabled(&self) -> bool {\n        self.enable\n    }\n\n    fn config(&self) -> serde_json::Result<serde_json::Value> {\n        serde_json::to_value(self)\n    }\n\n    /// Applies the Compression middleware layer to the Axum router.\n    fn apply(&self, app: AXRouter<AppContext>) -> Result<AXRouter<AppContext>> {\n        Ok(app.layer(CompressionLayer::new()))\n    }\n}\n"
  },
  {
    "path": "src/controller/middleware/cors.rs",
    "content": "//! Configurable and Flexible CORS Middleware\n//!\n//! This middleware enables Cross-Origin Resource Sharing (CORS) by allowing\n//! configurable origins, methods, and headers in HTTP requests. It can be\n//! tailored to fit various application requirements, supporting permissive CORS\n//! or specific rules as defined in the middleware configuration.\n\nuse std::time::Duration;\n\nuse axum::Router as AXRouter;\nuse serde::{Deserialize, Serialize};\nuse serde_json::json;\nuse tower_http::cors::{self, Any};\n\nuse crate::{app::AppContext, controller::middleware::MiddlewareLayer, Result};\n\n/// CORS middleware configuration\n#[derive(Debug, Clone, Deserialize, Serialize)]\npub struct Cors {\n    #[serde(default)]\n    pub enable: bool,\n    /// Allow origins\n    #[serde(default = \"default_allow_origins\")]\n    pub allow_origins: Vec<String>,\n    /// Allow headers\n    #[serde(default = \"default_allow_headers\")]\n    pub allow_headers: Vec<String>,\n    /// Allow methods\n    #[serde(default = \"default_allow_methods\")]\n    pub allow_methods: Vec<String>,\n    /// Expose headers\n    #[serde(default = \"default_expose_headers\")]\n    pub expose_headers: Vec<String>,\n    /// Allow credentials\n    #[serde(default)]\n    pub allow_credentials: bool,\n    /// Max age\n    pub max_age: Option<u64>,\n    // Vary headers\n    #[serde(default = \"default_vary_headers\")]\n    pub vary: Vec<String>,\n}\n\nfn default_allow_origins() -> Vec<String> {\n    vec![\"*\".to_string()]\n}\n\nfn default_allow_headers() -> Vec<String> {\n    vec![\"*\".to_string()]\n}\n\nfn default_allow_methods() -> Vec<String> {\n    vec![\"*\".to_string()]\n}\n\nfn default_expose_headers() -> Vec<String> {\n    vec![]\n}\n\nfn default_vary_headers() -> Vec<String> {\n    vec![\n        \"origin\".to_string(),\n        \"access-control-request-method\".to_string(),\n        \"access-control-request-headers\".to_string(),\n    ]\n}\n\nimpl Default for Cors {\n    fn default() -> Self {\n        serde_json::from_value(json!({})).unwrap()\n    }\n}\n\nimpl Cors {\n    /// Creates cors layer\n    ///\n    /// # Errors\n    ///\n    /// This function returns an error in the following cases:\n    ///\n    /// - If any of the provided origins in `allow_origins` cannot be parsed as\n    ///   a valid URI, the function will return a parsing error.\n    /// - If any of the provided headers in `allow_headers` cannot be parsed as\n    ///   valid HTTP headers, the function will return a parsing error.\n    /// - If any of the provided methods in `allow_methods` cannot be parsed as\n    ///   valid HTTP methods, the function will return a parsing error.\n    ///\n    /// In all of these cases, the error returned will be the result of the\n    /// `parse` method of the corresponding type.\n    pub fn cors(&self) -> Result<cors::CorsLayer> {\n        let mut cors: cors::CorsLayer = cors::CorsLayer::new();\n\n        // testing CORS, assuming https://example.com in the allow list:\n        // $ curl -v --request OPTIONS 'localhost:5150/api/_ping' -H 'Origin: https://example.com' -H 'Acces\n        // look for '< access-control-allow-origin: https://example.com' in response.\n        // if it doesn't appear (test with a bogus domain), it is not allowed.\n        if self.allow_origins == default_allow_origins() {\n            cors = cors.allow_origin(Any);\n        } else {\n            let mut list = vec![];\n            for origin in &self.allow_origins {\n                list.push(origin.parse()?);\n            }\n            if !list.is_empty() {\n                cors = cors.allow_origin(list);\n            }\n        }\n\n        if self.allow_headers == default_allow_headers() {\n            cors = cors.allow_headers(Any);\n        } else {\n            let mut list = vec![];\n            for header in &self.allow_headers {\n                list.push(header.parse()?);\n            }\n            if !list.is_empty() {\n                cors = cors.allow_headers(list);\n            }\n        }\n\n        if self.allow_methods == default_allow_methods() {\n            cors = cors.allow_methods(Any);\n        } else {\n            let mut list = vec![];\n            for method in &self.allow_methods {\n                list.push(method.parse()?);\n            }\n            if !list.is_empty() {\n                cors = cors.allow_methods(list);\n            }\n        }\n\n        if self.expose_headers != default_expose_headers() {\n            let mut list = vec![];\n            for method in &self.expose_headers {\n                list.push(method.parse()?);\n            }\n            if !list.is_empty() {\n                cors = cors.expose_headers(list);\n            }\n        }\n\n        let mut list = vec![];\n        for v in &self.vary {\n            list.push(v.parse()?);\n        }\n        if !list.is_empty() {\n            cors = cors.vary(list);\n        }\n\n        if let Some(max_age) = self.max_age {\n            cors = cors.max_age(Duration::from_secs(max_age));\n        }\n\n        cors = cors.allow_credentials(self.allow_credentials);\n\n        Ok(cors)\n    }\n}\n\nimpl MiddlewareLayer for Cors {\n    /// Returns the name of the middleware\n    fn name(&self) -> &'static str {\n        \"cors\"\n    }\n\n    /// Returns whether the middleware is enabled or not\n    fn is_enabled(&self) -> bool {\n        self.enable\n    }\n\n    fn config(&self) -> serde_json::Result<serde_json::Value> {\n        serde_json::to_value(self)\n    }\n\n    /// Applies the CORS middleware layer to the Axum router.\n    fn apply(&self, app: AXRouter<AppContext>) -> Result<AXRouter<AppContext>> {\n        Ok(app.layer(self.cors()?))\n    }\n}\n\n#[cfg(test)]\nmod tests {\n\n    use axum::{\n        body::Body,\n        http::{Method, Request},\n        routing::get,\n        Router,\n    };\n    use insta::assert_debug_snapshot;\n    use rstest::rstest;\n    use tower::ServiceExt;\n\n    use super::*;\n    use crate::tests_cfg;\n\n    #[rstest]\n    #[case(\"default\", None, None, None, None)]\n    #[case(\"with_allow_headers\", Some(vec![\"token\".to_string(), \"user\".to_string()]), None, None, None)]\n    #[case(\"with_expose_headers\", None, None, Some(vec![\"token\".to_string(), \"user\".to_string()]), None)]\n    #[case(\"with_allow_methods\", None, Some(vec![\"post\".to_string(), \"get\".to_string()]), None, None)]\n    #[case(\"with_max_age\", None, None, None, Some(20))]\n    #[tokio::test]\n    async fn cors_enabled(\n        #[case] test_name: &str,\n        #[case] allow_headers: Option<Vec<String>>,\n        #[case] allow_methods: Option<Vec<String>>,\n        #[case] expose_headers: Option<Vec<String>>,\n        #[case] max_age: Option<u64>,\n    ) {\n        let mut middleware = Cors::default();\n        if let Some(allow_headers) = allow_headers {\n            middleware.allow_headers = allow_headers;\n        }\n        if let Some(allow_methods) = allow_methods {\n            middleware.allow_methods = allow_methods;\n        }\n        if let Some(expose_headers) = expose_headers {\n            middleware.expose_headers = expose_headers;\n        }\n        middleware.max_age = max_age;\n\n        let app = Router::new().route(\"/\", get(|| async {}));\n        let app = middleware\n            .apply(app)\n            .expect(\"apply middleware\")\n            .with_state(tests_cfg::app::get_app_context().await);\n\n        let req = Request::builder()\n            .uri(\"/\")\n            .method(Method::GET)\n            .body(Body::empty())\n            .expect(\"request\");\n\n        let response = app.oneshot(req).await.expect(\"valid response\");\n\n        assert_debug_snapshot!(\n            format!(\"cors_[{test_name}]\"),\n            (\n                format!(\n                    \"access-control-allow-origin: {:?}\",\n                    response.headers().get(\"access-control-allow-origin\")\n                ),\n                format!(\"vary: {:?}\", response.headers().get(\"vary\")),\n                format!(\n                    \"access-control-allow-methods: {:?}\",\n                    response.headers().get(\"access-control-allow-methods\")\n                ),\n                format!(\n                    \"access-control-allow-headers: {:?}\",\n                    response.headers().get(\"access-control-allow-headers\")\n                ),\n                format!(\n                    \"access-control-expose-headers: {:?}\",\n                    response.headers().get(\"access-control-expose-headers\")\n                ),\n                format!(\"allow: {:?}\", response.headers().get(\"allow\")),\n            )\n        );\n    }\n\n    #[tokio::test]\n    async fn cors_options() {\n        let middleware = Cors {\n            allow_origins: vec![\n                \"http://localhost:8080\".to_string(),\n                \"http://example.com\".to_string(),\n            ],\n            ..Cors::default()\n        };\n        let app = Router::new().route(\"/\", get(|| async {}));\n        let app = middleware\n            .apply(app)\n            .expect(\"apply middleware\")\n            .with_state(tests_cfg::app::get_app_context().await);\n\n        let req = Request::builder()\n            .uri(\"/\")\n            .header(\"Origin\", \"http://example.com\")\n            .method(Method::OPTIONS)\n            .body(Body::empty())\n            .expect(\"request\");\n\n        let response = app.oneshot(req).await.expect(\"valid response\");\n\n        assert_debug_snapshot!(\n            format!(\"cors_OPTIONS_[allow_origins]\"),\n            (\n                format!(\n                    \"access-control-allow-origin: {:?}\",\n                    response.headers().get(\"access-control-allow-origin\")\n                ),\n                format!(\"vary: {:?}\", response.headers().get(\"vary\")),\n                format!(\n                    \"access-control-allow-methods: {:?}\",\n                    response.headers().get(\"access-control-allow-methods\")\n                ),\n                format!(\n                    \"access-control-allow-headers: {:?}\",\n                    response.headers().get(\"access-control-allow-headers\")\n                ),\n                format!(\n                    \"access-control-expose-headers: {:?}\",\n                    response.headers().get(\"access-control-expose-headers\")\n                ),\n                format!(\"allow: {:?}\", response.headers().get(\"allow\")),\n            )\n        );\n    }\n    #[test]\n    fn should_be_disabled() {\n        let middleware = Cors::default();\n        assert!(!middleware.is_enabled());\n    }\n}\n"
  },
  {
    "path": "src/controller/middleware/etag.rs",
    "content": "//! `ETag` Middleware for Caching Requests\n//!\n//! This middleware implements the [ETag](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag)\n//! HTTP header for caching responses in Axum. `ETags` are used to validate\n//! cache entries by comparing a client's stored `ETag` with the one generated\n//! by the server. If the `ETags` match, a `304 Not Modified` response is sent,\n//! avoiding the need to resend the full content.\n\nuse std::task::{Context, Poll};\n\nuse axum::{\n    body::Body,\n    extract::Request,\n    http::{\n        header::{ETAG, IF_NONE_MATCH},\n        StatusCode,\n    },\n    response::Response,\n    Router as AXRouter,\n};\nuse futures_util::future::BoxFuture;\nuse serde::{Deserialize, Serialize};\nuse tower::{Layer, Service};\n\nuse crate::{app::AppContext, controller::middleware::MiddlewareLayer, Result};\n\n#[derive(Debug, Clone, Deserialize, Serialize)]\npub struct Etag {\n    #[serde(default)]\n    pub enable: bool,\n}\n\nimpl MiddlewareLayer for Etag {\n    /// Returns the name of the middleware\n    fn name(&self) -> &'static str {\n        \"etag\"\n    }\n\n    /// Returns whether the middleware is enabled or not\n    fn is_enabled(&self) -> bool {\n        self.enable\n    }\n\n    fn config(&self) -> serde_json::Result<serde_json::Value> {\n        serde_json::to_value(self)\n    }\n\n    /// Applies the `ETag` middleware to the application router.\n    fn apply(&self, app: AXRouter<AppContext>) -> Result<AXRouter<AppContext>> {\n        Ok(app.layer(EtagLayer))\n    }\n}\n\n/// [`EtagLayer`] struct for adding `ETag` functionality as a Tower service\n/// layer.\n#[derive(Default, Clone)]\nstruct EtagLayer;\n\nimpl<S> Layer<S> for EtagLayer {\n    type Service = EtagMiddleware<S>;\n\n    fn layer(&self, inner: S) -> Self::Service {\n        EtagMiddleware { inner }\n    }\n}\n\n#[derive(Clone)]\nstruct EtagMiddleware<S> {\n    inner: S,\n}\n\nimpl<S> Service<Request<Body>> for EtagMiddleware<S>\nwhere\n    S: Service<Request, Response = Response> + Send + 'static,\n    S::Future: Send + 'static,\n{\n    type Response = S::Response;\n    type Error = S::Error;\n    // `BoxFuture` is a type alias for `Pin<Box<dyn Future + Send + 'a>>`\n    type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;\n\n    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {\n        self.inner.poll_ready(cx)\n    }\n\n    fn call(&mut self, request: Request) -> Self::Future {\n        let ifnm = request.headers().get(IF_NONE_MATCH).cloned();\n\n        let future = self.inner.call(request);\n\n        let res_fut = async move {\n            let response = future.await?;\n            let etag_from_response = response.headers().get(ETAG).cloned();\n            if let Some(etag_in_request) = ifnm {\n                if let Some(etag_from_response) = etag_from_response {\n                    if etag_in_request == etag_from_response {\n                        return Ok(Response::builder()\n                            .status(StatusCode::NOT_MODIFIED)\n                            .body(Body::empty())\n                            .unwrap());\n                    }\n                }\n            }\n            Ok(response)\n        };\n        Box::pin(res_fut)\n    }\n}\n"
  },
  {
    "path": "src/controller/middleware/fallback.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>Welcome to Loco!</title>\n    <link rel=\"icon\" type=\"image/svg+xml\" href=\"data:image/svg+xml,%3Csvg width='64' height='64' viewBox='0 0 64 64' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cmask id='mask0_293_2' style='mask-type:alpha' maskUnits='userSpaceOnUse' x='0' y='0' width='64' height='64'%3E%3Ccircle cx='32' cy='32' r='32' fill='white'/%3E%3C/mask%3E%3Cg mask='url(%23mask0_293_2)'%3E%3Ccircle cx='32' cy='32' r='19.5' fill='white' stroke='white' stroke-width='25'/%3E%3Cpath d='M32.0763 27.5835C38.7519 27.5835 44.1635 22.1719 44.1635 15.4963C44.1635 8.82079 38.7519 3.40918 32.0763 3.40918C25.4007 3.40918 19.9891 8.82079 19.9891 15.4963C19.9891 22.1719 25.4007 27.5835 32.0763 27.5835Z' fill='%23FC3820'/%3E%3Cpath d='M2.01355 46.5963L45.8696 41.0655L44.9398 45.9468L2.6863 51.7803L2.01355 46.5963Z' fill='white'/%3E%3Cpath d='M44.6292 32.4648L37.6099 30.9014L-4.91905 43.1285C-5.1407 44.6833 -4.80821 51.6623 -4.80821 51.6623C-4.80821 51.6623 -4.01285 51.7178 -3.89224 52.461C-3.84335 52.7543 -3.80098 53.0738 -3.76187 53.3509L-3.55977 53.315C-3.55977 53.315 -2.69269 54.4103 -1.55506 54.3255C-0.417438 54.2408 -0.150146 53.6736 -0.150146 53.6736C-0.150146 53.6736 0.739745 54.0484 2.21312 53.8137C3.68649 53.579 4.2602 52.8912 4.2602 52.8912C4.2602 52.8912 6.31378 53.4226 7.91101 53.1259C9.50825 52.8261 11.8063 50.955 11.8063 50.955L16.6046 50.5117C16.6046 50.5117 16.66 51.6493 18.9352 51.2288C21.2105 50.8083 21.1551 50.3748 21.3213 50.1629C21.3213 50.1629 22.0124 50.2248 22.9577 50.0586C23.8508 49.8989 24.5679 49.5729 24.8124 49.5273C25.0569 49.4816 25.1319 50.1173 27.1463 49.9021C29.477 49.6544 29.4607 48.5722 30.0572 48.5722C30.6537 48.5722 31.1427 49.0155 32.7791 49.0155C34.4154 49.0155 35.1684 47.6431 35.1684 47.6431L36.2832 47.441C36.2832 47.441 36.3484 47.4443 36.4657 47.4508C35.465 46.5218 33.2191 43.5816 33.2191 43.5816L-4.59308 50.5736V50.0455L33.6918 42.9427L37.4912 47.2246L45.1993 46.7816L46.5662 42.9427L44.6292 32.4648Z' fill='%237A1307'/%3E%3Cpath d='M37.9804 31.7055V31.0829H38.2119V30.2582C38.2119 29.9779 37.9837 29.7529 37.7066 29.7529H37.5404C37.2601 29.7529 37.0352 29.9811 37.0352 30.2582V31.0959H37.6252V31.9271L37.9804 31.7055Z' fill='%237A1307'/%3E%3Cpath d='M38.02 25.0777C37.6902 24.7686 37.4239 24.5555 37.4239 24.5555C37.7443 24.0779 38.2132 22.8909 37.1083 21.945C36.0033 20.9991 34.6982 21.5072 34.6982 21.5072C34.6982 21.5072 35.0798 18.721 32.5732 18.0795C30.0666 17.4379 28.7025 20.1726 28.7025 20.1726C28.1206 18.5992 30.1066 17.5831 30.1066 17.5831C30.1066 17.5831 28.8627 15.511 25.9555 15.6305C23.0484 15.7499 22.1249 19.0979 22.1249 19.0979C20.8009 16.7075 23.1474 15.3729 23.1474 15.3729C23.1474 15.3729 21.6231 14.0571 18.7772 15.0334C15.929 16.0097 15.1727 19.6364 15.1727 19.6364C14.2751 16.8269 16.5321 15.4712 16.5321 15.4712C15.2882 14.2561 12.202 14.235 9.87436 16.1292C7.54911 18.0233 8.41137 22.3055 8.41137 22.3055C8.41137 22.3055 7.30882 21.1489 6.94837 19.4351C6.58792 17.7212 7.78942 16.8643 7.78942 16.8643C3.93992 14.214 0.530971 21.6874 0.530971 21.6874C-0.0297263 18.0022 3.59831 16.0074 3.59831 16.0074C3.59831 16.0074 3.45697 15.7288 1.47332 15.9676C-0.51268 16.2064 -2.35733 17.0235 -2.35733 17.0235C-2.29608 16.7051 -1.69534 16.087 -1.05454 15.785C-0.413743 15.483 -0.352481 14.0547 -2.23718 13.2985C-4.12188 12.5423 -5.38462 13.5584 -5.38462 13.5584C-5.04302 12.2636 -4.05356 12.0178 -4.30093 11.2264C-4.54829 10.4351 -5.16318 9.87082 -5.16318 9.87082C-5.16318 9.87082 -4.11717 9.66947 -4.14073 8.8945C-4.16429 8.11952 -4.86162 8.01885 -4.86162 8.01885C-4.86162 8.01885 -3.34445 7.48269 -2.99814 6.93014C-2.85443 6.70069 -2.79552 6.45017 -2.95572 6.2652C-3.16775 6.19731 -3.46224 6.17624 -3.86038 6.2254C-6.10553 6.50402 -8.15042 11.7439 -8.21167 13.837C-8.27292 15.9301 -6.66857 16.7075 -6.66857 16.7075C-6.66857 16.7075 -7.38948 18.8404 -5.68618 21.7694C-3.98288 24.6984 0.448507 22.727 0.448507 22.727C0.288308 24.1622 1.36966 28.4866 4.83986 28.407C8.3077 28.3274 9.93325 26.1547 9.93325 26.1547C9.93325 26.1547 11.7567 27.7093 14.2233 27.529C16.6899 27.3487 17.8325 25.1783 17.8325 25.1783C17.8325 25.1783 18.5746 26.075 20.5582 26.1149C22.5442 26.1547 23.8871 24.2629 23.8871 24.2629C23.8871 24.2629 24.509 25.6185 26.4126 25.6372C28.3185 25.6583 29.421 24.1833 29.421 24.1833C29.421 24.1833 30.0618 25.1807 31.0913 25.8573C32.1209 26.5339 34.5945 25.9369 34.5945 25.9369C34.2341 26.8734 35.8479 28.6247 36.9999 28.5077C39.0707 28.2876 38.3922 26.0165 38.02 25.0777Z' fill='%23B80000'/%3E%3Cpath d='M38.2168 22.4194C37.6322 20.9182 35.5188 20.7771 35.5188 20.7771C35.5188 20.7771 35.862 19.0547 34.5319 17.7724C33.2019 16.4901 30.8471 17.0124 30.8471 17.0124C30.8471 17.0124 30.2625 15.1913 28.1491 14.3889C26.0334 13.5889 23.9389 14.7889 23.9389 14.7889C20.5546 11.6643 17.2319 14.3489 17.2319 14.3489C17.2319 14.3489 15.2179 12.4054 12.2762 12.9678C9.33445 13.5278 8.08725 15.8713 8.08725 15.8713C6.43534 14.5701 4.32194 15.6501 4.32194 15.6501C3.33505 13.7466 0.232414 15.1701 0.232414 15.1701C0.232414 15.1701 0.393332 14.6901 -0.652717 13.4878C-1.69877 12.2854 -3.93523 11.9066 -3.93523 11.9066C-3.93523 11.9066 -3.75298 11.7066 -3.83345 10.9043C-3.91391 10.1043 -4.37778 9.78196 -4.37778 9.78196C-4.37778 9.78196 -3.79322 9.48078 -3.79322 8.86197C-3.79322 8.2408 -4.23578 8.00081 -4.23578 8.00081C-2.87733 7.86904 -1.99695 6.51613 -2.98384 6.19849C-2.82291 6.38201 -2.88207 6.63613 -3.02643 6.86671C-3.37433 7.42199 -4.89843 7.96081 -4.89843 7.96081C-4.89843 7.96081 -4.19554 8.06434 -4.17424 8.84079C-4.15058 9.61961 -5.20136 9.82195 -5.20136 9.82195C-5.20136 9.82195 -4.58368 10.389 -4.33519 11.1843C-4.08669 11.9796 -5.08067 12.2266 -5.42383 13.5278C-5.42383 13.5278 -4.15532 12.5066 -2.26202 13.2666C-0.368719 14.0266 -0.430246 15.4619 -1.07397 15.7654C-1.71769 16.0689 -2.32355 16.6901 -2.38272 17.0101C-2.38272 17.0101 -0.529649 16.1889 1.46542 15.9489C3.46048 15.7089 3.60012 15.9889 3.60012 15.9889C3.60012 15.9889 -0.0444908 17.9912 0.518766 21.697C0.518766 21.697 3.94327 14.1889 7.81034 16.8501C7.81034 16.8501 6.60099 17.7112 6.96545 19.4336C7.32755 21.1559 8.43513 22.3182 8.43513 22.3182C8.43513 22.3182 7.56895 18.0124 9.90481 16.1113C12.2407 14.2078 15.3433 14.2289 16.5929 15.4501C16.5929 15.4501 14.3257 16.8124 15.2274 19.6359C15.2274 19.6359 15.9894 15.9913 18.8483 15.0101C21.7095 14.0289 23.2384 15.3513 23.2384 15.3513C23.2384 15.3513 20.8812 16.6924 22.2113 19.0947C22.2113 19.0947 23.1366 15.7301 26.0594 15.6101C28.9798 15.4901 30.2294 17.5724 30.2294 17.5724C30.2294 17.5724 28.2343 18.5936 28.8189 20.1747C28.8189 20.1747 30.1892 17.4265 32.7072 18.0712C35.2253 18.7159 34.8419 21.5159 34.8419 21.5159C34.8419 21.5159 36.1531 21.0053 37.263 21.9559C38.373 22.9064 37.902 24.0994 37.5801 24.5794C37.5801 24.5794 37.8476 24.7935 38.1789 25.1041C38.0795 24.857 38.0038 24.6994 38.0038 24.6994C38.0038 24.6994 38.8013 23.9205 38.2168 22.4194Z' fill='white'/%3E%3Cpath d='M72.6722 42.1285L4.85703 51.3971L36.493 85.586L72.6722 42.1285Z' fill='%23BD210F'/%3E%3Cpath d='M73.9711 45.1103L5.91677 52.419L36.5534 87.5062L73.9711 45.1103Z' fill='%23D45546'/%3E%3Cpath d='M43.0016 35.8115C43.1301 36.59 42.8316 37.2875 42.335 37.3694C41.8384 37.4514 41.3317 36.8867 41.2033 36.1083C41.0748 35.3298 41.3733 34.6324 41.8699 34.5504C42.3665 34.4685 42.8732 35.0331 43.0016 35.8115Z' fill='white'/%3E%3C/g%3E%3C/svg%3E%0A\">\n    <link href=\"https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css\" rel=\"stylesheet\">\n</head>\n\n<body class=\"bg-gray-100 text-gray-900\">\n    <div class=\"min-h-screen flex flex-col justify-center items-center py-8\">\n        <div class=\"bg-white shadow-md rounded-lg p-8 max-w-xl text-center\">\n            <svg class=\"mx-auto my-4\" width=\"113\" height=\"\" viewBox=\"0 0 413 413\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n              <mask id=\"mask0_201_516\" style=\"mask-type:alpha\" maskUnits=\"userSpaceOnUse\" x=\"0\" y=\"0\" width=\"413\" height=\"413\">\n              <circle cx=\"206.5\" cy=\"206.5\" r=\"206.5\" fill=\"white\"/>\n              </mask>\n              <g mask=\"url(#mask0_201_516)\">\n              <circle cx=\"206.5\" cy=\"206.5\" r=\"194\" fill=\"white\" stroke=\"white\" stroke-width=\"25\"/>\n              <path d=\"M207 178C250.078 178 285 143.078 285 100C285 56.9218 250.078 22 207 22C163.922 22 129 56.9218 129 100C129 143.078 163.922 178 207 178Z\" fill=\"#FC3820\"/>\n              <path d=\"M12.9917 300.691L296 265L290 296.5L17.3331 334.144L12.9917 300.691Z\" fill=\"white\"/>\n              <path d=\"M288 209.5L242.704 199.411L-31.741 278.314C-33.1714 288.348 -31.0257 333.384 -31.0257 333.384C-31.0257 333.384 -25.8932 333.742 -25.1149 338.538C-24.7994 340.431 -24.526 342.492 -24.2736 344.28L-22.9694 344.049C-22.9694 344.049 -17.374 351.117 -10.0328 350.57C-2.69154 350.023 -0.966669 346.363 -0.966669 346.363C-0.966669 346.363 4.77591 348.782 14.2838 347.267C23.7916 345.753 27.4938 341.314 27.4938 341.314C27.4938 341.314 40.7458 344.743 51.053 342.829C61.3602 340.894 76.1899 328.819 76.1899 328.819L107.154 325.959C107.154 325.959 107.511 333.3 122.194 330.586C136.876 327.873 136.519 325.075 137.591 323.708C137.591 323.708 142.051 324.108 148.151 323.035C153.915 322.004 158.542 319.9 160.12 319.606C161.698 319.311 162.181 323.413 175.181 322.025C190.221 320.426 190.116 313.443 193.965 313.443C197.815 313.443 200.97 316.303 211.53 316.303C222.089 316.303 226.948 307.448 226.948 307.448L234.142 306.143C234.142 306.143 234.563 306.165 235.32 306.207C228.863 300.212 214.369 281.238 214.369 281.238L-29.6375 326.358V322.951L217.419 277.115L241.938 304.746L291.679 301.888L300.5 277.115L288 209.5Z\" fill=\"#7A1307\"/>\n              <path d=\"M245.1 204.6V200.582H246.594V195.26C246.594 193.451 245.121 192 243.333 192H242.26C240.451 192 239 193.473 239 195.26V200.667H242.807V206.031L245.1 204.6Z\" fill=\"#7A1307\"/>\n              <path d=\"M245.35 161.83C243.221 159.835 241.503 158.46 241.503 158.46C243.571 155.378 246.596 147.718 239.466 141.614C232.336 135.51 223.914 138.789 223.914 138.789C223.914 138.789 226.377 120.809 210.201 116.669C194.025 112.53 185.223 130.177 185.223 130.177C181.468 120.024 194.284 113.466 194.284 113.466C194.284 113.466 186.257 100.095 167.496 100.866C148.736 101.636 142.777 123.242 142.777 123.242C134.233 107.816 149.375 99.2037 149.375 99.2037C149.375 99.2037 139.539 90.7126 121.174 97.0129C102.794 103.313 97.9134 126.717 97.9134 126.717C92.1211 108.586 106.685 99.8383 106.685 99.8383C98.6583 91.9968 78.7427 91.8608 63.7224 104.084C48.7173 116.307 54.2816 143.941 54.2816 143.941C54.2816 143.941 47.1667 136.477 44.8406 125.417C42.5146 114.358 50.268 108.828 50.268 108.828C25.4268 91.7248 3.42839 139.952 3.42839 139.952C-0.189865 116.171 23.2223 103.298 23.2223 103.298C23.2223 103.298 22.3102 101.5 9.50948 103.041C-3.30643 104.582 -15.2102 109.855 -15.2102 109.855C-14.8149 107.801 -10.9383 103.812 -6.80312 101.863C-2.66797 99.9138 -2.27264 90.6974 -14.4348 85.8173C-26.597 80.9372 -34.7457 87.4944 -34.7457 87.4944C-32.5413 79.1392 -26.1562 77.5528 -27.7524 72.446C-29.3487 67.3393 -33.3167 63.6981 -33.3167 63.6981C-33.3167 63.6981 -26.5666 62.3987 -26.7187 57.3977C-26.8707 52.3967 -31.3707 51.747 -31.3707 51.747C-31.3707 51.747 -21.5802 48.2871 -19.3454 44.7214C-18.418 43.2408 -18.0379 41.6241 -19.0717 40.4305C-20.4399 39.9924 -22.3403 39.8564 -24.9095 40.1737C-39.3978 41.9716 -52.5937 75.7851 -52.989 89.2923C-53.3842 102.8 -43.0312 107.816 -43.0312 107.816C-43.0312 107.816 -47.6832 121.58 -36.6917 140.481C-25.7001 159.382 2.89624 146.66 2.89624 146.66C1.86245 155.922 8.84053 183.828 31.2342 183.314C53.6126 182.801 64.1025 168.78 64.1025 168.78C64.1025 168.78 75.8694 178.812 91.7867 177.648C107.704 176.485 115.077 162.479 115.077 162.479C115.077 162.479 119.866 168.266 132.667 168.523C145.483 168.78 154.148 156.572 154.148 156.572C154.148 156.572 158.162 165.32 170.446 165.441C182.745 165.577 189.86 156.058 189.86 156.058C189.86 156.058 193.995 162.494 200.638 166.861C207.282 171.227 223.245 167.374 223.245 167.374C220.919 173.418 231.333 184.719 238.767 183.964C252.13 182.544 247.752 167.888 245.35 161.83Z\" fill=\"#B80000\"/>\n              <path d=\"M246.618 144.676C242.846 134.988 229.208 134.077 229.208 134.077C229.208 134.077 231.422 122.963 222.839 114.688C214.257 106.413 199.061 109.784 199.061 109.784C199.061 109.784 195.288 98.0316 181.65 92.854C167.997 87.6916 154.481 95.4352 154.481 95.4352C132.642 75.2714 111.2 92.5959 111.2 92.5959C111.2 92.5959 98.2035 80.0543 79.2203 83.6831C60.237 87.2968 52.1887 102.42 52.1887 102.42C41.5287 94.0232 27.8906 100.992 27.8906 100.992C21.5222 88.7089 1.50043 97.895 1.50043 97.895C1.50043 97.895 2.53886 94.7975 -4.21142 87.0387C-10.9617 79.2799 -25.3939 76.8353 -25.3939 76.8353C-25.3939 76.8353 -24.2178 75.5447 -24.7371 70.3671C-25.2563 65.2047 -28.2497 63.1246 -28.2497 63.1246C-28.2497 63.1246 -24.4775 61.1811 -24.4775 57.1878C-24.4775 53.1793 -27.3334 51.6306 -27.3334 51.6306C-18.5672 50.7803 -12.886 42.0498 -19.2544 40C-18.2159 41.1843 -18.5977 42.8241 -19.5293 44.3121C-21.7743 47.8955 -31.6095 51.3725 -31.6095 51.3725C-31.6095 51.3725 -27.0737 52.0406 -26.9363 57.0511C-26.7835 62.0769 -33.5644 63.3827 -33.5644 63.3827C-33.5644 63.3827 -29.5784 67.0419 -27.9749 72.174C-26.3713 77.306 -32.7855 78.9003 -35 87.2968C-35 87.2968 -26.8142 80.7072 -14.5965 85.6114C-2.37875 90.5157 -2.77579 99.7777 -6.92981 101.736C-11.0838 103.695 -14.9935 107.704 -15.3753 109.769C-15.3753 109.769 -3.41725 104.469 9.45716 102.921C22.3316 101.372 23.2327 103.179 23.2327 103.179C23.2327 103.179 -0.286465 116.1 3.3483 140.014C3.3483 140.014 25.4471 91.5634 50.4017 108.736C50.4017 108.736 42.5977 114.293 44.9496 125.408C47.2862 136.522 54.4336 144.023 54.4336 144.023C54.4336 144.023 48.844 116.237 63.9176 103.968C78.9912 91.6849 99.013 91.8215 107.077 99.7018C107.077 99.7018 92.446 108.493 98.2646 126.713C98.2646 126.713 103.182 103.194 121.631 96.8625C140.095 90.5309 149.961 99.0641 149.961 99.0641C149.961 99.0641 134.75 107.719 143.333 123.221C143.333 123.221 149.304 101.509 168.165 100.734C187.011 99.9599 195.075 113.397 195.075 113.397C195.075 113.397 182.2 119.987 185.973 130.19C185.973 130.19 194.815 112.456 211.065 116.616C227.314 120.777 224.84 138.845 224.84 138.845C224.84 138.845 233.301 135.55 240.463 141.684C247.626 147.819 244.587 155.517 242.51 158.614C242.51 158.614 244.236 159.996 246.374 162C245.732 160.406 245.244 159.388 245.244 159.388C245.244 159.388 250.39 154.363 246.618 144.676Z\" fill=\"white\"/>\n              <path d=\"M468.968 271.861L31.3486 331.673L235.5 552.298L468.968 271.861Z\" fill=\"#BD210F\"/>\n              <path d=\"M477.337 291.102L38.1737 338.266L235.876 564.688L477.337 291.102Z\" fill=\"#D45546\"/>\n              <circle cx=\"206.5\" cy=\"206.5\" r=\"200\" stroke=\"white\" stroke-width=\"13\"/>\n              <path d=\"M277.501 231.096C278.33 236.119 276.404 240.62 273.199 241.149C269.995 241.678 266.725 238.034 265.896 233.011C265.067 227.987 266.993 223.486 270.198 222.957C273.402 222.429 276.672 226.072 277.501 231.096Z\" fill=\"white\"/>\n              </g>\n            </svg>\n\n            <h1 class=\"text-3xl font-bold mb-4\">Welcome to Loco!</h1>\n            <p class=\"mb-12\">It looks like you've just started your Loco server, and this is the fallback page displayed when a route doesn't exist.</p>\n            \n            <div class=\"text-left space-y-4\">\n                <div>\n                    <h3 class=\"text-xl font-semibold\">Remove this Fallback Page</h3>\n                    <p>To remove this fallback page, adjust the configuration in your <code class=\"bg-gray-200 rounded px-2 py-1\">config/development.yaml</code> file. Look for the <code class=\"bg-gray-200 rounded px-2 py-1\">fallback:</code> setting and disable or customize it.</p>\n                </div>\n                <div>\n                    <h3 class=\"text-xl font-semibold\">Scaffold Your Application</h3>\n                    <p>Use the Loco CLI to scaffold your application:</p>\n<pre class=\"my-1\"><code class=\"bg-gray-200 rounded px-2 py-1\">cargo loco generate scaffold movie title:string</code></pre>\n                      <p>This creates models, controllers, and views.</p>\n                </div>\n            </div>\n\n            <div class=\"mt-8\">\n                <h3 class=\"text-xl font-semibold\">Need More Help?</h3>\n                <p>If you need further assistance, check out the <a href=\"https://loco.rs/docs\" class=\"text-blue-500 hover:underline\">Loco documentation</a> or post on our <a href=\"https://github.com/loco-rs/loco/discussions\" class=\"text-blue-500 hover:underline\">discussions forum</a>.</p>\n            </div>\n        </div>\n    </div>\n\n</body>\n\n</html>\n"
  },
  {
    "path": "src/controller/middleware/fallback.rs",
    "content": "//! Fallback Middleware\n//!\n//! This middleware handles fallback logic for the application when routes do\n//! not match. It serves a file, a custom not-found message, or a default HTML\n//! fallback page based on the configuration.\n\nuse axum::{http::StatusCode, response::Html, Router as AXRouter};\nuse serde::{Deserialize, Deserializer, Serialize, Serializer};\nuse serde_json::json;\nuse tower_http::services::ServeFile;\n\nuse crate::{app::AppContext, controller::middleware::MiddlewareLayer, Result};\n\n#[derive(Debug)]\npub struct StatusCodeWrapper(pub StatusCode);\n\n#[derive(Debug, Clone, Deserialize, Serialize)]\npub struct Fallback {\n    /// By default when enabled, returns a prebaked 404 not found page optimized\n    /// for development. For production set something else (see fields below)\n    #[serde(default)]\n    pub enable: bool,\n    /// For the unlikely reason to return something different than `404`, you\n    /// can set it here\n    #[serde(\n        default = \"default_status_code\",\n        serialize_with = \"serialize_status_code\",\n        deserialize_with = \"deserialize_status_code\"\n    )]\n    pub code: StatusCode,\n    /// Returns content from a file pointed to by this field with a `404` status\n    /// code.\n    pub file: Option<String>,\n    /// Returns a \"404 not found\" with a single message string. This sets the\n    /// message.\n    pub not_found: Option<String>,\n}\n\nfn default_status_code() -> StatusCode {\n    StatusCode::OK\n}\n\nimpl Default for Fallback {\n    fn default() -> Self {\n        serde_json::from_value(json!({})).unwrap()\n    }\n}\n\nfn deserialize_status_code<'de, D>(de: D) -> Result<StatusCode, D::Error>\nwhere\n    D: Deserializer<'de>,\n{\n    let code: u16 = Deserialize::deserialize(de)?;\n    StatusCode::from_u16(code).map_or_else(\n        |_| {\n            Err(serde::de::Error::invalid_value(\n                serde::de::Unexpected::Unsigned(u64::from(code)),\n                &\"a value between 100 and 600\",\n            ))\n        },\n        Ok,\n    )\n}\n\n#[allow(clippy::trivially_copy_pass_by_ref)]\nfn serialize_status_code<S>(status: &StatusCode, ser: S) -> Result<S::Ok, S::Error>\nwhere\n    S: Serializer,\n{\n    ser.serialize_u16(status.as_u16())\n}\nimpl MiddlewareLayer for Fallback {\n    /// Returns the name of the middleware\n    fn name(&self) -> &'static str {\n        \"fallback\"\n    }\n\n    /// Returns whether the middleware is enabled or not\n    fn is_enabled(&self) -> bool {\n        self.enable\n    }\n\n    fn config(&self) -> serde_json::Result<serde_json::Value> {\n        serde_json::to_value(self)\n    }\n\n    /// Applies the fallback middleware to the application router.\n    fn apply(&self, app: AXRouter<AppContext>) -> Result<AXRouter<AppContext>> {\n        let app = if let Some(path) = &self.file {\n            app.fallback_service(ServeFile::new(path))\n        } else if let Some(not_found) = &self.not_found {\n            let not_found = not_found.clone();\n            let status_code = self.code;\n            app.fallback(move || async move { (status_code, not_found) })\n        } else {\n            let content = include_str!(\"fallback.html\");\n            let status_code = self.code;\n            app.fallback(move || async move { (status_code, Html(content)) })\n        };\n        Ok(app)\n    }\n}\n"
  },
  {
    "path": "src/controller/middleware/format.rs",
    "content": "//! Detect a content type and format and responds accordingly\nuse axum::{\n    extract::FromRequestParts,\n    http::{\n        header::{HeaderMap, ACCEPT, CONTENT_TYPE},\n        request::Parts,\n    },\n};\nuse serde::{Deserialize, Serialize};\n\nuse crate::Error;\n\n#[derive(Debug, Deserialize, Serialize)]\npub struct Format(pub RespondTo);\n\n#[derive(Debug, Deserialize, Serialize)]\npub enum RespondTo {\n    None,\n    Html,\n    Json,\n    Xml,\n    Other(String),\n}\n\nfn detect_format(content_type: &str) -> RespondTo {\n    if content_type.starts_with(\"application/json\") {\n        RespondTo::Json\n    } else if content_type.starts_with(\"text/html\") {\n        RespondTo::Html\n    } else if content_type.starts_with(\"text/xml\")\n        || content_type.starts_with(\"application/xml\")\n        || content_type.starts_with(\"application/xhtml\")\n    {\n        RespondTo::Xml\n    } else {\n        RespondTo::Other(content_type.to_string())\n    }\n}\n\npub fn get_respond_to(headers: &HeaderMap) -> RespondTo {\n    #[allow(clippy::option_if_let_else)]\n    if let Some(content_type) = headers.get(CONTENT_TYPE).and_then(|h| h.to_str().ok()) {\n        detect_format(content_type)\n    } else if let Some(content_type) = headers.get(ACCEPT).and_then(|h| h.to_str().ok()) {\n        detect_format(content_type)\n    } else {\n        RespondTo::None\n    }\n}\n\nimpl<S> FromRequestParts<S> for Format\nwhere\n    S: Send + Sync,\n{\n    type Rejection = Error;\n\n    async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Error> {\n        Ok(Self(get_respond_to(&parts.headers)))\n    }\n}\n\nimpl<S> FromRequestParts<S> for RespondTo\nwhere\n    S: Send + Sync,\n{\n    type Rejection = Error;\n\n    async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Error> {\n        Ok(get_respond_to(&parts.headers))\n    }\n}\n"
  },
  {
    "path": "src/controller/middleware/limit_payload.rs",
    "content": "//! Limit Payload Middleware\n//!\n//! This middleware restricts the maximum allowed size for HTTP request\n//! payloads. It is configurable based on the [`LimitPayloadMiddleware`]\n//! settings in the application's middleware configuration. The middleware sets\n//! a limit on the request body size using Axum's `DefaultBodyLimit` layer.\n//!\n//! # Note\n//!\n//! Ensure that the `body: axum::body::Bytes` variable is properly set in the\n//! request action to enforce the payload limit correctly. Without this, the\n//! middleware will not function as intended.\n\nuse axum::Router as AXRouter;\nuse serde::{Deserialize, Deserializer, Serialize};\n\nuse crate::{app::AppContext, controller::middleware::MiddlewareLayer, Result};\n\n#[derive(Debug, Clone, Copy, Deserialize, Serialize)]\npub enum DefaultBodyLimitKind {\n    Disable,\n    Limit(usize),\n}\n\n#[derive(Debug, Clone, Deserialize, Serialize)]\npub struct LimitPayload {\n    #[serde(\n        default = \"default_body_limit\",\n        deserialize_with = \"deserialize_body_limit\"\n    )]\n    pub body_limit: DefaultBodyLimitKind,\n}\n\nimpl Default for LimitPayload {\n    fn default() -> Self {\n        Self {\n            body_limit: default_body_limit(),\n        }\n    }\n}\n\n/// Returns the default body limit in bytes (2MB).\nfn default_body_limit() -> DefaultBodyLimitKind {\n    DefaultBodyLimitKind::Limit(2_000_000)\n}\n\nfn deserialize_body_limit<'de, D>(deserializer: D) -> Result<DefaultBodyLimitKind, D::Error>\nwhere\n    D: Deserializer<'de>,\n{\n    let s: String = String::deserialize(deserializer)?;\n\n    match s.as_str() {\n        \"disable\" => Ok(DefaultBodyLimitKind::Disable),\n        limit => {\n            let bytes = byte_unit::Byte::from_str(limit)\n                .map_err(|err| serde::de::Error::custom(err.to_string()))?\n                .get_bytes();\n            Ok(DefaultBodyLimitKind::Limit(bytes as usize))\n        }\n    }\n}\nimpl MiddlewareLayer for LimitPayload {\n    /// Returns the name of the middleware\n    fn name(&self) -> &'static str {\n        \"limit_payload\"\n    }\n\n    /// Returns whether the middleware is enabled or not\n    fn is_enabled(&self) -> bool {\n        true\n    }\n\n    fn config(&self) -> serde_json::Result<serde_json::Value> {\n        serde_json::to_value(self)\n    }\n\n    /// Applies the payload limit middleware to the application router by adding\n    /// a `DefaultBodyLimit` layer.\n    fn apply(&self, app: AXRouter<AppContext>) -> Result<AXRouter<AppContext>> {\n        let body_limit_layer = match self.body_limit {\n            DefaultBodyLimitKind::Disable => axum::extract::DefaultBodyLimit::disable(),\n            DefaultBodyLimitKind::Limit(limit) => axum::extract::DefaultBodyLimit::max(limit),\n        };\n\n        Ok(app.layer(body_limit_layer))\n    }\n}\n"
  },
  {
    "path": "src/controller/middleware/logger.rs",
    "content": "//! Logger Middleware\n//!\n//! This middleware provides logging functionality for HTTP requests. It uses\n//! `TraceLayer` to log detailed information about each request, such as the\n//! HTTP method, URI, version, user agent, and an associated request ID.\n//! Additionally, it integrates the application's runtime environment\n//! into the log context, allowing environment-specific logging (e.g.,\n//! \"development\", \"production\").\n\nuse axum::{http, Router as AXRouter};\nuse serde::{Deserialize, Serialize};\nuse tower_http::{add_extension::AddExtensionLayer, trace::TraceLayer};\n\nuse crate::{\n    app::AppContext,\n    controller::middleware::{request_id::LocoRequestId, MiddlewareLayer},\n    environment::Environment,\n    Result,\n};\n\n#[derive(Debug, Clone, Deserialize, Serialize)]\npub struct Config {\n    #[serde(default)]\n    pub enable: bool,\n}\n\n/// [`Middleware`] struct responsible for logging HTTP requests.\n#[derive(Serialize, Debug)]\npub struct Middleware {\n    config: Config,\n    environment: Environment,\n}\n\n/// Creates a new instance of [`Middleware`] by cloning the [`Config`]\n/// configuration.\n#[must_use]\npub fn new(config: &Config, environment: &Environment) -> Middleware {\n    Middleware {\n        config: config.clone(),\n        environment: environment.clone(),\n    }\n}\n\nimpl MiddlewareLayer for Middleware {\n    /// Returns the name of the middleware\n    fn name(&self) -> &'static str {\n        \"logger\"\n    }\n\n    /// Returns whether the middleware is enabled or not\n    fn is_enabled(&self) -> bool {\n        self.config.enable\n    }\n\n    fn config(&self) -> serde_json::Result<serde_json::Value> {\n        serde_json::to_value(self)\n    }\n\n    /// Applies the logger middleware to the application router by adding layers\n    /// for:\n    ///\n    /// - `TraceLayer`: Logs detailed information about each HTTP request.\n    /// - `AddExtensionLayer`: Adds the current environment to the request\n    ///   extensions, making it accessible to the `TraceLayer` for logging.\n    ///\n    /// The `TraceLayer` is customized with `make_span_with` to extract\n    /// request-specific details like method, URI, version, user agent, and\n    /// request ID, then create a tracing span for the request.\n    fn apply(&self, app: AXRouter<AppContext>) -> Result<AXRouter<AppContext>> {\n        Ok(app\n            .layer(\n                TraceLayer::new_for_http().make_span_with(|request: &http::Request<_>| {\n                    let ext = request.extensions();\n                    let request_id = ext\n                        .get::<LocoRequestId>()\n                        .map_or_else(|| \"req-id-none\".to_string(), |r| r.get().to_string());\n                    let user_agent = request\n                        .headers()\n                        .get(axum::http::header::USER_AGENT)\n                        .map_or(\"\", |h| h.to_str().unwrap_or(\"\"));\n\n                    let env: String = request\n                        .extensions()\n                        .get::<Environment>()\n                        .map(std::string::ToString::to_string)\n                        .unwrap_or_default();\n\n                    tracing::error_span!(\n                        \"http-request\",\n                        \"http.method\" = tracing::field::display(request.method()),\n                        \"http.uri\" = tracing::field::display(request.uri()),\n                        \"http.version\" = tracing::field::debug(request.version()),\n                        \"http.user_agent\" = tracing::field::display(user_agent),\n                        \"environment\" = tracing::field::display(env),\n                        request_id = tracing::field::display(request_id),\n                    )\n                }),\n            )\n            .layer(AddExtensionLayer::new(self.environment.clone())))\n    }\n}\n"
  },
  {
    "path": "src/controller/middleware/mod.rs",
    "content": "//! Base Middleware for Loco Application\n//!\n//! This module defines the various middleware components that Loco provides.\n//! Each middleware is responsible for handling different aspects of request\n//! processing, such as authentication, logging, CORS, compression, and error\n//! handling. The middleware can be easily configured and applied to the\n//! application's router.\n\npub mod catch_panic;\npub mod compression;\npub mod cors;\npub mod etag;\npub mod fallback;\npub mod format;\npub mod limit_payload;\npub mod logger;\npub mod powered_by;\npub mod remote_ip;\npub mod request_id;\npub mod secure_headers;\n#[cfg(feature = \"embedded_assets\")]\npub mod static_assets_embedded;\n#[cfg(feature = \"embedded_assets\")]\npub use static_assets_embedded as static_assets;\n\n#[cfg(not(feature = \"embedded_assets\"))]\npub mod static_assets;\npub mod timeout;\n\nuse axum::Router as AXRouter;\nuse serde::{Deserialize, Serialize};\n\nuse crate::{app::AppContext, environment::Environment, Result};\n\n/// Trait representing the behavior of middleware components in the application.\n/// When implementing a new middleware, make sure to go over this checklist:\n/// * The name of the middleware should be an ID that is similar to the field\n///   name in configuration (look at how `serde` calls it)\n/// * Default value implementation should be paired with `serde` default\n///   handlers and default serialization implementation. Which means deriving\n///   `Default` will _not_ work. You can use `serde_json` and serialize a new\n///   config from an empty value, which will cause `serde` default value\n///   handlers to kick in.\n/// * If you need completely blank values for configuration (for example for\n///   testing), implement an `::empty() -> Self` call ad-hoc.\npub trait MiddlewareLayer {\n    /// Returns the name of the middleware.\n    /// This should match the name of the property in the containing\n    /// `middleware` section in configuration (as named by `serde`)\n    fn name(&self) -> &'static str;\n\n    /// Returns whether the middleware is enabled or not.\n    /// If the middleware is switchable, take this value from a configuration\n    /// value\n    fn is_enabled(&self) -> bool {\n        true\n    }\n\n    /// Returns middleware config.\n    ///\n    /// # Errors\n    /// when could not convert middleware to [`serde_json::Value`]\n    fn config(&self) -> serde_json::Result<serde_json::Value>;\n\n    /// Applies the middleware to the given Axum router and returns the modified\n    /// router.\n    ///\n    /// # Errors\n    ///\n    /// If there is an issue when adding the middleware to the router.\n    fn apply(&self, app: AXRouter<AppContext>) -> Result<AXRouter<AppContext>>;\n}\n\n#[allow(clippy::unnecessary_lazy_evaluations)]\n#[must_use]\npub fn default_middleware_stack(ctx: &AppContext) -> Vec<Box<dyn MiddlewareLayer>> {\n    // Shortened reference to middlewares\n    let middlewares = &ctx.config.server.middlewares;\n\n    vec![\n        // Limit Payload middleware with a default if none\n        Box::new(middlewares.limit_payload.clone().unwrap_or_default()),\n        // CORS middleware with a default if none\n        Box::new(middlewares.cors.clone().unwrap_or_else(|| cors::Cors {\n            enable: false,\n            ..Default::default()\n        })),\n        // Catch Panic middleware with a default if none\n        Box::new(\n            middlewares\n                .catch_panic\n                .clone()\n                .unwrap_or_else(|| catch_panic::CatchPanic { enable: true }),\n        ),\n        // Etag middleware with a default if none\n        Box::new(\n            middlewares\n                .etag\n                .clone()\n                .unwrap_or_else(|| etag::Etag { enable: true }),\n        ),\n        // Remote IP middleware with a default if none\n        Box::new(\n            middlewares\n                .remote_ip\n                .clone()\n                .unwrap_or_else(|| remote_ip::RemoteIpMiddleware {\n                    enable: false,\n                    ..Default::default()\n                }),\n        ),\n        // Compression middleware with a default if none\n        Box::new(\n            middlewares\n                .compression\n                .clone()\n                .unwrap_or_else(|| compression::Compression { enable: false }),\n        ),\n        // Timeout Request middleware with a default if none\n        Box::new(\n            middlewares\n                .timeout_request\n                .clone()\n                .unwrap_or_else(|| timeout::TimeOut {\n                    enable: false,\n                    ..Default::default()\n                }),\n        ),\n        // Static Assets middleware with a default if none\n        Box::new(middlewares.static_assets.clone().unwrap_or_else(|| {\n            static_assets::StaticAssets {\n                enable: false,\n                ..Default::default()\n            }\n        })),\n        // Secure Headers middleware with a default if none\n        Box::new(middlewares.secure_headers.clone().unwrap_or_else(|| {\n            secure_headers::SecureHeader {\n                enable: false,\n                ..Default::default()\n            }\n        })),\n        // Logger middleware with default logger configuration\n        Box::new(logger::new(\n            &middlewares\n                .logger\n                .clone()\n                .unwrap_or_else(|| logger::Config { enable: true }),\n            &ctx.environment,\n        )),\n        // Request ID middleware with a default if none\n        Box::new(\n            middlewares\n                .request_id\n                .clone()\n                .unwrap_or_else(|| request_id::RequestId { enable: true }),\n        ),\n        // Fallback middleware with a default if none\n        Box::new(\n            middlewares\n                .fallback\n                .clone()\n                .unwrap_or_else(|| fallback::Fallback {\n                    enable: ctx.environment != Environment::Production,\n                    ..Default::default()\n                }),\n        ),\n        // Powered by middleware with a default identifier\n        Box::new(powered_by::new(ctx.config.server.ident.as_deref())),\n    ]\n}\n\n/// Server middleware configuration structure.\n#[derive(Default, Debug, Clone, Deserialize, Serialize)]\npub struct Config {\n    /// Compression for the response.\n    pub compression: Option<compression::Compression>,\n\n    /// Etag cache headers.\n    pub etag: Option<etag::Etag>,\n\n    /// Limit the payload request.\n    pub limit_payload: Option<limit_payload::LimitPayload>,\n\n    /// Logger and augmenting trace id with request data\n    pub logger: Option<logger::Config>,\n\n    /// Catch any code panic and log the error.\n    pub catch_panic: Option<catch_panic::CatchPanic>,\n\n    /// Setting a global timeout for requests\n    pub timeout_request: Option<timeout::TimeOut>,\n\n    /// CORS configuration\n    pub cors: Option<cors::Cors>,\n\n    /// Serving static assets\n    #[serde(rename = \"static\")]\n    pub static_assets: Option<static_assets::StaticAssets>,\n\n    /// Sets a set of secure headers\n    pub secure_headers: Option<secure_headers::SecureHeader>,\n\n    /// Calculates a remote IP based on `X-Forwarded-For` when behind a proxy\n    pub remote_ip: Option<remote_ip::RemoteIpMiddleware>,\n\n    /// Configure fallback behavior when hitting a missing URL\n    pub fallback: Option<fallback::Fallback>,\n\n    /// Request ID\n    pub request_id: Option<request_id::RequestId>,\n}\n"
  },
  {
    "path": "src/controller/middleware/powered_by.rs",
    "content": "//! Powered-By Middleware\n//!\n//! This middleware injects an HTTP header `X-Powered-By` into the response\n//! headers of every request handled by the application. The header identifies\n//! the software or technology stack powering the application. It supports a\n//! custom identifier string or defaults to \"loco.rs\" if no identifier is\n//! provided.\n\nuse std::sync::OnceLock;\n\nuse axum::{\n    http::header::{HeaderName, HeaderValue},\n    Router as AXRouter,\n};\nuse tower_http::set_header::SetResponseHeaderLayer;\n\nuse crate::{app::AppContext, controller::middleware::MiddlewareLayer, Result};\n\nstatic DEFAULT_IDENT_HEADER_VALUE: OnceLock<HeaderValue> = OnceLock::new();\n\nfn get_default_ident_header_value() -> &'static HeaderValue {\n    DEFAULT_IDENT_HEADER_VALUE.get_or_init(|| HeaderValue::from_static(\"loco.rs\"))\n}\n\n/// [`Middleware`] struct responsible for managing the identifier value for the\n/// `X-Powered-By` header.\n#[derive(Debug)]\npub struct Middleware {\n    ident: Option<HeaderValue>,\n}\n\n/// Creates a new instance of [`Middleware`] by cloning the [`Config`]\n/// configuration.\n#[must_use]\npub fn new(ident: Option<&str>) -> Middleware {\n    let ident_value = ident.map_or_else(\n        || Some(get_default_ident_header_value().clone()),\n        |ident| {\n            if ident.is_empty() {\n                None\n            } else {\n                match HeaderValue::from_str(ident) {\n                    Ok(val) => Some(val),\n                    Err(e) => {\n                        tracing::info!(\n                            error = format!(\"{}\", e),\n                            val = ident,\n                            \"could not set custom ident header\"\n                        );\n                        Some(get_default_ident_header_value().clone())\n                    }\n                }\n            }\n        },\n    );\n\n    Middleware { ident: ident_value }\n}\n\nimpl MiddlewareLayer for Middleware {\n    /// Returns the name of the middleware\n    fn name(&self) -> &'static str {\n        \"powered_by\"\n    }\n\n    /// Returns whether the middleware is enabled or not\n    fn is_enabled(&self) -> bool {\n        self.ident.is_some()\n    }\n\n    fn config(&self) -> serde_json::Result<serde_json::Value> {\n        self.ident.as_ref().map_or_else(\n            || Ok(serde_json::json!({})),\n            |ident| Ok(serde_json::json!({\"ident\": ident.to_str().unwrap_or_default()})),\n        )\n    }\n\n    /// Applies the middleware to the application by adding the `X-Powered-By`\n    /// header to each response.\n    fn apply(&self, app: AXRouter<AppContext>) -> Result<AXRouter<AppContext>> {\n        Ok(app.layer(SetResponseHeaderLayer::overriding(\n            HeaderName::from_static(\"x-powered-by\"),\n            self.ident\n                .clone()\n                .unwrap_or_else(|| get_default_ident_header_value().clone()),\n        )))\n    }\n}\n"
  },
  {
    "path": "src/controller/middleware/remote_ip.rs",
    "content": "//! Remote IP Middleware for inferring the client's IP address based on the\n//! `X-Forwarded-For` header.\n//!\n//! This middleware is useful when running behind proxies or load balancers that\n//! add the `X-Forwarded-For` header, which includes the original client IP\n//! address.\n//!\n//! The middleware provides a mechanism to configure trusted proxies and extract\n//! the most likely client IP from the `X-Forwarded-For` header, skipping any\n//! trusted proxy IPs.\nuse std::{\n    fmt,\n    iter::Iterator,\n    net::{IpAddr, SocketAddr},\n    str::FromStr,\n    sync::OnceLock,\n    task::{Context, Poll},\n};\n\nuse axum::{\n    body::Body,\n    extract::{ConnectInfo, FromRequestParts, Request},\n    http::{header::HeaderMap, request::Parts},\n    response::Response,\n    Router as AXRouter,\n};\nuse futures_util::future::BoxFuture;\nuse ipnetwork::IpNetwork;\nuse serde::{Deserialize, Serialize};\nuse tower::{Layer, Service};\nuse tracing::error;\n\nuse crate::{app::AppContext, controller::middleware::MiddlewareLayer, Error, Result};\n\nstatic LOCAL_TRUSTED_PROXIES: OnceLock<Vec<IpNetwork>> = OnceLock::new();\n\nfn get_local_trusted_proxies() -> &'static Vec<IpNetwork> {\n    LOCAL_TRUSTED_PROXIES.get_or_init(|| {\n        [\n            \"127.0.0.0/8\",   // localhost IPv4 range, per RFC-3330\n            \"::1\",           // localhost IPv6\n            \"fc00::/7\",      // private IPv6 range fc00::/7\n            \"10.0.0.0/8\",    // private IPv4 range 10.x.x.x\n            \"172.16.0.0/12\", // private IPv4 range 172.16.0.0 .. 172.31.255.255\n            \"192.168.0.0/16\",\n        ]\n        .iter()\n        .map(|ip| IpNetwork::from_str(ip).unwrap())\n        .collect()\n    })\n}\n\nconst X_FORWARDED_FOR: &str = \"X-Forwarded-For\";\n\n///\n/// Performs a remote ip \"calculation\", inferring the most likely\n/// client IP from the `X-Forwarded-For` header that is used by\n/// load balancers and proxies.\n///\n/// WARNING\n/// =======\n///\n/// LIKE ANY SUCH REMOTE IP MIDDLEWARE, IN THE WRONG ARCHITECTURE IT CAN MAKE\n/// YOU VULNERABLE TO IP SPOOFING.\n///\n/// This middleware assumes that there is at least one proxy sitting around and\n/// setting headers with the client's remote IP address. Otherwise any client\n/// can claim to have any IP address by setting the `X-Forwarded-For` header.\n///\n/// DO NOT USE THIS MIDDLEWARE IF YOU DONT KNOW THAT YOU NEED IT\n///\n/// -- But if you need it, it's crucial to use it (since it's the only way to\n/// get the original client IP)\n///\n/// This middleware is mostly implemented after the Rails `remote_ip`\n/// middleware, and looking at other production Rust services with Axum, taking\n/// the best of both worlds to balance performance and pragmatism.\n///\n/// Similarities to the Rails `remote_ip` middleware:\n///\n/// * Uses `X-Forwarded-For`\n/// * Uses the same built-in trusted proxies list\n/// * You can provide a list of `trusted_proxies` which will **replace** the\n///   built-in trusted proxies\n///\n/// Differences from the Rails `remote_ip` middleware:\n///\n/// * You get an indication if the remote IP is actually resolved or is the\n///   socket IP (no `X-Forwarded-For` header or could not parse)\n/// * We do not not use the `Client-IP` header, or try to detect \"spoofing\"\n///   (spoofing while doing remote IP resolution is virtually non-detectable)\n/// * Order of filtering IPs from `X-Forwarded-For` is done according to [the de\n///   facto spec](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For#selecting_an_ip_address)\n///   \"Trusted proxy list\"\n#[derive(Default, Serialize, Deserialize, Debug, Clone)]\npub struct RemoteIpMiddleware {\n    #[serde(default)]\n    pub enable: bool,\n    /// A list of alternative proxy list IP ranges and/or network range (will\n    /// replace built-in proxy list)\n    pub trusted_proxies: Option<Vec<String>>,\n}\n\nimpl MiddlewareLayer for RemoteIpMiddleware {\n    /// Returns the name of the middleware\n    fn name(&self) -> &'static str {\n        \"remote_ip\"\n    }\n\n    /// Returns whether the middleware is enabled or not\n    fn is_enabled(&self) -> bool {\n        self.enable\n            && (self.trusted_proxies.is_none()\n                || self.trusted_proxies.as_ref().is_some_and(|t| !t.is_empty()))\n    }\n\n    fn config(&self) -> serde_json::Result<serde_json::Value> {\n        serde_json::to_value(self)\n    }\n\n    /// Applies the Remote IP middleware to the given Axum router.\n    fn apply(&self, app: AXRouter<AppContext>) -> Result<AXRouter<AppContext>> {\n        Ok(app.layer(RemoteIPLayer::new(self)?))\n    }\n}\n\n// implementation reference: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For\nfn maybe_get_forwarded(\n    headers: &HeaderMap,\n    trusted_proxies: Option<&Vec<IpNetwork>>,\n) -> Option<IpAddr> {\n    /*\n    > There may be multiple X-Forwarded-For headers present in a request. The IP addresses in these headers must be treated as a single list,\n    > starting with the first IP address of the first header and continuing to the last IP address of the last header.\n    > There are two ways of making this single list:\n    > join the X-Forwarded-For full header values with commas and then split by comma into a list, or\n    > split each X-Forwarded-For header by comma into lists and then join the lists\n     */\n    let xffs = headers\n        .get_all(X_FORWARDED_FOR)\n        .iter()\n        .map(|hdr| hdr.to_str())\n        .filter_map(Result::ok)\n        .collect::<Vec<_>>();\n\n    if xffs.is_empty() {\n        return None;\n    }\n\n    let forwarded = xffs.join(\",\");\n\n    forwarded\n        .split(',')\n        .map(str::trim)\n        .map(str::parse)\n        .filter_map(Result::ok)\n        /*\n        > Trusted proxy list: The IPs or IP ranges of the trusted reverse proxies are configured.\n        > The X-Forwarded-For IP list is searched from the rightmost, skipping all addresses that\n        > are on the trusted proxy list. The first non-matching address is the target address.\n        */\n        /*\n        > When choosing the X-Forwarded-For client IP address closest to the client (untrustworthy\n        > and not for security-related purposes), the first IP from the leftmost that is a valid\n        > address and not private/internal should be selected.\n        >\n        NOTE:\n        > The first trustworthy X-Forwarded-For IP address may belong to an untrusted intermediate\n        > proxy rather than the actual client computer, but it is the only IP suitable for security uses.\n        */\n        .rfind(|ip| {\n            // trusted proxies provided REPLACES our default local proxies\n            let proxies = trusted_proxies.unwrap_or_else(|| get_local_trusted_proxies());\n            !proxies\n                .iter()\n                .any(|trusted_proxy| trusted_proxy.contains(*ip))\n        })\n}\n\n#[derive(Copy, Clone, Debug)]\npub enum RemoteIP {\n    Forwarded(IpAddr),\n    Socket(IpAddr),\n    None,\n}\n\nimpl<S> FromRequestParts<S> for RemoteIP\nwhere\n    S: Send + Sync,\n{\n    type Rejection = ();\n\n    async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {\n        let ip = parts.extensions.get::<Self>();\n        Ok(*ip.unwrap_or(&Self::None))\n    }\n}\n\nimpl fmt::Display for RemoteIP {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        match self {\n            Self::Forwarded(ip) => write!(f, \"remote: {ip}\"),\n            Self::Socket(ip) => write!(f, \"socket: {ip}\"),\n            Self::None => write!(f, \"--\"),\n        }\n    }\n}\n\n#[derive(Clone, Debug)]\nstruct RemoteIPLayer {\n    trusted_proxies: Option<Vec<IpNetwork>>,\n}\n\nimpl RemoteIPLayer {\n    /// Returns new secure headers middleware\n    ///\n    /// # Errors\n    /// Fails if invalid header values found\n    pub fn new(config: &RemoteIpMiddleware) -> Result<Self> {\n        Ok(Self {\n            trusted_proxies: config\n                .trusted_proxies\n                .as_ref()\n                .map(|proxies| {\n                    proxies\n                        .iter()\n                        .map(|proxy| {\n                            IpNetwork::from_str(proxy).map_err(|err| {\n                                Error::Message(format!(\n                                    \"remote ip middleare cannot parse trusted proxy \\\n                                     configuration: `{proxy}`, reason: `{err}`\",\n                                ))\n                            })\n                        })\n                        .collect::<Result<Vec<_>>>()\n                })\n                .transpose()?,\n        })\n    }\n}\n\nimpl<S> Layer<S> for RemoteIPLayer {\n    type Service = RemoteIPMiddleware<S>;\n\n    fn layer(&self, inner: S) -> Self::Service {\n        RemoteIPMiddleware {\n            inner,\n            layer: self.clone(),\n        }\n    }\n}\n\n/// Remote IP Detection Middleware\n#[derive(Clone, Debug)]\n#[must_use]\npub struct RemoteIPMiddleware<S> {\n    inner: S,\n    layer: RemoteIPLayer,\n}\n\nimpl<S> Service<Request<Body>> for RemoteIPMiddleware<S>\nwhere\n    S: Service<Request<Body>, Response = Response> + Send + 'static,\n    S::Future: Send + 'static,\n{\n    type Response = S::Response;\n    type Error = S::Error;\n    type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;\n\n    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {\n        self.inner.poll_ready(cx)\n    }\n\n    fn call(&mut self, mut req: Request<Body>) -> Self::Future {\n        let layer = self.layer.clone();\n        let xff_ip = maybe_get_forwarded(req.headers(), layer.trusted_proxies.as_ref());\n        let remote_ip = xff_ip.map_or_else(\n            || {\n                let ip = req\n                    .extensions()\n                    .get::<ConnectInfo<SocketAddr>>()\n                    .map_or_else(\n                        || {\n                            error!(\n                                \"remote ip middleware cannot get socket IP (not set in axum \\\n                                 extensions): setting IP to `127.0.0.1`\"\n                            );\n                            RemoteIP::None\n                        },\n                        |info| RemoteIP::Socket(info.ip()),\n                    );\n                ip\n            },\n            RemoteIP::Forwarded,\n        );\n\n        req.extensions_mut().insert(remote_ip);\n\n        Box::pin(self.inner.call(req))\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use std::str::FromStr;\n\n    use axum::http::{HeaderMap, HeaderName, HeaderValue};\n    use insta::assert_debug_snapshot;\n    use ipnetwork::IpNetwork;\n\n    use super::maybe_get_forwarded;\n\n    fn xff(val: &str) -> HeaderMap {\n        let mut headers = HeaderMap::new();\n\n        headers.insert(\n            HeaderName::from_static(\"x-forwarded-for\"),\n            HeaderValue::from_str(val).unwrap(),\n        );\n        headers\n    }\n\n    #[test]\n    pub fn test_parsing() {\n        let res = maybe_get_forwarded(&xff(\"\"), None);\n        assert_debug_snapshot!(res);\n        let res = maybe_get_forwarded(&xff(\"foobar\"), None);\n        assert_debug_snapshot!(res);\n        let res = maybe_get_forwarded(&xff(\"192.1.1.1\"), None);\n        assert_debug_snapshot!(res);\n        let res = maybe_get_forwarded(&xff(\"51.50.51.50,10.0.0.1,192.168.1.1\"), None);\n        assert_debug_snapshot!(res);\n        let res = maybe_get_forwarded(&xff(\"19.84.19.84,192.168.0.1\"), None);\n        assert_debug_snapshot!(res);\n        let res = maybe_get_forwarded(&xff(\"b51.50.51.50b,/10.0.0.1-,192.168.1.1\"), None);\n        assert_debug_snapshot!(res);\n        let res = maybe_get_forwarded(\n            &xff(\"51.50.51.50,192.1.1.1\"),\n            Some(&vec![IpNetwork::from_str(\"192.1.1.1/8\").unwrap()]),\n        );\n        assert_debug_snapshot!(res);\n\n        // we replaced the proxy list, which is why 192.168.1.1 should appear as a valid\n        // remote IP and not skipped\n        let res = maybe_get_forwarded(\n            &xff(\"51.50.51.50,192.168.1.1\"),\n            Some(&vec![IpNetwork::from_str(\"192.1.1.1/16\").unwrap()]),\n        );\n        assert_debug_snapshot!(res);\n    }\n}\n"
  },
  {
    "path": "src/controller/middleware/request_id.rs",
    "content": "//! Middleware to generate or ensure a unique request ID for every request.\n//!\n//! The request ID is stored in the `x-request-id` header, and it is either\n//! generated or sanitized if already present in the request.\n//!\n//! This can be useful for tracking requests across services, logging, and\n//! debugging.\n\nuse axum::{\n    extract::Request, http::HeaderValue, middleware::Next, response::Response, Router as AXRouter,\n};\nuse regex::Regex;\nuse serde::{Deserialize, Serialize};\nuse uuid::Uuid;\n\nuse crate::{app::AppContext, controller::middleware::MiddlewareLayer, Result};\n\nconst X_REQUEST_ID: &str = \"x-request-id\";\nconst MAX_LEN: usize = 255;\n\nuse std::sync::OnceLock;\n\nstatic ID_CLEANUP: OnceLock<Regex> = OnceLock::new();\n\nfn get_id_cleanup() -> &'static Regex {\n    ID_CLEANUP.get_or_init(|| Regex::new(r\"[^\\w\\-@]\").unwrap())\n}\n#[derive(Debug, Clone, Deserialize, Serialize)]\npub struct RequestId {\n    #[serde(default)]\n    pub enable: bool,\n}\n\nimpl MiddlewareLayer for RequestId {\n    /// Returns the name of the middleware\n    fn name(&self) -> &'static str {\n        \"request_id\"\n    }\n\n    /// Returns whether the middleware is enabled or not\n    fn is_enabled(&self) -> bool {\n        self.enable\n    }\n\n    fn config(&self) -> serde_json::Result<serde_json::Value> {\n        serde_json::to_value(self)\n    }\n\n    /// Applies the request ID middleware to the Axum router.\n    ///\n    /// This function sets up the middleware in the router and ensures that\n    /// every request passing through it will have a unique or sanitized\n    /// request ID.\n    ///\n    /// # Errors\n    /// This function returns an error if the middleware cannot be applied.\n    fn apply(&self, app: AXRouter<AppContext>) -> Result<AXRouter<AppContext>> {\n        Ok(app.layer(axum::middleware::from_fn(request_id_middleware)))\n    }\n}\n\n/// Wrapper struct for storing the request ID in the request's extensions.\n#[derive(Debug, Clone)]\npub struct LocoRequestId(String);\n\nimpl LocoRequestId {\n    /// Retrieves the request ID as a string slice.\n    #[must_use]\n    pub fn get(&self) -> &str {\n        self.0.as_str()\n    }\n}\n\n/// Middleware function to ensure or generate a unique request ID.\n///\n/// This function intercepts requests, checks for the presence of the\n/// `x-request-id` header, and either sanitizes its value or generates a new\n/// UUID if absent. The resulting request ID is added to both the request\n/// extensions and the response headers.\npub async fn request_id_middleware(mut request: Request, next: Next) -> Response {\n    let header_request_id = request.headers().get(X_REQUEST_ID).cloned();\n    let request_id = make_request_id(header_request_id);\n    request\n        .extensions_mut()\n        .insert(LocoRequestId(request_id.clone()));\n    let mut res = next.run(request).await;\n\n    if let Ok(v) = HeaderValue::from_str(request_id.as_str()) {\n        res.headers_mut().insert(X_REQUEST_ID, v);\n    } else {\n        tracing::warn!(\"could not set request ID into response headers: `{request_id}`\",);\n    }\n    res\n}\n\n/// Generates or sanitizes a request ID.\nfn make_request_id(maybe_request_id: Option<HeaderValue>) -> String {\n    maybe_request_id\n        .and_then(|hdr| {\n            // see: https://github.com/rails/rails/blob/main/actionpack/lib/action_dispatch/middleware/request_id.rb#L39\n            let id: Option<String> = hdr.to_str().ok().map(|s| {\n                get_id_cleanup()\n                    .replace_all(s, \"\")\n                    .chars()\n                    .take(MAX_LEN)\n                    .collect()\n            });\n            id.filter(|s| !s.is_empty())\n        })\n        .unwrap_or_else(|| Uuid::new_v4().to_string())\n}\n\n#[cfg(test)]\nmod tests {\n    use axum::http::HeaderValue;\n    use insta::assert_debug_snapshot;\n\n    use super::make_request_id;\n\n    #[test]\n    fn create_or_fetch_request_id() {\n        let id = make_request_id(Some(HeaderValue::from_static(\"foo-bar=baz\")));\n        assert_debug_snapshot!(id);\n        let id = make_request_id(Some(HeaderValue::from_static(\"\")));\n        assert_debug_snapshot!(id.len());\n        let id = make_request_id(Some(HeaderValue::from_static(\"==========\")));\n        assert_debug_snapshot!(id.len());\n        let long_id = \"x\".repeat(1000);\n        let id = make_request_id(Some(HeaderValue::from_str(&long_id).unwrap()));\n        assert_debug_snapshot!(id.len());\n        let id = make_request_id(None);\n        assert_debug_snapshot!(id.len());\n    }\n}\n"
  },
  {
    "path": "src/controller/middleware/secure_headers.json",
    "content": "{\n  \"empty\":{},\n  \"github\":{\n    \"Content-Security-Policy\": \"default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src https:; style-src 'self' https: 'unsafe-inline'\",\n    \"Strict-Transport-Security\": \"max-age=631138519\",\n    \"X-Content-Type-Options\": \"nosniff\",\n    \"X-Download-Options\": \"noopen\",\n    \"X-Frame-Options\": \"sameorigin\",\n    \"X-Permitted-Cross-Domain-Policies\": \"none\",\n    \"X-Xss-Protection\": \"0\"\n  },\n  \"owasp\":{\n    \"Cache-Control\": \"no-store, max-age=0\",\n    \"Clear-Site-Data\": \"\\\"cache\\\",\\\"cookies\\\",\\\"storage\\\"\",\n    \"Content-Security-Policy\": \"default-src 'self'; form-action 'self'; object-src 'none'; frame-ancestors 'none'; upgrade-insecure-requests; block-all-mixed-content\",\n    \"Cross-Origin-Embedder-Policy\": \"require-corp\",\n    \"Cross-Origin-Opener-Policy\": \"same-origin\",\n    \"Cross-Origin-Resource-Policy\": \"same-origin\",\n    \"Permissions-Policy\": \"accelerometer=(), autoplay=(), camera=(), cross-origin-isolated=(), display-capture=(), encrypted-media=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(self), usb=(), web-share=(), xr-spatial-tracking=(), clipboard-read=(), clipboard-write=(), gamepad=(), hid=(), idle-detection=(), interest-cohort=(), serial=(), unload=()\",\n    \"Referrer-Policy\": \"no-referrer\",\n    \"Strict-Transport-Security\": \"max-age=31536000; includeSubDomains\",\n    \"X-Content-Type-Options\": \"nosniff\",\n    \"X-Frame-Options\": \"deny\",\n    \"X-Permitted-Cross-Domain-Policies\": \"none\"\n  }\n}\n"
  },
  {
    "path": "src/controller/middleware/secure_headers.rs",
    "content": "//! Sets secure headers for your backend to promote security-by-default.\n//!\n//! This middleware applies secure HTTP headers, providing pre-defined presets\n//! (e.g., \"github\") and the ability to override or define custom headers.\n\nuse std::{\n    collections::{BTreeMap, HashMap},\n    sync::OnceLock,\n    task::{Context, Poll},\n};\n\nuse axum::{\n    body::Body,\n    http::{HeaderName, HeaderValue, Request},\n    response::Response,\n    Router as AXRouter,\n};\nuse futures_util::future::BoxFuture;\nuse serde::{Deserialize, Serialize};\nuse serde_json::{self, json};\nuse tower::{Layer, Service};\n\nuse crate::{app::AppContext, controller::middleware::MiddlewareLayer, Error, Result};\n\nstatic PRESETS: OnceLock<HashMap<String, BTreeMap<String, String>>> = OnceLock::new();\nfn get_presets() -> &'static HashMap<String, BTreeMap<String, String>> {\n    PRESETS.get_or_init(|| {\n        let json_data = include_str!(\"secure_headers.json\");\n        serde_json::from_str(json_data).unwrap()\n    })\n}\n/// Sets a predefined or custom set of secure headers.\n///\n/// We recommend our `github` preset. Presets values are derived\n/// from the [secure_headers](https://github.com/github/secure_headers) Ruby\n/// library which Github (and originally Twitter) use.\n///\n/// To use a preset, in your `config/development.yaml`:\n///\n/// ```yaml\n/// middlewares:\n///   secure_headers:\n///     preset: github\n/// ```\n///\n/// You can also override individual headers on a given preset:\n///\n/// ```yaml\n/// middlewares:\n///   secure_headers:\n///     preset: github\n///     overrides:\n///       foo: bar\n/// ```\n///\n/// Or start from scratch:\n///\n///```yaml\n/// middlewares:\n///   secure_headers:\n///     preset: empty\n///     overrides:\n///       one: two\n/// ```\n///\n/// To support `htmx`, You can add the following override, to allow some inline\n/// running of scripts:\n///\n/// ```yaml\n/// secure_headers:\n///     preset: github\n///     overrides:\n///         # this allows you to use HTMX, and has unsafe-inline. Remove or consider in production\n///         \"Content-Security-Policy\": \"default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src 'unsafe-inline' 'self' https:; style-src 'self' https: 'unsafe-inline'\"\n/// ```\n///\n/// For the list of presets and their content look at [secure_headers.json](https://github.com/loco-rs/loco/blob/master/src/controller/middleware/secure_headers.rs)\n#[derive(Serialize, Deserialize, Debug, Clone)]\npub struct SecureHeader {\n    #[serde(default)]\n    pub enable: bool,\n    #[serde(default = \"default_preset\")]\n    pub preset: String,\n    #[serde(default)]\n    pub overrides: Option<BTreeMap<String, String>>,\n}\n\nimpl Default for SecureHeader {\n    fn default() -> Self {\n        serde_json::from_value(json!({})).unwrap()\n    }\n}\n\nfn default_preset() -> String {\n    \"github\".to_string()\n}\n\nimpl MiddlewareLayer for SecureHeader {\n    /// Returns the name of the middleware\n    fn name(&self) -> &'static str {\n        \"secure_headers\"\n    }\n\n    /// Returns whether the middleware is enabled or not\n    fn is_enabled(&self) -> bool {\n        self.enable\n    }\n\n    fn config(&self) -> serde_json::Result<serde_json::Value> {\n        serde_json::to_value(self)\n    }\n\n    /// Applies the secure headers layer to the application router\n    fn apply(&self, app: AXRouter<AppContext>) -> Result<AXRouter<AppContext>> {\n        Ok(app.layer(SecureHeaders::new(self)?))\n    }\n}\n\nimpl SecureHeader {\n    /// Converts the configuration into a list of headers.\n    ///\n    /// Applies the preset headers and any custom overrides.\n    fn as_headers(&self) -> Result<Vec<(HeaderName, HeaderValue)>> {\n        let mut pairs = BTreeMap::new();\n        if let Some(overrides) = &self.overrides {\n            for (key, value) in overrides {\n                pairs.insert(key, value);\n            }\n        }\n\n        let preset = &self.preset;\n        let p = get_presets().get(preset).ok_or_else(|| {\n            Error::Message(format!(\n                \"secure_headers: a preset named `{preset}` does not exist\"\n            ))\n        })?;\n\n        for (key, value) in p {\n            if pairs.contains_key(key) {\n                continue;\n            }\n            pairs.insert(key, value);\n        }\n        let mut headers = Vec::with_capacity(pairs.len());\n        for (key, value) in pairs {\n            headers.push((\n                HeaderName::from_bytes(key.as_bytes()).map_err(Box::from)?,\n                HeaderValue::from_str(value).map_err(Box::from)?,\n            ));\n        }\n        Ok(headers)\n    }\n}\n\n/// The [`SecureHeaders`] layer which wraps around the service and injects\n/// security headers\n#[derive(Clone, Debug)]\npub struct SecureHeaders {\n    headers: Vec<(HeaderName, HeaderValue)>,\n}\n\nimpl SecureHeaders {\n    /// Creates a new [`SecureHeaders`] instance with the provided\n    /// configuration.\n    ///\n    /// # Errors\n    /// Returns an error if any header values are invalid.\n    pub fn new(config: &SecureHeader) -> Result<Self> {\n        Ok(Self {\n            headers: config.as_headers()?,\n        })\n    }\n}\n\nimpl<S> Layer<S> for SecureHeaders {\n    type Service = SecureHeadersMiddleware<S>;\n\n    /// Wraps the provided service with the secure headers middleware.\n    fn layer(&self, inner: S) -> Self::Service {\n        SecureHeadersMiddleware {\n            inner,\n            layer: self.clone(),\n        }\n    }\n}\n\n/// The secure headers middleware\n#[derive(Clone, Debug)]\n#[must_use]\npub struct SecureHeadersMiddleware<S> {\n    inner: S,\n    layer: SecureHeaders,\n}\n\nimpl<S> Service<Request<Body>> for SecureHeadersMiddleware<S>\nwhere\n    S: Service<Request<Body>, Response = Response> + Send + 'static,\n    S::Future: Send + 'static,\n{\n    type Response = S::Response;\n    type Error = S::Error;\n    type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;\n\n    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {\n        self.inner.poll_ready(cx)\n    }\n\n    fn call(&mut self, request: Request<Body>) -> Self::Future {\n        let layer = self.layer.clone();\n        let future = self.inner.call(request);\n        Box::pin(async move {\n            let mut response: Response = future.await?;\n            let headers = response.headers_mut();\n            for (k, v) in &layer.headers {\n                if headers.contains_key(k) {\n                    continue;\n                }\n                headers.insert(k, v.clone());\n            }\n            Ok(response)\n        })\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use crate::controller::format;\n    use axum::{\n        body::Body,\n        http::{header::CONTENT_SECURITY_POLICY, HeaderMap, HeaderValue, Method, Response},\n        routing::get,\n        Router,\n    };\n    use insta::assert_debug_snapshot;\n    use tower::ServiceExt;\n\n    use super::*;\n    fn normalize_headers(headers: &HeaderMap) -> BTreeMap<String, String> {\n        headers\n            .iter()\n            .map(|(k, v)| {\n                let key = k.to_string();\n                let value = v.to_str().unwrap_or(\"\").to_string();\n                (key, value)\n            })\n            .collect()\n    }\n    #[tokio::test]\n    async fn can_set_headers() {\n        let config = SecureHeader {\n            enable: true,\n            preset: \"github\".to_string(),\n            overrides: None,\n        };\n        let app = Router::new()\n            .route(\"/\", get(|| async {}))\n            .layer(SecureHeaders::new(&config).unwrap());\n\n        let req = Request::builder()\n            .uri(\"/\")\n            .method(Method::GET)\n            .body(Body::empty())\n            .unwrap();\n        let response = app.oneshot(req).await.unwrap();\n        assert_debug_snapshot!(normalize_headers(response.headers()));\n    }\n\n    #[tokio::test]\n    async fn can_override_headers() {\n        let mut overrides = BTreeMap::new();\n        overrides.insert(\"X-Download-Options\".to_string(), \"foobar\".to_string());\n        overrides.insert(\"New-Header\".to_string(), \"baz\".to_string());\n\n        let config = SecureHeader {\n            enable: true,\n            preset: \"github\".to_string(),\n            overrides: Some(overrides),\n        };\n        let app = Router::new()\n            .route(\"/\", get(|| async {}))\n            .layer(SecureHeaders::new(&config).unwrap());\n\n        let req = Request::builder()\n            .uri(\"/\")\n            .method(Method::GET)\n            .body(Body::empty())\n            .unwrap();\n        let response = app.oneshot(req).await.unwrap();\n        assert_debug_snapshot!(normalize_headers(response.headers()));\n    }\n\n    #[tokio::test]\n    async fn can_dynamically_override_headers() {\n        let config = SecureHeader {\n            enable: true,\n            preset: \"github\".to_string(),\n            overrides: None,\n        };\n        async fn handler() -> Result<Response<Body>> {\n            let nonce = \"random\";\n            let mut res: Response<Body> = format::template(\n                \"<style nonce='{{ nonce }}'></style>\",\n                serde_json::json!({\"nonce\": nonce}),\n            )\n            .unwrap();\n            res.headers_mut().insert(\n                CONTENT_SECURITY_POLICY, HeaderValue::from_str(\n                    &format!(\"default-src 'self'; form-action 'self'; object-src 'none'; frame-ancestors 'none'; upgrade-insecure-requests; block-all-mixed-content; style-src 'self' 'nonce-{nonce}';\")\n                ).expect(\"cannot create a header value\")\n            );\n            Ok(res)\n        }\n        let app = Router::new()\n            .route(\"/\", get(handler))\n            .layer(SecureHeaders::new(&config).unwrap());\n\n        let req = Request::builder()\n            .uri(\"/\")\n            .method(Method::GET)\n            .body(Body::empty())\n            .unwrap();\n        let response = app.oneshot(req).await.unwrap();\n        let headers = normalize_headers(response.headers());\n        assert_eq!(\n            headers.get(&CONTENT_SECURITY_POLICY.to_string()).unwrap(),\n            \"default-src 'self'; form-action 'self'; object-src 'none'; frame-ancestors 'none'; upgrade-insecure-requests; block-all-mixed-content; style-src 'self' 'nonce-random';\"\n        )\n    }\n\n    #[tokio::test]\n    async fn default_is_github_preset() {\n        let config = SecureHeader::default();\n        let app = Router::new()\n            .route(\"/\", get(|| async {}))\n            .layer(SecureHeaders::new(&config).unwrap());\n\n        let req = Request::builder()\n            .uri(\"/\")\n            .method(Method::GET)\n            .body(Body::empty())\n            .unwrap();\n        let response = app.oneshot(req).await.unwrap();\n        assert_debug_snapshot!(normalize_headers(response.headers()));\n    }\n}\n"
  },
  {
    "path": "src/controller/middleware/snapshots/loco_rs__controller__middleware__cors__tests__cors_OPTIONS_[allow_origins].snap",
    "content": "---\nsource: src/controller/middleware/cors.rs\nexpression: \"(format!(\\\"access-control-allow-origin: {:?}\\\",\\nresponse.headers().get(\\\"access-control-allow-origin\\\")),\\nformat!(\\\"vary: {:?}\\\", response.headers().get(\\\"vary\\\")),\\nformat!(\\\"access-control-allow-methods: {:?}\\\",\\nresponse.headers().get(\\\"access-control-allow-methods\\\")),\\nformat!(\\\"access-control-allow-headers: {:?}\\\",\\nresponse.headers().get(\\\"access-control-allow-headers\\\")),\\nformat!(\\\"access-control-expose-headers: {:?}\\\",\\nresponse.headers().get(\\\"access-control-expose-headers\\\")),\\nformat!(\\\"allow: {:?}\\\", response.headers().get(\\\"allow\\\")),)\"\n---\n(\n    \"access-control-allow-origin: Some(\\\"http://example.com\\\")\",\n    \"vary: Some(\\\"origin, access-control-request-method, access-control-request-headers\\\")\",\n    \"access-control-allow-methods: Some(\\\"*\\\")\",\n    \"access-control-allow-headers: Some(\\\"*\\\")\",\n    \"access-control-expose-headers: None\",\n    \"allow: Some(\\\"GET,HEAD\\\")\",\n)\n"
  },
  {
    "path": "src/controller/middleware/snapshots/loco_rs__controller__middleware__cors__tests__cors_[default].snap",
    "content": "---\nsource: src/controller/middleware/cors.rs\nexpression: \"(format!(\\\"access-control-allow-origin: {:?}\\\",\\nresponse.headers().get(\\\"access-control-allow-origin\\\")),\\nformat!(\\\"vary: {:?}\\\", response.headers().get(\\\"vary\\\")),\\nformat!(\\\"access-control-allow-methods: {:?}\\\",\\nresponse.headers().get(\\\"access-control-allow-methods\\\")),\\nformat!(\\\"access-control-allow-headers: {:?}\\\",\\nresponse.headers().get(\\\"access-control-allow-headers\\\")),\\nformat!(\\\"access-control-expose-headers: {:?}\\\",\\nresponse.headers().get(\\\"access-control-expose-headers\\\")),\\nformat!(\\\"allow: {:?}\\\", response.headers().get(\\\"allow\\\")),)\"\n---\n(\n    \"access-control-allow-origin: Some(\\\"*\\\")\",\n    \"vary: Some(\\\"origin, access-control-request-method, access-control-request-headers\\\")\",\n    \"access-control-allow-methods: None\",\n    \"access-control-allow-headers: None\",\n    \"access-control-expose-headers: None\",\n    \"allow: None\",\n)\n"
  },
  {
    "path": "src/controller/middleware/snapshots/loco_rs__controller__middleware__cors__tests__cors_[with_allow_headers].snap",
    "content": "---\nsource: src/controller/middleware/cors.rs\nexpression: \"(format!(\\\"access-control-allow-origin: {:?}\\\",\\nresponse.headers().get(\\\"access-control-allow-origin\\\")),\\nformat!(\\\"vary: {:?}\\\", response.headers().get(\\\"vary\\\")),\\nformat!(\\\"access-control-allow-methods: {:?}\\\",\\nresponse.headers().get(\\\"access-control-allow-methods\\\")),\\nformat!(\\\"access-control-allow-headers: {:?}\\\",\\nresponse.headers().get(\\\"access-control-allow-headers\\\")),\\nformat!(\\\"access-control-expose-headers: {:?}\\\",\\nresponse.headers().get(\\\"access-control-expose-headers\\\")),\\nformat!(\\\"allow: {:?}\\\", response.headers().get(\\\"allow\\\")),)\"\n---\n(\n    \"access-control-allow-origin: Some(\\\"*\\\")\",\n    \"vary: Some(\\\"origin, access-control-request-method, access-control-request-headers\\\")\",\n    \"access-control-allow-methods: None\",\n    \"access-control-allow-headers: None\",\n    \"access-control-expose-headers: None\",\n    \"allow: None\",\n)\n"
  },
  {
    "path": "src/controller/middleware/snapshots/loco_rs__controller__middleware__cors__tests__cors_[with_allow_methods].snap",
    "content": "---\nsource: src/controller/middleware/cors.rs\nexpression: \"(format!(\\\"access-control-allow-origin: {:?}\\\",\\nresponse.headers().get(\\\"access-control-allow-origin\\\")),\\nformat!(\\\"vary: {:?}\\\", response.headers().get(\\\"vary\\\")),\\nformat!(\\\"access-control-allow-methods: {:?}\\\",\\nresponse.headers().get(\\\"access-control-allow-methods\\\")),\\nformat!(\\\"access-control-allow-headers: {:?}\\\",\\nresponse.headers().get(\\\"access-control-allow-headers\\\")),\\nformat!(\\\"access-control-expose-headers: {:?}\\\",\\nresponse.headers().get(\\\"access-control-expose-headers\\\")),\\nformat!(\\\"allow: {:?}\\\", response.headers().get(\\\"allow\\\")),)\"\n---\n(\n    \"access-control-allow-origin: Some(\\\"*\\\")\",\n    \"vary: Some(\\\"origin, access-control-request-method, access-control-request-headers\\\")\",\n    \"access-control-allow-methods: None\",\n    \"access-control-allow-headers: None\",\n    \"access-control-expose-headers: None\",\n    \"allow: None\",\n)\n"
  },
  {
    "path": "src/controller/middleware/snapshots/loco_rs__controller__middleware__cors__tests__cors_[with_expose_headers].snap",
    "content": "---\nsource: src/controller/middleware/cors.rs\nexpression: \"(format!(\\\"access-control-allow-origin: {:?}\\\",\\nresponse.headers().get(\\\"access-control-allow-origin\\\")),\\nformat!(\\\"vary: {:?}\\\", response.headers().get(\\\"vary\\\")),\\nformat!(\\\"access-control-allow-methods: {:?}\\\",\\nresponse.headers().get(\\\"access-control-allow-methods\\\")),\\nformat!(\\\"access-control-allow-headers: {:?}\\\",\\nresponse.headers().get(\\\"access-control-allow-headers\\\")),\\nformat!(\\\"access-control-expose-headers: {:?}\\\",\\nresponse.headers().get(\\\"access-control-expose-headers\\\")),\\nformat!(\\\"allow: {:?}\\\", response.headers().get(\\\"allow\\\")),)\"\n---\n(\n    \"access-control-allow-origin: Some(\\\"*\\\")\",\n    \"vary: Some(\\\"origin, access-control-request-method, access-control-request-headers\\\")\",\n    \"access-control-allow-methods: None\",\n    \"access-control-allow-headers: None\",\n    \"access-control-expose-headers: Some(\\\"token,user\\\")\",\n    \"allow: None\",\n)\n"
  },
  {
    "path": "src/controller/middleware/snapshots/loco_rs__controller__middleware__cors__tests__cors_[with_max_age].snap",
    "content": "---\nsource: src/controller/middleware/cors.rs\nexpression: \"(format!(\\\"access-control-allow-origin: {:?}\\\",\\nresponse.headers().get(\\\"access-control-allow-origin\\\")),\\nformat!(\\\"vary: {:?}\\\", response.headers().get(\\\"vary\\\")),\\nformat!(\\\"access-control-allow-methods: {:?}\\\",\\nresponse.headers().get(\\\"access-control-allow-methods\\\")),\\nformat!(\\\"access-control-allow-headers: {:?}\\\",\\nresponse.headers().get(\\\"access-control-allow-headers\\\")),\\nformat!(\\\"access-control-expose-headers: {:?}\\\",\\nresponse.headers().get(\\\"access-control-expose-headers\\\")),\\nformat!(\\\"allow: {:?}\\\", response.headers().get(\\\"allow\\\")),)\"\n---\n(\n    \"access-control-allow-origin: Some(\\\"*\\\")\",\n    \"vary: Some(\\\"origin, access-control-request-method, access-control-request-headers\\\")\",\n    \"access-control-allow-methods: None\",\n    \"access-control-allow-headers: None\",\n    \"access-control-expose-headers: None\",\n    \"allow: None\",\n)\n"
  },
  {
    "path": "src/controller/middleware/snapshots/loco_rs__controller__middleware__remote_ip__tests__parsing-2.snap",
    "content": "---\nsource: src/controller/middleware/remote_ip.rs\nexpression: res\n---\nNone\n"
  },
  {
    "path": "src/controller/middleware/snapshots/loco_rs__controller__middleware__remote_ip__tests__parsing-3.snap",
    "content": "---\nsource: src/controller/middleware/remote_ip.rs\nexpression: res\n---\nSome(\n    192.1.1.1,\n)\n"
  },
  {
    "path": "src/controller/middleware/snapshots/loco_rs__controller__middleware__remote_ip__tests__parsing-4.snap",
    "content": "---\nsource: src/controller/middleware/remote_ip.rs\nexpression: res\n---\nSome(\n    51.50.51.50,\n)\n"
  },
  {
    "path": "src/controller/middleware/snapshots/loco_rs__controller__middleware__remote_ip__tests__parsing-5.snap",
    "content": "---\nsource: src/controller/middleware/remote_ip.rs\nexpression: res\n---\nSome(\n    19.84.19.84,\n)\n"
  },
  {
    "path": "src/controller/middleware/snapshots/loco_rs__controller__middleware__remote_ip__tests__parsing-6.snap",
    "content": "---\nsource: src/controller/middleware/remote_ip.rs\nexpression: res\n---\nNone\n"
  },
  {
    "path": "src/controller/middleware/snapshots/loco_rs__controller__middleware__remote_ip__tests__parsing-7.snap",
    "content": "---\nsource: src/controller/middleware/remote_ip.rs\nexpression: res\n---\nSome(\n    51.50.51.50,\n)\n"
  },
  {
    "path": "src/controller/middleware/snapshots/loco_rs__controller__middleware__remote_ip__tests__parsing-8.snap",
    "content": "---\nsource: src/controller/middleware/remote_ip.rs\nexpression: res\n---\nSome(\n    192.168.1.1,\n)\n"
  },
  {
    "path": "src/controller/middleware/snapshots/loco_rs__controller__middleware__remote_ip__tests__parsing.snap",
    "content": "---\nsource: src/controller/middleware/remote_ip.rs\nexpression: res\n---\nNone\n"
  },
  {
    "path": "src/controller/middleware/snapshots/loco_rs__controller__middleware__request_id__tests__create_or_fetch_request_id-2.snap",
    "content": "---\nsource: src/controller/middleware/request_id.rs\nexpression: id.len()\n---\n36\n"
  },
  {
    "path": "src/controller/middleware/snapshots/loco_rs__controller__middleware__request_id__tests__create_or_fetch_request_id-3.snap",
    "content": "---\nsource: src/controller/middleware/request_id.rs\nexpression: id.len()\n---\n36\n"
  },
  {
    "path": "src/controller/middleware/snapshots/loco_rs__controller__middleware__request_id__tests__create_or_fetch_request_id-4.snap",
    "content": "---\nsource: src/controller/middleware/request_id.rs\nexpression: id.len()\n---\n255\n"
  },
  {
    "path": "src/controller/middleware/snapshots/loco_rs__controller__middleware__request_id__tests__create_or_fetch_request_id-5.snap",
    "content": "---\nsource: src/controller/middleware/request_id.rs\nexpression: id.len()\n---\n36\n"
  },
  {
    "path": "src/controller/middleware/snapshots/loco_rs__controller__middleware__request_id__tests__create_or_fetch_request_id.snap",
    "content": "---\nsource: src/controller/middleware/request_id.rs\nexpression: id\n---\n\"foo-barbaz\"\n"
  },
  {
    "path": "src/controller/middleware/snapshots/loco_rs__controller__middleware__secure_headers__tests__can_override_headers.snap",
    "content": "---\nsource: src/controller/middleware/secure_headers.rs\nexpression: normalize_headers(response.headers())\nsnapshot_kind: text\n---\n{\n    \"content-length\": \"0\",\n    \"content-security-policy\": \"default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src https:; style-src 'self' https: 'unsafe-inline'\",\n    \"new-header\": \"baz\",\n    \"strict-transport-security\": \"max-age=631138519\",\n    \"x-content-type-options\": \"nosniff\",\n    \"x-download-options\": \"foobar\",\n    \"x-frame-options\": \"sameorigin\",\n    \"x-permitted-cross-domain-policies\": \"none\",\n    \"x-xss-protection\": \"0\",\n}\n"
  },
  {
    "path": "src/controller/middleware/snapshots/loco_rs__controller__middleware__secure_headers__tests__can_set_headers.snap",
    "content": "---\nsource: src/controller/middleware/secure_headers.rs\nexpression: res.headers()\n---\n{\n    \"content-length\": \"0\",\n    \"content-security-policy\": \"default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src https:; style-src 'self' https: 'unsafe-inline'\",\n    \"strict-transport-security\": \"max-age=631138519\",\n    \"x-content-type-options\": \"nosniff\",\n    \"x-download-options\": \"noopen\",\n    \"x-frame-options\": \"sameorigin\",\n    \"x-permitted-cross-domain-policies\": \"none\",\n    \"x-xss-protection\": \"0\",\n}\n"
  },
  {
    "path": "src/controller/middleware/snapshots/loco_rs__controller__middleware__secure_headers__tests__default_is_github_preset.snap",
    "content": "---\nsource: src/controller/middleware/secure_headers.rs\nexpression: res.headers()\n---\n{\n    \"content-length\": \"0\",\n    \"content-security-policy\": \"default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src https:; style-src 'self' https: 'unsafe-inline'\",\n    \"strict-transport-security\": \"max-age=631138519\",\n    \"x-content-type-options\": \"nosniff\",\n    \"x-download-options\": \"noopen\",\n    \"x-frame-options\": \"sameorigin\",\n    \"x-permitted-cross-domain-policies\": \"none\",\n    \"x-xss-protection\": \"0\",\n}\n"
  },
  {
    "path": "src/controller/middleware/static_assets.rs",
    "content": "//! Static Assets Middleware.\n//!\n//! This middleware serves static files (e.g., images, CSS, JS) from a specified\n//! folder to the client. It also allows configuration of a fallback file to\n//! serve in case a requested file is not found. Additionally, it can serve\n//! precompressed files if enabled via the configuration.\n//!\n//! The middleware checks if the specified folder and fallback file exist, and\n//! if either is missing, it returns an error. If the files exist, the\n//! middleware is added to the router to serve static files.\n\nuse std::path::PathBuf;\n\nuse axum::http::header::{HeaderValue, CACHE_CONTROL};\nuse axum::Router as AXRouter;\nuse serde::{Deserialize, Serialize};\nuse serde_json::json;\nuse tower_http::services::{ServeDir, ServeFile};\nuse tower_http::set_header::SetResponseHeaderLayer;\n\nuse crate::{app::AppContext, controller::middleware::MiddlewareLayer, Error, Result};\n\n/// Static asset middleware configuration\n#[derive(Debug, Clone, Deserialize, Serialize)]\npub struct StaticAssets {\n    #[serde(default)]\n    pub enable: bool,\n    /// Check that assets must exist on disk\n    #[serde(default = \"default_must_exist\")]\n    pub must_exist: bool,\n    /// Assets location\n    #[serde(default = \"default_folder_config\")]\n    pub folder: FolderConfig,\n    /// Fallback page for a case when no asset exists. Useful for SPA\n    /// (single page app) where routes are virtual.\n    #[serde(default = \"default_fallback\")]\n    pub fallback: PathBuf,\n    /// Enable `precompressed_gzip`\n    #[serde(default = \"default_precompressed\")]\n    pub precompressed: bool,\n    /// Cache control header value for static assets (e.g., \"max-age=31536000\")\n    pub cache_control: Option<String>,\n}\n\nimpl Default for StaticAssets {\n    fn default() -> Self {\n        serde_json::from_value(json!({})).unwrap()\n    }\n}\n\nfn default_must_exist() -> bool {\n    true\n}\n\nfn default_precompressed() -> bool {\n    false\n}\n\nfn default_fallback() -> PathBuf {\n    PathBuf::from(\"assets\").join(\"static\").join(\"404.html\")\n}\n\nfn default_folder_config() -> FolderConfig {\n    FolderConfig {\n        uri: \"/static\".to_string(),\n        path: PathBuf::from(\"assets/static\"),\n    }\n}\n\n#[derive(Default, Debug, Clone, Deserialize, Serialize)]\npub struct FolderConfig {\n    /// Uri for the assets\n    pub uri: String,\n    /// Path for the assets\n    pub path: PathBuf,\n}\n\n// Implement the MiddlewareTrait for your Middleware struct\nimpl MiddlewareLayer for StaticAssets {\n    /// Returns the name of the middleware.\n    fn name(&self) -> &'static str {\n        \"static\"\n    }\n\n    /// Checks if the static assets middleware is enabled.\n    fn is_enabled(&self) -> bool {\n        self.enable\n    }\n\n    fn config(&self) -> serde_json::Result<serde_json::Value> {\n        serde_json::to_value(self)\n    }\n\n    /// Applies the static assets middleware to the application router.\n    ///\n    /// This method wraps the provided [`AXRouter`] with a service to serve\n    /// static files from the folder specified in the configuration. It will\n    /// serve a fallback file if the requested file is not found, and can\n    /// also serve precompressed (gzip) files if enabled.\n    ///\n    /// Before applying, it checks if the folder and fallback file exist. If\n    /// either is missing, it returns an error.\n    fn apply(&self, app: AXRouter<AppContext>) -> Result<AXRouter<AppContext>> {\n        if self.must_exist && (!&self.folder.path.exists() || !&self.fallback.exists()) {\n            return Err(Error::Message(format!(\n                \"one of the static path are not found, Folder `{}` fallback: `{}`\",\n                self.folder.path.display(),\n                self.fallback.display(),\n            )));\n        }\n\n        let serve_dir = ServeDir::new(&self.folder.path).fallback(ServeFile::new(&self.fallback));\n\n        // Create static service with cache control if configured\n        let static_service = if let Some(cache_control) = &self.cache_control {\n            let cache_header_layer = SetResponseHeaderLayer::overriding(\n                CACHE_CONTROL,\n                HeaderValue::from_str(cache_control)\n                    .unwrap_or_else(|_| HeaderValue::from_static(\"max-age=31536000\")),\n            );\n\n            let base_service = if self.precompressed {\n                serve_dir.precompressed_gzip()\n            } else {\n                serve_dir\n            };\n\n            // Create a router with the cache control layer applied to the static service\n            AXRouter::new()\n                .fallback_service(base_service)\n                .layer(cache_header_layer)\n        } else {\n            let base_service = if self.precompressed {\n                serve_dir.precompressed_gzip()\n            } else {\n                serve_dir\n            };\n            AXRouter::new().fallback_service(base_service)\n        };\n\n        if &self.folder.uri == \"/\" {\n            Ok(app.fallback_service(static_service))\n        } else {\n            Ok(app.nest_service(&self.folder.uri, static_service))\n        }\n    }\n}\n"
  },
  {
    "path": "src/controller/middleware/static_assets_embedded.rs",
    "content": "//! Static Assets Embedded Middleware.\n//!\n//! This middleware serves static files (e.g., images, CSS, JS) from embedded\n//! assets built into the binary. It also provides a fallback file to serve in\n//! case a requested file is not found.\n//!\n//! This is particularly useful for distributing single-binary applications\n//! with all assets included, eliminating the need for external asset files.\n\nuse std::path::PathBuf;\n\nuse axum::Router as AXRouter;\nuse axum::{\n    body::Body,\n    extract::{Path as AxumPath, Request},\n    http::StatusCode,\n    response::{IntoResponse, Response},\n    routing::get,\n};\nuse serde::{Deserialize, Serialize};\nuse serde_json::json;\n\nuse crate::{app::AppContext, controller::middleware::MiddlewareLayer, Result};\n\n// Include the generated static assets at the module level\ninclude!(concat!(env!(\"OUT_DIR\"), \"/generated_code/static_assets.rs\"));\n\n/// Static asset middleware configuration\n#[derive(Debug, Clone, Deserialize, Serialize)]\npub struct StaticAssets {\n    #[serde(default)]\n    pub enable: bool,\n    /// Check that assets must exist on disk\n    #[serde(default = \"default_must_exist\")]\n    pub must_exist: bool,\n    /// Assets location\n    #[serde(default = \"default_folder_config\")]\n    pub folder: FolderConfig,\n    /// Fallback page for a case when no asset exists. Useful for SPA\n    /// (single page app) where routes are virtual.\n    #[serde(default = \"default_fallback\")]\n    pub fallback: PathBuf,\n    /// Enable `precompressed_gzip`\n    #[serde(default = \"default_precompressed\")]\n    pub precompressed: bool,\n}\n\nimpl Default for StaticAssets {\n    fn default() -> Self {\n        serde_json::from_value(json!({})).unwrap()\n    }\n}\n\nfn default_must_exist() -> bool {\n    true\n}\n\nfn default_precompressed() -> bool {\n    false\n}\n\nfn default_fallback() -> PathBuf {\n    PathBuf::from(\"assets\").join(\"static\").join(\"404.html\")\n}\n\nfn default_folder_config() -> FolderConfig {\n    FolderConfig {\n        uri: \"/static\".to_string(),\n        path: PathBuf::from(\"assets/static\"),\n    }\n}\n\n#[derive(Default, Debug, Clone, Deserialize, Serialize)]\npub struct FolderConfig {\n    /// Uri for the assets\n    pub uri: String,\n    /// Path for the assets\n    pub path: PathBuf,\n}\n\n#[derive(Clone)]\npub struct EmbeddedAssets {\n    fallback_content: &'static [u8],\n}\n\nimpl EmbeddedAssets {\n    fn new(fallback_path: &str) -> Self {\n        tracing::info!(\n            \"Initializing embedded static assets with fallback path: {}\",\n            fallback_path\n        );\n\n        let assets = get_embedded_static_assets();\n        tracing::info!(\"Loaded {} embedded static assets\", assets.len());\n\n        // Log what assets are available\n        let available_files: Vec<String> = assets.keys().cloned().collect();\n        tracing::info!(\"Available embedded assets: {:?}\", available_files);\n\n        // Try to get the fallback content or use a default empty bytes\n        let fallback = assets.get(fallback_path).copied().unwrap_or_else(|| {\n            tracing::warn!(\n                \"Fallback file not found in embedded assets: {}\",\n                fallback_path\n            );\n\n            // Generate a static fallback page\n            let fallback_html = concat!(\n                \"<!DOCTYPE html><html><body>\",\n                \"<h1>404 - Not Found</h1>\",\n                \"</body></html>\"\n            );\n\n            fallback_html.as_bytes()\n        });\n\n        Self {\n            fallback_content: fallback,\n        }\n    }\n\n    fn serve(&self, uri: &str) -> impl IntoResponse {\n        let assets = get_embedded_static_assets();\n\n        assets.get(uri).map_or_else(\n            || {\n                tracing::warn!(\"Static asset not found: {}, serving fallback\", uri);\n                Response::builder()\n                    .status(StatusCode::NOT_FOUND)\n                    .header(\"content-type\", \"text/html\")\n                    .body(Body::from(self.fallback_content))\n                    .unwrap()\n            },\n            |content| {\n                // Set appropriate content type based on file extension\n                let content_type = match uri.rsplit('.').next() {\n                    Some(\"css\") => \"text/css\",\n                    Some(\"js\") => \"application/javascript\",\n                    Some(\"html\") => \"text/html\",\n                    Some(\"png\") => \"image/png\",\n                    Some(\"jpg\" | \"jpeg\") => \"image/jpeg\",\n                    Some(\"svg\") => \"image/svg+xml\",\n                    Some(\"ico\") => \"image/x-icon\",\n                    Some(\"json\") => \"application/json\",\n                    Some(\"woff\") => \"font/woff\",\n                    Some(\"woff2\") => \"font/woff2\",\n                    Some(\"ttf\") => \"font/ttf\",\n                    Some(\"eot\") => \"application/vnd.ms-fontobject\",\n                    Some(\"otf\") => \"font/otf\",\n                    _ => \"application/octet-stream\",\n                };\n\n                tracing::debug!(\"Serving embedded static asset: {}\", uri);\n                Response::builder()\n                    .status(StatusCode::OK)\n                    .header(\"content-type\", content_type)\n                    .body(Body::from(*content))\n                    .unwrap()\n            },\n        )\n    }\n}\n\n// Implement the MiddlewareTrait for your Middleware struct\nimpl MiddlewareLayer for StaticAssets {\n    /// Returns the name of the middleware.\n    fn name(&self) -> &'static str {\n        \"static\"\n    }\n\n    /// Checks if the static assets middleware is enabled.\n    fn is_enabled(&self) -> bool {\n        self.enable\n    }\n\n    fn config(&self) -> serde_json::Result<serde_json::Value> {\n        serde_json::to_value(self)\n    }\n\n    /// Applies the static assets middleware to the application router.\n    ///\n    /// This method wraps the provided [`AXRouter`] with a service to serve\n    /// static files from embedded assets built into the binary.\n    fn apply(&self, app: AXRouter<AppContext>) -> Result<AXRouter<AppContext>> {\n        let fallback_path = format!(\n            \"/{}\",\n            self.fallback\n                .strip_prefix(\"assets\")\n                .unwrap_or(&self.fallback)\n                .display()\n                .to_string()\n                .replace('\\\\', \"/\")\n        );\n        let embedded_assets = EmbeddedAssets::new(&fallback_path);\n        let base_uri = self.folder.uri.clone();\n\n        if &base_uri == \"/\" {\n            Ok(app.fallback(move |req: Request| {\n                let uri = req.uri().path().to_string();\n                let assets = embedded_assets.clone();\n                async move { assets.serve(&uri) }\n            }))\n        } else {\n            Ok(app.route(\n                &format!(\"{base_uri}/{{*path}}\"),\n                get(move |AxumPath(path): AxumPath<String>| {\n                    let uri = format!(\"{base_uri}/{path}\");\n                    let assets = embedded_assets.clone();\n                    async move { assets.serve(&uri) }\n                }),\n            ))\n        }\n    }\n}\n"
  },
  {
    "path": "src/controller/middleware/timeout.rs",
    "content": "//! Timeout Request Middleware.\n//!\n//! This middleware applies a timeout to requests processed by the application.\n//! The timeout duration is configurable and defined via the\n//! [`TimeoutRequestMiddleware`] configuration. The middleware ensures that\n//! requests do not run beyond the specified timeout period, improving the\n//! overall performance and responsiveness of the application.\n//!\n//! If a request exceeds the specified timeout duration, the middleware will\n//! return a `408 Request Timeout` status code to the client, indicating that\n//! the request took too long to process.\nuse std::time::Duration;\n\nuse axum::http::StatusCode;\nuse axum::Router as AXRouter;\nuse serde::{Deserialize, Serialize};\nuse serde_json::json;\nuse tower_http::timeout::TimeoutLayer;\n\nuse crate::{app::AppContext, controller::middleware::MiddlewareLayer, Result};\n\n/// Timeout middleware configuration\n#[derive(Debug, Clone, Deserialize, Serialize)]\npub struct TimeOut {\n    #[serde(default)]\n    pub enable: bool,\n    // Timeout request in milliseconds\n    #[serde(default = \"default_timeout\")]\n    pub timeout: u64,\n}\n\nimpl Default for TimeOut {\n    fn default() -> Self {\n        serde_json::from_value(json!({})).unwrap()\n    }\n}\n\nfn default_timeout() -> u64 {\n    5_000\n}\n\nimpl MiddlewareLayer for TimeOut {\n    /// Returns the name of the middleware.\n    fn name(&self) -> &'static str {\n        \"timeout_request\"\n    }\n\n    /// Checks if the timeout middleware is enabled.\n    fn is_enabled(&self) -> bool {\n        self.enable\n    }\n\n    fn config(&self) -> serde_json::Result<serde_json::Value> {\n        serde_json::to_value(self)\n    }\n\n    /// Applies the timeout middleware to the application router.\n    ///\n    /// This method wraps the provided [`AXRouter`] in a [`TimeoutLayer`],\n    /// ensuring that requests exceeding the specified timeout duration will\n    /// be interrupted.\n    fn apply(&self, app: AXRouter<AppContext>) -> Result<AXRouter<AppContext>> {\n        Ok(app.layer(TimeoutLayer::with_status_code(\n            StatusCode::REQUEST_TIMEOUT,\n            Duration::from_millis(self.timeout),\n        )))\n    }\n}\n"
  },
  {
    "path": "src/controller/mod.rs",
    "content": "//! Manage web server routing\n//!\n//! # Example\n//!\n//! This example you can adding custom routes into your application by\n//! implementing routes trait from [`crate::app::Hooks`] and adding your\n//! endpoints to your application\n//!\n//! ```rust\n//! use async_trait::async_trait;\n//! use loco_rs::{\n//!    app::{AppContext, Hooks},\n//!    boot::{create_app, BootResult, StartMode},\n//!    config::Config,\n//!    controller::AppRoutes,\n//!    prelude::*,\n//!    task::Tasks,\n//!    environment::Environment,\n//!    Result,\n//! };\n//! use sea_orm::DatabaseConnection;\n//! use std::path::Path;\n//!\n//! /// this code block should be taken from the sea_orm migration model.\n//! pub struct App;\n//! pub use sea_orm_migration::prelude::*;\n//! pub struct Migrator;\n//! #[async_trait::async_trait]\n//! impl MigratorTrait for Migrator {\n//!     fn migrations() -> Vec<Box<dyn MigrationTrait>> {\n//!         vec![]\n//!     }\n//! }\n//!\n//! #[async_trait]\n//! impl Hooks for App {\n//!\n//!    fn app_name() -> &'static str {\n//!        env!(\"CARGO_CRATE_NAME\")\n//!    }\n//!\n//!     fn routes(ctx: &AppContext) -> AppRoutes {\n//!         AppRoutes::with_default_routes()\n//!             // .add_route(controllers::notes::routes())\n//!     }\n//!\n//!     async fn boot(mode: StartMode, environment: &Environment, config: Config) -> Result<BootResult>{\n//!          create_app::<Self, Migrator>(mode, environment, config).await\n//!     }\n//!\n//!     async fn connect_workers(_ctx: &AppContext, _queue: &Queue) -> Result<()> {\n//!         Ok(())\n//!     }\n//!\n//!\n//!     fn register_tasks(tasks: &mut Tasks) {}\n//!\n//!     async fn truncate(_ctx: &AppContext) -> Result<()> {\n//!         Ok(())\n//!     }\n//!\n//!     async fn seed(_ctx: &AppContext, base: &Path) -> Result<()> {\n//!         Ok(())\n//!     }\n//! }\n//! ```\n\npub use app_routes::{AppRoutes, ListRoutes};\nuse axum::{\n    extract::FromRequest,\n    http::StatusCode,\n    response::{IntoResponse, Response},\n};\nuse colored::Colorize;\npub use routes::Routes;\nuse serde::Serialize;\n\nuse crate::{errors::Error, Result};\n\nmod app_routes;\nmod backtrace;\nmod describe;\npub mod extractor;\npub mod format;\npub mod middleware;\npub mod monitoring;\nmod routes;\npub mod views;\n\n/// Create an unauthorized error with a specified message.\n///\n/// This function is used to generate an `Error::Unauthorized` variant with a\n/// custom message.\n///\n/// # Errors\n///\n/// returns unauthorized enum\n///\n/// # Example\n///\n/// ```rust\n/// use loco_rs::prelude::*;\n///\n/// async fn login() -> Result<Response> {\n///     let valid = false;\n///     if !valid {\n///         return unauthorized(\"unauthorized access\");\n///     }\n///     format::json(())\n/// }\n/// ````\npub fn unauthorized<T: Into<String>, U>(msg: T) -> Result<U> {\n    Err(Error::Unauthorized(msg.into()))\n}\n\n/// Return a bad request with a message\n///\n/// # Errors\n///\n/// This function will return an error result\npub fn bad_request<T: Into<String>, U>(msg: T) -> Result<U> {\n    Err(Error::BadRequest(msg.into()))\n}\n\n/// return not found status code\n///\n/// # Errors\n/// Currently this function doesn't return any error. this is for feature\n/// functionality\npub fn not_found<T>() -> Result<T> {\n    Err(Error::NotFound)\n}\n#[derive(Debug, Serialize)]\n/// Structure representing details about an error.\npub struct ErrorDetail {\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub error: Option<String>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub description: Option<String>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub errors: Option<serde_json::Value>,\n}\n\nimpl ErrorDetail {\n    /// Create a new `ErrorDetail` with the specified error and description.\n    #[must_use]\n    pub fn new<T1: Into<String> + AsRef<str>, T2: Into<String> + AsRef<str>>(\n        error: T1,\n        description: T2,\n    ) -> Self {\n        let description = (!description.as_ref().is_empty()).then(|| description.into());\n        Self {\n            error: Some(error.into()),\n            description,\n            errors: None,\n        }\n    }\n\n    /// Create an `ErrorDetail` with only an error reason and no description.\n    #[must_use]\n    pub fn with_reason<T: Into<String>>(error: T) -> Self {\n        Self {\n            error: Some(error.into()),\n            description: None,\n            errors: None,\n        }\n    }\n}\n\n#[derive(Debug, FromRequest)]\n#[from_request(via(axum::Json), rejection(Error))]\npub struct Json<T>(pub T);\n\nimpl<T: Serialize> IntoResponse for Json<T> {\n    fn into_response(self) -> axum::response::Response {\n        axum::Json(self.0).into_response()\n    }\n}\n\nimpl IntoResponse for Error {\n    /// Convert an `Error` into an HTTP response.\n    #[allow(clippy::cognitive_complexity)]\n    fn into_response(self) -> Response {\n        match &self {\n            Self::WithBacktrace {\n                inner,\n                backtrace: _,\n            } => {\n                tracing::error!(\n                error.msg = %inner,\n                error.details = ?inner,\n                \"controller_error\"\n                );\n            }\n            err => {\n                tracing::error!(\n                error.msg = %err,\n                error.details = ?err,\n                \"controller_error\"\n                );\n            }\n        }\n\n        let public_facing_error = match self {\n            Self::NotFound => (\n                StatusCode::NOT_FOUND,\n                ErrorDetail::new(\"not_found\", \"Resource was not found\"),\n            ),\n            Self::Unauthorized(err) => {\n                tracing::warn!(err);\n                (\n                    StatusCode::UNAUTHORIZED,\n                    ErrorDetail::new(\n                        \"unauthorized\",\n                        \"You do not have permission to access this resource\",\n                    ),\n                )\n            }\n            Self::CustomError(status_code, data) => (status_code, data),\n            Self::WithBacktrace { inner, backtrace } => {\n                println!(\"\\n{}\", inner.to_string().red().underline());\n                backtrace::print_backtrace(&backtrace).unwrap();\n                (\n                    StatusCode::BAD_REQUEST,\n                    ErrorDetail::with_reason(\"Bad Request\"),\n                )\n            }\n            Self::BadRequest(err) => (\n                StatusCode::BAD_REQUEST,\n                ErrorDetail::new(\"Bad Request\", &err),\n            ),\n            Self::JsonRejection(err) => {\n                tracing::debug!(err = err.body_text(), \"json rejection\");\n                (err.status(), ErrorDetail::with_reason(\"Bad Request\"))\n            }\n\n            Self::Validation(ref errors) => (\n                StatusCode::BAD_REQUEST,\n                ErrorDetail {\n                    error: None,\n                    description: None,\n                    errors: Some(serde_json::to_value(&errors.errors).unwrap_or_default()),\n                },\n            ),\n            _ => (\n                StatusCode::INTERNAL_SERVER_ERROR,\n                ErrorDetail::new(\"internal_server_error\", \"Internal Server Error\"),\n            ),\n        };\n\n        (public_facing_error.0, Json(public_facing_error.1)).into_response()\n    }\n}\n"
  },
  {
    "path": "src/controller/monitoring.rs",
    "content": "//! This module contains a base routes related to readiness checks and status\n//! reporting. These routes are commonly used to monitor the readiness of the\n//! application and its dependencies.\n\nuse super::{format, routes::Routes};\n#[cfg(any(feature = \"cache_inmem\", feature = \"cache_redis\"))]\nuse crate::config;\nuse crate::{app::AppContext, Result};\nuse axum::{\n    extract::State,\n    http::StatusCode,\n    response::{IntoResponse, Response},\n    routing::get,\n};\nuse serde::Serialize;\n\n/// Represents the health status of the application.\n#[derive(Serialize)]\npub struct Health {\n    pub ok: bool,\n}\n\n/// Check application ping endpoint\n///\n/// # Errors\n/// This function always returns `Ok` with a JSON response indicating the\npub async fn ping() -> Result<Response> {\n    format::json(Health { ok: true })\n}\n\n/// Check application ping endpoint\n///\n/// # Errors\n/// This function always returns `Ok` with a JSON response indicating the\npub async fn health() -> Result<Response> {\n    format::json(Health { ok: true })\n}\n\n/// Check the readiness of the application by sending a ping request to\n/// Redis or the DB (depending on feature flags) to ensure connection liveness.\n///\n/// # Errors\n/// All errors are logged, and the readiness status is returned as a JSON response.\npub async fn readiness(State(ctx): State<AppContext>) -> (StatusCode, Response) {\n    // Check database connection\n    #[cfg(feature = \"with-db\")]\n    if let Err(error) = &ctx.db.ping().await {\n        tracing::error!(err.msg = %error, err.detail = ?error, \"readiness_db_ping_error\");\n        return (\n            StatusCode::SERVICE_UNAVAILABLE,\n            format::json(Health { ok: false }).into_response(),\n        );\n    }\n\n    // Check queue connection\n    if let Some(queue) = &ctx.queue_provider {\n        if let Err(error) = queue.ping().await {\n            tracing::error!(err.msg = %error, err.detail = ?error, \"readiness_queue_ping_error\");\n            return (\n                StatusCode::SERVICE_UNAVAILABLE,\n                format::json(Health { ok: false }).into_response(),\n            );\n        }\n    }\n\n    // Check cache connection\n    #[cfg(any(feature = \"cache_inmem\", feature = \"cache_redis\"))]\n    {\n        match ctx.config.cache {\n            #[cfg(feature = \"cache_inmem\")]\n            config::CacheConfig::InMem(_) => {\n                if let Err(error) = &ctx.cache.driver.ping().await {\n                    tracing::error!(err.msg = %error, err.detail = ?error, \"readiness_cache_ping_error\");\n                    return (\n                        StatusCode::SERVICE_UNAVAILABLE,\n                        format::json(Health { ok: false }).into_response(),\n                    );\n                }\n            }\n            #[cfg(feature = \"cache_redis\")]\n            config::CacheConfig::Redis(_) => {\n                if let Err(error) = &ctx.cache.driver.ping().await {\n                    tracing::error!(err.msg = %error, err.detail = ?error, \"readiness_cache_ping_error\");\n                    return (\n                        StatusCode::SERVICE_UNAVAILABLE,\n                        format::json(Health { ok: false }).into_response(),\n                    );\n                }\n            }\n            config::CacheConfig::Null => (),\n        }\n    }\n\n    (\n        StatusCode::OK,\n        format::json(Health { ok: true }).into_response(),\n    )\n}\n\n/// Defines and returns the readiness-related routes.\npub fn routes() -> Routes {\n    Routes::new()\n        .add(\"/_readiness\", get(readiness))\n        .add(\"/_ping\", get(ping))\n        .add(\"/_health\", get(health))\n}\n\n#[cfg(test)]\nmod tests {\n    use axum::routing::get;\n    use loco_rs::tests_cfg::db::fail_connection;\n    use loco_rs::{bgworker, cache, config, controller::monitoring, tests_cfg};\n    use serde_json::Value;\n    use tower::ServiceExt;\n\n    #[cfg(feature = \"cache_redis\")]\n    use crate::tests_cfg::redis::setup_redis_container;\n\n    #[tokio::test]\n    async fn ping_works() {\n        let ctx = tests_cfg::app::get_app_context().await;\n\n        // Create a router with the ping route\n        let router = axum::Router::new()\n            .route(\"/_ping\", get(monitoring::ping))\n            .with_state(ctx);\n\n        // Create a request\n        let req = axum::http::Request::builder()\n            .uri(\"/_ping\")\n            .method(\"GET\")\n            .body(axum::body::Body::empty())\n            .unwrap();\n\n        // Test the router directly using oneshot\n        let response = router.oneshot(req).await.unwrap();\n        assert_eq!(response.status(), 200);\n\n        // Get the response body\n        let body = axum::body::to_bytes(response.into_body(), usize::MAX)\n            .await\n            .unwrap();\n        let res_json: Value = serde_json::from_slice(&body).expect(\"Valid JSON response\");\n        assert_eq!(res_json[\"ok\"], true);\n    }\n\n    #[tokio::test]\n    async fn health_works() {\n        let ctx = tests_cfg::app::get_app_context().await;\n\n        // Create a router with the health route\n        let router = axum::Router::new()\n            .route(\"/_health\", get(monitoring::health))\n            .with_state(ctx);\n\n        // Create a request\n        let req = axum::http::Request::builder()\n            .uri(\"/_health\")\n            .method(\"GET\")\n            .body(axum::body::Body::empty())\n            .unwrap();\n\n        // Test the router directly using oneshot\n        let response = router.oneshot(req).await.unwrap();\n        assert_eq!(response.status(), 200);\n\n        // Get the response body\n        let body = axum::body::to_bytes(response.into_body(), usize::MAX)\n            .await\n            .unwrap();\n        let res_json: Value = serde_json::from_slice(&body).expect(\"Valid JSON response\");\n        assert_eq!(res_json[\"ok\"], true);\n    }\n\n    #[cfg(not(feature = \"with-db\"))]\n    #[tokio::test]\n    async fn readiness_no_features() {\n        let ctx = tests_cfg::app::get_app_context().await;\n\n        // Create a router with the readiness route\n        let router = axum::Router::new()\n            .route(\"/_readiness\", get(monitoring::readiness))\n            .with_state(ctx);\n\n        // Create a request\n        let req = axum::http::Request::builder()\n            .uri(\"/_readiness\")\n            .method(\"GET\")\n            .body(axum::body::Body::empty())\n            .unwrap();\n\n        // Test the router directly using oneshot\n        let response = router.oneshot(req).await.unwrap();\n        assert_eq!(response.status(), 200);\n\n        // Get the response body\n        let body = axum::body::to_bytes(response.into_body(), usize::MAX)\n            .await\n            .unwrap();\n        let res_json: Value = serde_json::from_slice(&body).expect(\"Valid JSON response\");\n        assert_eq!(res_json[\"ok\"], true);\n    }\n\n    #[cfg(feature = \"with-db\")]\n    #[tokio::test]\n    async fn readiness_with_db_success() {\n        let ctx = tests_cfg::app::get_app_context().await;\n\n        // Create a router with the readiness route\n        let router = axum::Router::new()\n            .route(\"/_readiness\", get(monitoring::readiness))\n            .with_state(ctx);\n\n        // Create a request\n        let req = axum::http::Request::builder()\n            .uri(\"/_readiness\")\n            .method(\"GET\")\n            .body(axum::body::Body::empty())\n            .unwrap();\n\n        // Test the router directly using oneshot\n        let response = router.oneshot(req).await.unwrap();\n        assert_eq!(response.status(), 200);\n\n        // Get the response body\n        let body = axum::body::to_bytes(response.into_body(), usize::MAX)\n            .await\n            .unwrap();\n        let res_json: Value = serde_json::from_slice(&body).expect(\"Valid JSON response\");\n        assert_eq!(res_json[\"ok\"], true);\n    }\n\n    #[cfg(feature = \"with-db\")]\n    #[tokio::test]\n    async fn readiness_with_db_failure() {\n        let mut ctx = tests_cfg::app::get_app_context().await;\n        ctx.db = fail_connection().await;\n\n        // Create a router with the readiness route\n        let router = axum::Router::new()\n            .route(\"/_readiness\", get(monitoring::readiness))\n            .with_state(ctx);\n\n        // Create a request\n        let req = axum::http::Request::builder()\n            .uri(\"/_readiness\")\n            .method(\"GET\")\n            .body(axum::body::Body::empty())\n            .unwrap();\n\n        // Test the router directly using oneshot\n        let response = router.oneshot(req).await.unwrap();\n        assert_eq!(response.status(), 503);\n\n        // Get the response body\n        let body = axum::body::to_bytes(response.into_body(), usize::MAX)\n            .await\n            .unwrap();\n        let res_json: Value = serde_json::from_slice(&body).expect(\"Valid JSON response\");\n        assert_eq!(res_json[\"ok\"], false);\n    }\n\n    #[cfg(feature = \"cache_inmem\")]\n    #[tokio::test]\n    async fn readiness_with_cache_inmem() {\n        let mut ctx = tests_cfg::app::get_app_context().await;\n\n        ctx.cache = cache::drivers::inmem::new(&loco_rs::config::InMemCacheConfig {\n            max_capacity: 32 * 1024 * 1024,\n        })\n        .into();\n\n        // Create a router with the readiness route\n        let router = axum::Router::new()\n            .route(\"/_readiness\", get(monitoring::readiness))\n            .with_state(ctx);\n\n        // Create a request\n        let req = axum::http::Request::builder()\n            .uri(\"/_readiness\")\n            .method(\"GET\")\n            .body(axum::body::Body::empty())\n            .unwrap();\n\n        // Test the router directly using oneshot\n        let response = router.oneshot(req).await.unwrap();\n        assert_eq!(response.status(), 200);\n\n        // Get the response body\n        let body = axum::body::to_bytes(response.into_body(), usize::MAX)\n            .await\n            .unwrap();\n        let res_json: Value = serde_json::from_slice(&body).expect(\"Valid JSON response\");\n        assert_eq!(res_json[\"ok\"], true);\n    }\n\n    #[cfg(feature = \"cache_redis\")]\n    #[tokio::test]\n    async fn readiness_with_cache_redis_success() {\n        let (redis_url, _container) = setup_redis_container().await;\n        let mut ctx = tests_cfg::app::get_app_context().await;\n\n        // Create Redis cache driver and assign to ctx.cache\n        let redis_cache = cache::drivers::redis::new(&config::RedisCacheConfig {\n            uri: redis_url,\n            max_size: 10,\n        })\n        .await\n        .expect(\"Failed to create Redis cache\");\n        ctx.cache = redis_cache.into();\n\n        // Create a router with the readiness route\n        let router = axum::Router::new()\n            .route(\"/_readiness\", get(monitoring::readiness))\n            .with_state(ctx);\n\n        // Create a request\n        let req = axum::http::Request::builder()\n            .uri(\"/_readiness\")\n            .method(\"GET\")\n            .body(axum::body::Body::empty())\n            .unwrap();\n\n        // Test the router directly using oneshot\n        let response = router.oneshot(req).await.unwrap();\n        assert_eq!(response.status(), 200);\n\n        // Get the response body\n        let body = axum::body::to_bytes(response.into_body(), usize::MAX)\n            .await\n            .unwrap();\n        let res_json: Value = serde_json::from_slice(&body).expect(\"Valid JSON response\");\n        assert_eq!(res_json[\"ok\"], true);\n    }\n\n    #[cfg(feature = \"cache_redis\")]\n    #[tokio::test]\n    async fn readiness_with_cache_redis_failure() {\n        let mut ctx = tests_cfg::app::get_app_context().await;\n        let failour_redis_url = \"redis://127.0.0.2:0\";\n        // Force config to Redis to ensure ping path executes, but swap driver to Null (which errors on ping)\n        ctx.config.cache = config::CacheConfig::Redis(loco_rs::config::RedisCacheConfig {\n            uri: failour_redis_url.to_string(),\n            max_size: 10,\n        });\n        // Create Redis cache driver and assign to ctx.cache\n        ctx.cache = cache::drivers::redis::new(&config::RedisCacheConfig {\n            uri: failour_redis_url.to_string(),\n            max_size: 10,\n        })\n        .await\n        .expect(\"Failed to create Redis cache\")\n        .into();\n\n        // Create a router with the readiness route\n        let router = axum::Router::new()\n            .route(\"/_readiness\", get(monitoring::readiness))\n            .with_state(ctx);\n\n        // Create a request\n        let req = axum::http::Request::builder()\n            .uri(\"/_readiness\")\n            .method(\"GET\")\n            .body(axum::body::Body::empty())\n            .unwrap();\n\n        // Test the router directly using oneshot\n        let response = router.oneshot(req).await.unwrap();\n        assert_eq!(response.status(), 503);\n\n        // Get the response body\n        let body = axum::body::to_bytes(response.into_body(), usize::MAX)\n            .await\n            .unwrap();\n        let res_json: Value = serde_json::from_slice(&body).expect(\"Valid JSON response\");\n        assert_eq!(res_json[\"ok\"], false);\n    }\n\n    #[tokio::test]\n    async fn readiness_with_queue_not_present() {\n        let mut ctx = tests_cfg::app::get_app_context().await;\n        // simulate background queue mode with a no-op provider\n        ctx.config.workers.mode = config::WorkerMode::BackgroundQueue;\n        ctx.queue_provider = Some(std::sync::Arc::new(bgworker::Queue::None));\n\n        // Create a router with the readiness route\n        let router = axum::Router::new()\n            .route(\"/_readiness\", get(monitoring::readiness))\n            .with_state(ctx);\n\n        // Create a request\n        let req = axum::http::Request::builder()\n            .uri(\"/_readiness\")\n            .method(\"GET\")\n            .body(axum::body::Body::empty())\n            .unwrap();\n\n        // Test the router directly using oneshot\n        let response = router.oneshot(req).await.unwrap();\n        assert_eq!(response.status(), 200);\n\n        // Get the response body\n        let body = axum::body::to_bytes(response.into_body(), usize::MAX)\n            .await\n            .unwrap();\n        let res_json: Value = serde_json::from_slice(&body).expect(\"Valid JSON response\");\n        assert_eq!(res_json[\"ok\"], true);\n    }\n\n    #[cfg(feature = \"bg_redis\")]\n    #[tokio::test]\n    async fn readiness_with_queue_present_failure() {\n        let mut ctx = tests_cfg::app::get_app_context().await;\n\n        // Configure Redis queue with invalid URL to trigger failure\n        let failure_redis_url = \"redis://127.0.0.2:0\";\n        ctx.config.workers.mode = config::WorkerMode::BackgroundQueue;\n        ctx.config.queue = Some(config::QueueConfig::Redis(config::RedisQueueConfig {\n            uri: failure_redis_url.to_string(),\n            dangerously_flush: false,\n            queues: None,\n            num_workers: 1,\n        }));\n\n        // Create Redis queue provider directly with failing Redis connection\n        ctx.queue_provider = Some(std::sync::Arc::new(\n            bgworker::redis::create_provider(&config::RedisQueueConfig {\n                uri: failure_redis_url.to_string(),\n                dangerously_flush: false,\n                queues: None,\n                num_workers: 1,\n            })\n            .await\n            .expect(\"Failed to create Redis queue provider\"),\n        ));\n\n        // Create a router with the readiness route\n        let router = axum::Router::new()\n            .route(\"/_readiness\", get(monitoring::readiness))\n            .with_state(ctx);\n\n        // Create a request\n        let req = axum::http::Request::builder()\n            .uri(\"/_readiness\")\n            .method(\"GET\")\n            .body(axum::body::Body::empty())\n            .unwrap();\n\n        // Test the router directly using oneshot\n        let response = router.oneshot(req).await.unwrap();\n        assert_eq!(response.status(), 503);\n\n        // Get the response body\n        let body = axum::body::to_bytes(response.into_body(), usize::MAX)\n            .await\n            .unwrap();\n        let res_json: Value = serde_json::from_slice(&body).expect(\"Valid JSON response\");\n        assert_eq!(res_json[\"ok\"], false);\n    }\n}\n"
  },
  {
    "path": "src/controller/routes.rs",
    "content": "use std::convert::Infallible;\n\nuse axum::{extract::Request, response::IntoResponse, routing::Route};\nuse tower::{Layer, Service};\n\nuse super::describe;\nuse crate::app::AppContext;\n#[derive(Clone, Default, Debug)]\npub struct Routes {\n    pub prefix: Option<String>,\n    pub handlers: Vec<Handler>,\n    // pub version: Option<String>,\n}\n\n#[derive(Clone, Default, Debug)]\npub struct Handler {\n    pub uri: String,\n    pub method: axum::routing::MethodRouter<AppContext>,\n    pub actions: Vec<axum::http::Method>,\n}\n\nimpl Routes {\n    /// Creates a new [`Routes`] instance with default settings.\n    #[must_use]\n    pub fn new() -> Self {\n        Self::default()\n    }\n\n    /// Set a prefix for the routes. this prefix will be a prefix for all the\n    /// routes.\n    ///\n    /// # Example\n    ///\n    /// In the following example the we are adding `status`  as a prefix to the\n    /// _ping endpoint HOST/status/_ping.\n    ///\n    /// ```rust\n    /// use loco_rs::prelude::*;\n    /// use serde::Serialize;;\n    ///\n    /// #[derive(Serialize)]\n    /// struct Health {\n    ///    pub ok: bool,\n    /// }\n    ///\n    /// async fn ping() -> Result<Response> {\n    ///     format::json(Health { ok: true })\n    /// }\n    /// Routes::at(\"status\").add(\"/_ping\", get(ping));\n    ///    \n    /// ````\n    #[must_use]\n    pub fn at(prefix: &str) -> Self {\n        Self {\n            prefix: Some(prefix.to_string()),\n            ..Self::default()\n        }\n    }\n\n    /// Adding new router\n    ///\n    /// # Example\n    ///\n    /// This example preset how to add a get endpoint int the Router.\n    ///\n    /// ```rust\n    /// use loco_rs::prelude::*;\n    /// use serde::Serialize;\n    ///\n    /// #[derive(Serialize)]\n    /// struct Health {\n    ///    pub ok: bool,\n    /// }\n    ///\n    /// async fn ping() -> Result<Response> {\n    ///     format::json(Health { ok: true })\n    /// }\n    /// Routes::new().add(\"/_ping\", get(ping));\n    /// ````\n    #[must_use]\n    pub fn add(mut self, uri: &str, method: axum::routing::MethodRouter<AppContext>) -> Self {\n        describe::method_action(&method);\n        self.handlers.push(Handler {\n            uri: uri.to_owned(),\n            actions: describe::method_action(&method),\n            method,\n        });\n        self\n    }\n\n    /// Merge another Routes instance into this one.\n    ///\n    /// This method allows you to combine multiple Routes instances into a single\n    /// Routes struct. All handlers from the other Routes will be added to this one.\n    /// This is useful for collecting routes from different controllers before\n    /// nesting them under a common prefix.\n    ///\n    /// # Example\n    ///\n    /// ```rust\n    /// use loco_rs::prelude::*;\n    /// use axum::routing::{get, post};\n    ///\n    /// async fn list_users() -> Result<Response> {\n    ///     format::json(\"users list\")\n    /// }\n    ///\n    /// async fn create_user() -> Result<Response> {\n    ///     format::json(\"user created\")\n    /// }\n    ///\n    /// async fn list_products() -> Result<Response> {\n    ///     format::json(\"products list\")\n    /// }\n    ///\n    /// async fn create_product() -> Result<Response> {\n    ///     format::json(\"product created\")\n    /// }\n    ///\n    /// // Create separate route groups\n    /// let user_routes = Routes::new()\n    ///     .add(\"/users\", get(list_users))\n    ///     .add(\"/users\", post(create_user));\n    ///\n    /// let product_routes = Routes::new()\n    ///     .add(\"/products\", get(list_products))\n    ///     .add(\"/products\", post(create_product));\n    ///\n    /// // Merge them into a single Routes instance\n    /// let api_routes = Routes::new()\n    ///     .merge(user_routes)\n    ///     .merge(product_routes);\n    ///\n    /// // Now nest the combined routes under /api\n    /// let app_routes = Routes::new()\n    ///     .add(\"/health\", get(|| async { \"ok\" }))\n    ///     .nest(\"/api\", api_routes);\n    ///\n    /// // This will result in routes:\n    /// // - GET /health\n    /// // - GET /api/users\n    /// // - POST /api/users\n    /// // - GET /api/products\n    /// // - POST /api/products\n    /// ```\n    #[must_use]\n    pub fn merge(mut self, other: Self) -> Self {\n        // Extend the handlers vector with all handlers from the other Routes\n        self.handlers.extend(other.handlers);\n        self\n    }\n\n    /// Merge multiple Routes instances into this one.\n    ///\n    /// This is a convenience method that allows you to merge multiple Routes\n    /// instances at once, which is particularly useful when setting up `AppRoutes`\n    /// and you want to collect routes from different controllers before nesting them.\n    ///\n    /// # Example\n    ///\n    /// ```rust\n    /// use loco_rs::prelude::*;\n    /// use axum::routing::{get, post};\n    ///\n    /// async fn list_users() -> Result<Response> {\n    ///     format::json(\"users list\")\n    /// }\n    ///\n    /// async fn list_products() -> Result<Response> {\n    ///     format::json(\"products list\")\n    /// }\n    ///\n    /// async fn list_orders() -> Result<Response> {\n    ///     format::json(\"orders list\")\n    /// }\n    ///\n    /// // Create separate route groups from different controllers\n    /// let user_routes = Routes::new().add(\"/users\", get(list_users));\n    /// let product_routes = Routes::new().add(\"/products\", get(list_products));\n    /// let order_routes = Routes::new().add(\"/orders\", get(list_orders));\n    ///\n    /// // Merge all of them at once\n    /// let api_routes = Routes::new().merge_all(vec![user_routes, product_routes, order_routes]);\n    ///\n    /// // Now nest the combined routes under /api\n    /// let app_routes = Routes::new()\n    ///     .add(\"/health\", get(|| async { \"ok\" }))\n    ///     .nest(\"/api\", api_routes);\n    ///\n    /// // This will result in routes:\n    /// // - GET /health\n    /// // - GET /api/users\n    /// // - GET /api/products\n    /// // - GET /api/orders\n    /// ```\n    #[must_use]\n    pub fn merge_all(mut self, others: Vec<Self>) -> Self {\n        // Extend the handlers vector with all handlers from all Routes\n        for other in others {\n            self.handlers.extend(other.handlers);\n        }\n        self\n    }\n\n    /// Set a prefix for the routes. this prefix will be a prefix for all the\n    /// routes.\n    ///\n    /// # Example\n    ///\n    /// In the following example the we are adding `status`  as a prefix to the\n    /// _ping endpoint HOST/status/_ping.\n    ///\n    /// ```rust\n    /// use loco_rs::prelude::*;\n    /// use serde::Serialize;\n    ///\n    /// #[derive(Serialize)]\n    /// struct Health {\n    ///    pub ok: bool,\n    /// }\n    ///\n    /// async fn ping() -> Result<Response> {\n    ///     format::json(Health { ok: true })\n    /// }\n    /// Routes::new().prefix(\"status\").add(\"/_ping\", get(ping));\n    /// ````\n    #[must_use]\n    pub fn prefix(mut self, uri: &str) -> Self {\n        self.prefix = Some(uri.to_owned());\n        self\n    }\n\n    /// Set a layer for the routes. this layer will be a layer for all the\n    /// routes.\n    ///\n    /// # Example\n    ///\n    /// In the following example, we are adding a layer to the routes.\n    ///\n    /// ```rust\n    /// use loco_rs::prelude::*;\n    /// use tower::{Layer, Service};\n    /// use tower_http::timeout::TimeoutLayer;\n    /// async fn ping() -> Result<Response> {\n    ///     format::json(\"Ok\")\n    /// }\n    /// Routes::new().prefix(\"status\").add(\"/_ping\", get(ping)).layer(TimeoutLayer::new(std::time::Duration::from_secs(5)));\n    /// ```\n    #[allow(clippy::needless_pass_by_value)]\n    #[must_use]\n    pub fn layer<L>(self, layer: L) -> Self\n    where\n        L: Layer<Route> + Clone + Send + Sync + 'static,\n        L::Service: Service<Request> + Clone + Send + Sync + 'static,\n        <L::Service as Service<Request>>::Response: IntoResponse + 'static,\n        <L::Service as Service<Request>>::Error: Into<Infallible> + 'static,\n        <L::Service as Service<Request>>::Future: Send + 'static,\n    {\n        Self {\n            prefix: self.prefix,\n            handlers: self\n                .handlers\n                .iter()\n                .map(|handler| Handler {\n                    uri: handler.uri.clone(),\n                    actions: handler.actions.clone(),\n                    method: handler.method.clone().layer(layer.clone()),\n                })\n                .collect(),\n        }\n    }\n\n    /// Nest another Routes instance under a prefix path.\n    ///\n    /// This method allows you to nest a group of routes under a specific path prefix,\n    /// similar to Axum's `nest` method. The nested routes will have their URIs\n    /// prefixed with the given path.\n    ///\n    /// # Example\n    ///\n    /// ```rust\n    /// use loco_rs::prelude::*;\n    /// use axum::routing::{get, post, delete, patch};\n    ///\n    /// // Define user-related handlers\n    /// async fn list_users() -> Result<Response> {\n    ///     format::json(\"users list\")\n    /// }\n    ///\n    /// async fn get_user() -> Result<Response> {\n    ///     format::json(\"user detail\")\n    /// }\n    ///\n    /// async fn create_user() -> Result<Response> {\n    ///     format::json(\"user created\")\n    /// }\n    ///\n    /// async fn update_user() -> Result<Response> {\n    ///     format::json(\"user updated\")\n    /// }\n    ///\n    /// async fn delete_user() -> Result<Response> {\n    ///     format::json(\"user deleted\")\n    /// }\n    ///\n    /// // Create API routes for users\n    /// let user_routes = Routes::new()\n    ///     .add(\"/users\", get(list_users))\n    ///     .add(\"/users\", post(create_user))\n    ///     .add(\"/users/{id}\", get(get_user))\n    ///     .add(\"/users/{id}\", patch(update_user))\n    ///     .add(\"/users/{id}\", delete(delete_user));\n    ///\n    /// // Create the main application routes\n    /// let app_routes = Routes::new()\n    ///     .add(\"/health\", get(|| async { \"ok\" }))\n    ///     .nest(\"/api/v1\", user_routes);\n    ///\n    /// // This will result in routes:\n    /// // - GET /health\n    /// // - GET /api/v1/users\n    /// // - POST /api/v1/users\n    /// // - GET /api/v1/users/{id}\n    /// // - PATCH /api/v1/users/{id}\n    /// // - DELETE /api/v1/users/{id}\n    /// ```\n    #[must_use]\n    pub fn nest(mut self, path: &str, nested_routes: Self) -> Self {\n        // Normalize the path to ensure it starts with / and doesn't end with /\n        let mut normalized_path = path.to_string();\n        if !normalized_path.starts_with('/') {\n            normalized_path.insert(0, '/');\n        }\n        if normalized_path.ends_with('/') && normalized_path != \"/\" {\n            normalized_path.pop();\n        }\n\n        // Process each handler from the nested routes\n        for handler in nested_routes.handlers {\n            // Combine the path prefix with the handler's URI\n            let combined_uri = if handler.uri == \"/\" {\n                normalized_path.clone()\n            } else {\n                format!(\"{}{}\", normalized_path, handler.uri)\n            };\n\n            // Create a new handler with the combined URI\n            let new_handler = Handler {\n                uri: combined_uri,\n                method: handler.method,\n                actions: handler.actions,\n            };\n\n            self.handlers.push(new_handler);\n        }\n\n        self\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use crate::prelude::*;\n    use axum::routing::get;\n\n    async fn users() -> Result<Response> {\n        format::json(\"users list\")\n    }\n\n    async fn user_detail() -> Result<Response> {\n        format::json(\"user detail\")\n    }\n\n    async fn ping() -> Result<Response> {\n        format::json(\"pong\")\n    }\n\n    #[test]\n    fn test_nest_method() {\n        // Create nested routes\n        let api_routes = Routes::new()\n            .add(\"/users\", get(users))\n            .add(\"/users/{id}\", get(user_detail));\n\n        // Nest them under /api\n        let app_routes = Routes::new()\n            .add(\"/ping\", get(ping))\n            .nest(\"/api\", api_routes);\n\n        // Verify the routes are correctly nested\n        assert_eq!(app_routes.handlers.len(), 3);\n\n        // Check that the ping route is unchanged\n        let ping_handler = &app_routes.handlers[0];\n        assert_eq!(ping_handler.uri, \"/ping\");\n\n        // Check that the nested routes have the correct prefixes\n        let users_handler = &app_routes.handlers[1];\n        assert_eq!(users_handler.uri, \"/api/users\");\n\n        let user_detail_handler = &app_routes.handlers[2];\n        assert_eq!(user_detail_handler.uri, \"/api/users/{id}\");\n    }\n\n    #[test]\n    fn test_nest_method_with_root_path() {\n        // Create nested routes with a root path\n        let api_routes = Routes::new()\n            .add(\"/\", get(users))\n            .add(\"/users\", get(user_detail));\n\n        // Nest them under /api\n        let app_routes = Routes::new().nest(\"/api\", api_routes);\n\n        // Verify the routes are correctly nested\n        assert_eq!(app_routes.handlers.len(), 2);\n\n        // Check that the root path is handled correctly\n        let root_handler = &app_routes.handlers[0];\n        assert_eq!(root_handler.uri, \"/api\");\n\n        let users_handler = &app_routes.handlers[1];\n        assert_eq!(users_handler.uri, \"/api/users\");\n    }\n\n    #[test]\n    fn test_nest_method_with_trailing_slash() {\n        // Create nested routes\n        let api_routes = Routes::new().add(\"/users\", get(users));\n\n        // Nest them under /api/ (with trailing slash)\n        let app_routes = Routes::new().nest(\"/api/\", api_routes);\n\n        // Verify the routes are correctly nested (trailing slash should be removed)\n        assert_eq!(app_routes.handlers.len(), 1);\n\n        let users_handler = &app_routes.handlers[0];\n        assert_eq!(users_handler.uri, \"/api/users\");\n    }\n\n    #[test]\n    fn test_nest_method_without_leading_slash() {\n        // Create nested routes\n        let api_routes = Routes::new().add(\"/users\", get(users));\n\n        // Nest them under api (without leading slash)\n        let app_routes = Routes::new().nest(\"api\", api_routes);\n\n        // Verify the routes are correctly nested (leading slash should be added)\n        assert_eq!(app_routes.handlers.len(), 1);\n\n        let users_handler = &app_routes.handlers[0];\n        assert_eq!(users_handler.uri, \"/api/users\");\n    }\n\n    #[test]\n    fn test_merge_method() {\n        // Create separate route groups\n        let user_routes = Routes::new()\n            .add(\"/users\", get(users))\n            .add(\"/users/{id}\", get(user_detail));\n\n        let product_routes = Routes::new()\n            .add(\"/products\", get(users))\n            .add(\"/products/{id}\", get(user_detail));\n\n        // Merge them into a single Routes instance\n        let merged_routes = Routes::new().merge(user_routes).merge(product_routes);\n\n        // Verify all routes are present\n        assert_eq!(merged_routes.handlers.len(), 4);\n\n        // Check user routes\n        let user_list_handler = &merged_routes.handlers[0];\n        assert_eq!(user_list_handler.uri, \"/users\");\n\n        let user_detail_handler = &merged_routes.handlers[1];\n        assert_eq!(user_detail_handler.uri, \"/users/{id}\");\n\n        // Check product routes\n        let product_list_handler = &merged_routes.handlers[2];\n        assert_eq!(product_list_handler.uri, \"/products\");\n\n        let product_detail_handler = &merged_routes.handlers[3];\n        assert_eq!(product_detail_handler.uri, \"/products/{id}\");\n    }\n\n    #[test]\n    fn test_merge_and_nest_combination() {\n        // Create separate route groups\n        let user_routes = Routes::new()\n            .add(\"/users\", get(users))\n            .add(\"/users/{id}\", get(user_detail));\n\n        let product_routes = Routes::new()\n            .add(\"/products\", get(users))\n            .add(\"/products/{id}\", get(user_detail));\n\n        // Merge them and then nest under /api\n        let api_routes = Routes::new().merge(user_routes).merge(product_routes);\n\n        let app_routes = Routes::new()\n            .add(\"/health\", get(ping))\n            .nest(\"/api\", api_routes);\n\n        // Verify the final structure\n        assert_eq!(app_routes.handlers.len(), 5);\n\n        // Check health route is at root level\n        let health_handler = &app_routes.handlers[0];\n        assert_eq!(health_handler.uri, \"/health\");\n\n        // Check nested user routes\n        let user_list_handler = &app_routes.handlers[1];\n        assert_eq!(user_list_handler.uri, \"/api/users\");\n\n        let user_detail_handler = &app_routes.handlers[2];\n        assert_eq!(user_detail_handler.uri, \"/api/users/{id}\");\n\n        // Check nested product routes\n        let product_list_handler = &app_routes.handlers[3];\n        assert_eq!(product_list_handler.uri, \"/api/products\");\n\n        let product_detail_handler = &app_routes.handlers[4];\n        assert_eq!(product_detail_handler.uri, \"/api/products/{id}\");\n    }\n\n    #[test]\n    fn test_merge_all_method() {\n        // Create separate route groups\n        let user_routes = Routes::new().add(\"/users\", get(users));\n        let product_routes = Routes::new().add(\"/products\", get(users));\n        let order_routes = Routes::new().add(\"/orders\", get(users));\n\n        // Merge all of them at once\n        let merged_routes =\n            Routes::new().merge_all(vec![user_routes, product_routes, order_routes]);\n\n        // Verify all routes are present\n        assert_eq!(merged_routes.handlers.len(), 3);\n\n        // Check all routes are present\n        let user_handler = &merged_routes.handlers[0];\n        assert_eq!(user_handler.uri, \"/users\");\n\n        let product_handler = &merged_routes.handlers[1];\n        assert_eq!(product_handler.uri, \"/products\");\n\n        let order_handler = &merged_routes.handlers[2];\n        assert_eq!(order_handler.uri, \"/orders\");\n    }\n\n    #[test]\n    fn test_merge_all_and_nest_combination() {\n        // Create separate route groups from different controllers\n        let user_routes = Routes::new().add(\"/users\", get(users));\n        let product_routes = Routes::new().add(\"/products\", get(users));\n        let order_routes = Routes::new().add(\"/orders\", get(users));\n\n        // Merge all and then nest under /api\n        let api_routes = Routes::new().merge_all(vec![user_routes, product_routes, order_routes]);\n\n        let app_routes = Routes::new()\n            .add(\"/health\", get(ping))\n            .nest(\"/api\", api_routes);\n\n        // Verify the final structure\n        assert_eq!(app_routes.handlers.len(), 4);\n\n        // Check health route is at root level\n        let health_handler = &app_routes.handlers[0];\n        assert_eq!(health_handler.uri, \"/health\");\n\n        // Check nested routes\n        let user_handler = &app_routes.handlers[1];\n        assert_eq!(user_handler.uri, \"/api/users\");\n\n        let product_handler = &app_routes.handlers[2];\n        assert_eq!(product_handler.uri, \"/api/products\");\n\n        let order_handler = &app_routes.handlers[3];\n        assert_eq!(order_handler.uri, \"/api/orders\");\n    }\n}\n"
  },
  {
    "path": "src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]].snap",
    "content": "---\nsource: src/controller/app_routes.rs\nassertion_line: 370\nexpression: \"format!(\\\"{:?} {}\\\", route.actions, route.uri)\"\n---\n\"[GET] /\"\n"
  },
  {
    "path": "src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]_health].snap",
    "content": "---\nsource: src/controller/app_routes.rs\nassertion_line: 337\nexpression: \"format!(\\\"{:?} {}\\\", route.actions, route.uri)\"\n---\n\"[GET] /_health\"\n"
  },
  {
    "path": "src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]_ping].snap",
    "content": "---\nsource: src/controller/app_routes.rs\nassertion_line: 337\nexpression: \"format!(\\\"{:?} {}\\\", route.actions, route.uri)\"\n---\n\"[GET] /_ping\"\n"
  },
  {
    "path": "src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]_readiness].snap",
    "content": "---\nsource: src/controller/app_routes.rs\nexpression: \"format!(\\\"{:?} {}\\\", route.actions, route.uri)\"\n---\n\"[GET] /_readiness\"\n"
  },
  {
    "path": "src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]api[slash]loco-rs].snap",
    "content": "---\nsource: src/controller/app_routes.rs\nassertion_line: 388\nexpression: \"format!(\\\"{:?} {}\\\", route.actions, route.uri)\"\n---\n\"[GET] /api/loco-rs\"\n"
  },
  {
    "path": "src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]api[slash]loco].snap",
    "content": "---\nsource: src/controller/app_routes.rs\nassertion_line: 388\nexpression: \"format!(\\\"{:?} {}\\\", route.actions, route.uri)\"\n---\n\"[GET] /api/loco\"\n"
  },
  {
    "path": "src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]api[slash]v1[slash]notes].snap",
    "content": "---\nsource: src/controller/app_routes.rs\nassertion_line: 421\nexpression: \"format!(\\\"{:?} {}\\\", route.actions, route.uri)\"\n---\n\"[GET] /api/v1/notes\"\n"
  },
  {
    "path": "src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]api[slash]v1[slash]users].snap",
    "content": "---\nsource: src/controller/app_routes.rs\nassertion_line: 421\nexpression: \"format!(\\\"{:?} {}\\\", route.actions, route.uri)\"\n---\n\"[GET] /api/v1/users\"\n"
  },
  {
    "path": "src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]multiple1].snap",
    "content": "---\nsource: src/controller/app_routes.rs\nassertion_line: 370\nexpression: \"format!(\\\"{:?} {}\\\", route.actions, route.uri)\"\n---\n\"[PUT] /multiple1\"\n"
  },
  {
    "path": "src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]multiple2].snap",
    "content": "---\nsource: src/controller/app_routes.rs\nassertion_line: 370\nexpression: \"format!(\\\"{:?} {}\\\", route.actions, route.uri)\"\n---\n\"[OPTIONS] /multiple2\"\n"
  },
  {
    "path": "src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]multiple3].snap",
    "content": "---\nsource: src/controller/app_routes.rs\nassertion_line: 370\nexpression: \"format!(\\\"{:?} {}\\\", route.actions, route.uri)\"\n---\n\"[PATCH] /multiple3\"\n"
  },
  {
    "path": "src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]normalizer[slash]loco[slash]rs].snap",
    "content": "---\nsource: src/controller/app_routes.rs\nassertion_line: 370\nexpression: \"format!(\\\"{:?} {}\\\", route.actions, route.uri)\"\n---\n\"[DELETE] /normalizer/loco/rs\"\n"
  },
  {
    "path": "src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]normalizer[slash]multiple-end].snap",
    "content": "---\nsource: src/controller/app_routes.rs\nassertion_line: 370\nexpression: \"format!(\\\"{:?} {}\\\", route.actions, route.uri)\"\n---\n\"[TRACE] /normalizer/multiple-end\"\n"
  },
  {
    "path": "src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]normalizer[slash]multiple-start].snap",
    "content": "---\nsource: src/controller/app_routes.rs\nassertion_line: 370\nexpression: \"format!(\\\"{:?} {}\\\", route.actions, route.uri)\"\n---\n\"[HEAD] /normalizer/multiple-start\"\n"
  },
  {
    "path": "src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]normalizer[slash]no-slash].snap",
    "content": "---\nsource: src/controller/app_routes.rs\nassertion_line: 370\nexpression: \"format!(\\\"{:?} {}\\\", route.actions, route.uri)\"\n---\n\"[GET] /normalizer/no-slash\"\n"
  },
  {
    "path": "src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]normalizer].snap",
    "content": "---\nsource: src/controller/app_routes.rs\nassertion_line: 370\nexpression: \"format!(\\\"{:?} {}\\\", route.actions, route.uri)\"\n---\n\"[POST] /normalizer\"\n"
  },
  {
    "path": "src/controller/snapshots/loco_rs__controller__format__tests__builder_cookies_response.snap",
    "content": "---\nsource: src/controller/format.rs\nexpression: response.headers()\n---\n{\n    \"set-cookie\": \"foo=bar\",\n    \"set-cookie\": \"baz=qux\",\n}\n"
  },
  {
    "path": "src/controller/snapshots/loco_rs__controller__format__tests__builder_empty_response.snap",
    "content": "---\nsource: src/controller/format.rs\nexpression: response\n---\nResponse {\n    status: 200,\n    version: HTTP/1.1,\n    headers: {},\n    body: Body(\n        UnsyncBoxBody,\n    ),\n}\n"
  },
  {
    "path": "src/controller/snapshots/loco_rs__controller__format__tests__builder_html_response.snap",
    "content": "---\nsource: src/controller/format.rs\nexpression: response\n---\nResponse {\n    status: 200,\n    version: HTTP/1.1,\n    headers: {\n        \"content-type\": \"text/html; charset=utf-8\",\n    },\n    body: Body(\n        UnsyncBoxBody,\n    ),\n}\n"
  },
  {
    "path": "src/controller/snapshots/loco_rs__controller__format__tests__builder_json_response.snap",
    "content": "---\nsource: src/controller/format.rs\nexpression: response\n---\nResponse {\n    status: 200,\n    version: HTTP/1.1,\n    headers: {\n        \"content-type\": \"application/json\",\n    },\n    body: Body(\n        UnsyncBoxBody,\n    ),\n}\n"
  },
  {
    "path": "src/controller/snapshots/loco_rs__controller__format__tests__builder_redirect_response.snap",
    "content": "---\nsource: src/controller/format.rs\nexpression: response\n---\nResponse {\n    status: 303,\n    version: HTTP/1.1,\n    headers: {\n        \"location\": \"https://loco.rs\",\n    },\n    body: Body(\n        UnsyncBoxBody,\n    ),\n}\n"
  },
  {
    "path": "src/controller/snapshots/loco_rs__controller__format__tests__builder_redirect_with_custom_header_response.snap",
    "content": "---\nsource: src/controller/format.rs\nexpression: response\n---\nResponse {\n    status: 303,\n    version: HTTP/1.1,\n    headers: {\n        \"hx-redirect\": \"https://loco.rs\",\n    },\n    body: Body(\n        UnsyncBoxBody,\n    ),\n}\n"
  },
  {
    "path": "src/controller/snapshots/loco_rs__controller__format__tests__builder_template_response.snap",
    "content": "---\nsource: src/controller/format.rs\nexpression: response\n---\nResponse {\n    status: 200,\n    version: HTTP/1.1,\n    headers: {\n        \"content-type\": \"text/html; charset=utf-8\",\n    },\n    body: Body(\n        UnsyncBoxBody,\n    ),\n}\n"
  },
  {
    "path": "src/controller/snapshots/loco_rs__controller__format__tests__builder_text_response.snap",
    "content": "---\nsource: src/controller/format.rs\nexpression: response\n---\nResponse {\n    status: 200,\n    version: HTTP/1.1,\n    headers: {\n        \"content-type\": \"text/plain; charset=utf-8\",\n    },\n    body: Body(\n        UnsyncBoxBody,\n    ),\n}\n"
  },
  {
    "path": "src/controller/snapshots/loco_rs__controller__format__tests__builder_view_response-2.snap",
    "content": "---\nsource: src/controller/format.rs\nexpression: response\n---\nResponse {\n    status: 200,\n    version: HTTP/1.1,\n    headers: {\n        \"content-type\": \"text/html; charset=utf-8\",\n    },\n    body: Body(\n        UnsyncBoxBody,\n    ),\n}\n"
  },
  {
    "path": "src/controller/snapshots/loco_rs__controller__format__tests__builder_view_response.snap",
    "content": "---\nsource: src/controller/format.rs\nexpression: \"view(&v, \\\"template/none.html\\\", serde_json::json!({}))\"\n---\nErr(\n    Tera(\n        Error {\n            kind: TemplateNotFound(\n                \"template/none.html\",\n            ),\n            source: None,\n        },\n    ),\n)\n"
  },
  {
    "path": "src/controller/snapshots/loco_rs__controller__format__tests__empty_json_response_format.snap",
    "content": "---\nsource: src/controller/format.rs\nexpression: response\n---\nResponse {\n    status: 200,\n    version: HTTP/1.1,\n    headers: {\n        \"content-type\": \"application/json\",\n    },\n    body: Body(\n        UnsyncBoxBody,\n    ),\n}\n"
  },
  {
    "path": "src/controller/snapshots/loco_rs__controller__format__tests__empty_response_format.snap",
    "content": "---\nsource: src/controller/format.rs\nexpression: response\n---\nResponse {\n    status: 200,\n    version: HTTP/1.1,\n    headers: {},\n    body: Body(\n        UnsyncBoxBody,\n    ),\n}\n"
  },
  {
    "path": "src/controller/snapshots/loco_rs__controller__format__tests__html_response_format.snap",
    "content": "---\nsource: src/controller/format.rs\nexpression: response\n---\nResponse {\n    status: 200,\n    version: HTTP/1.1,\n    headers: {\n        \"content-type\": \"text/html; charset=utf-8\",\n    },\n    body: Body(\n        UnsyncBoxBody,\n    ),\n}\n"
  },
  {
    "path": "src/controller/snapshots/loco_rs__controller__format__tests__json_response_format.snap",
    "content": "---\nsource: src/controller/format.rs\nexpression: response\n---\nResponse {\n    status: 200,\n    version: HTTP/1.1,\n    headers: {\n        \"content-type\": \"application/json\",\n    },\n    body: Body(\n        UnsyncBoxBody,\n    ),\n}\n"
  },
  {
    "path": "src/controller/snapshots/loco_rs__controller__format__tests__redirect_response.snap",
    "content": "---\nsource: src/controller/format.rs\nexpression: response\n---\nResponse {\n    status: 303,\n    version: HTTP/1.1,\n    headers: {\n        \"location\": \"https://loco.rs\",\n    },\n    body: Body(\n        UnsyncBoxBody,\n    ),\n}\n"
  },
  {
    "path": "src/controller/snapshots/loco_rs__controller__format__tests__template_response.snap",
    "content": "---\nsource: src/controller/format.rs\nexpression: response\n---\nResponse {\n    status: 200,\n    version: HTTP/1.1,\n    headers: {\n        \"content-type\": \"text/html; charset=utf-8\",\n    },\n    body: Body(\n        UnsyncBoxBody,\n    ),\n}\n"
  },
  {
    "path": "src/controller/snapshots/loco_rs__controller__format__tests__text_response_format.snap",
    "content": "---\nsource: src/controller/format.rs\nexpression: response\n---\nResponse {\n    status: 200,\n    version: HTTP/1.1,\n    headers: {\n        \"content-type\": \"text/plain; charset=utf-8\",\n    },\n    body: Body(\n        UnsyncBoxBody,\n    ),\n}\n"
  },
  {
    "path": "src/controller/snapshots/loco_rs__controller__format__tests__view_response-2.snap",
    "content": "---\nsource: src/controller/format.rs\nexpression: response\n---\nResponse {\n    status: 200,\n    version: HTTP/1.1,\n    headers: {\n        \"content-type\": \"text/html; charset=utf-8\",\n    },\n    body: Body(\n        UnsyncBoxBody,\n    ),\n}\n"
  },
  {
    "path": "src/controller/snapshots/loco_rs__controller__format__tests__view_response.snap",
    "content": "---\nsource: src/controller/format.rs\nexpression: \"view(&v, \\\"template/none.html\\\", serde_json::json!({}))\"\n---\nErr(\n    Tera(\n        Error {\n            kind: TemplateNotFound(\n                \"template/none.html\",\n            ),\n            source: None,\n        },\n    ),\n)\n"
  },
  {
    "path": "src/controller/snapshots/loco_rs__controller__format__tests__yaml_response_format.snap",
    "content": "---\nsource: src/controller/format.rs\nexpression: response\n---\nResponse {\n    status: 200,\n    version: HTTP/1.1,\n    headers: {\n        \"content-type\": \"application/yaml\",\n    },\n    body: Body(\n        UnsyncBoxBody,\n    ),\n}\n"
  },
  {
    "path": "src/controller/views/engine.rs",
    "content": "use std::path::{Path, PathBuf};\n\nuse super::tera_builtins;\nuse crate::{controller::views::ViewRenderer, Error, Result};\nuse serde::Serialize;\n\n#[cfg(debug_assertions)]\nuse notify::{\n    event::{EventKind, ModifyKind},\n    Event, RecursiveMode, Watcher,\n};\n\npub static DEFAULT_ASSET_FOLDER: &str = \"assets\";\n\n#[cfg(debug_assertions)]\npub struct HotReloadingTeraEngine {\n    pub engine: tera::Tera,\n    pub view_path: PathBuf,\n    pub file_watcher: Box<dyn notify::Watcher + Send + Sync>,\n    pub dirty: bool,\n    pub post_process: Box<dyn Fn(&mut tera::Tera) -> Result<()> + Send + Sync>,\n}\n\n#[derive(Clone)]\npub struct TeraView(\n    #[cfg(debug_assertions)] std::sync::Arc<std::sync::Mutex<HotReloadingTeraEngine>>,\n    #[cfg(not(debug_assertions))] std::sync::Arc<tera::Tera>,\n);\n\nimpl TeraView {\n    /// Create a Tera view engine\n    ///\n    /// # Errors\n    ///\n    /// This function will return an error if building fails\n    pub fn build() -> Result<Self> {\n        Self::from_custom_dir(&PathBuf::from(DEFAULT_ASSET_FOLDER).join(\"views\"), |_| {\n            Ok(())\n        })\n    }\n\n    /// Create a Tera view engine with a post-processing function for subsequent instantiation.\n    ///\n    /// The post-processing function is also run during the call to this method.\n    ///\n    /// # Errors\n    ///\n    /// This function will return an error if building fails or if the post-processing function fails\n    pub fn build_with_post_process(\n        post_process: impl Fn(&mut tera::Tera) -> Result<()> + Send + Sync + 'static,\n    ) -> Result<Self> {\n        Self::from_custom_dir(\n            &PathBuf::from(DEFAULT_ASSET_FOLDER).join(\"views\"),\n            post_process,\n        )\n    }\n\n    /// Create a new Tera instance from a directory path\n    ///\n    /// # Errors\n    ///\n    /// This function will return an error if building fails\n    fn create_tera_instance<P: AsRef<Path>>(path: P) -> Result<tera::Tera> {\n        let path = path\n            .as_ref()\n            .to_str()\n            .ok_or_else(|| Error::string(\"invalid glob\"))?;\n\n        let mut tera = tera::Tera::new(path)?;\n\n        tera_builtins::filters::register_filters(&mut tera);\n\n        Ok(tera)\n    }\n\n    /// Create a Tera view engine from a custom directory\n    ///\n    /// The post-processing function is also run during the call to this method.\n    ///\n    /// # Errors\n    ///\n    /// This function will return an error if building fails or if the post-processing function fails\n    pub fn from_custom_dir<P: AsRef<Path>>(\n        path: &P,\n        post_process: impl Fn(&mut tera::Tera) -> Result<()> + Send + Sync + 'static,\n    ) -> Result<Self> {\n        if !path.as_ref().exists() {\n            return Err(Error::string(&format!(\n                \"missing views directory: `{}`\",\n                path.as_ref().display()\n            )));\n        }\n        let view_dir = path.as_ref();\n        let view_path: PathBuf = view_dir.join(\"**\").join(\"*.html\");\n\n        // Create instance\n        let mut tera = Self::create_tera_instance(&view_path)?;\n\n        // Do post processing\n        post_process(&mut tera)?;\n\n        // Enable hot-reloading in debug build\n        #[cfg(debug_assertions)]\n        let tera = {\n            let tera = std::sync::Arc::new(std::sync::Mutex::new(HotReloadingTeraEngine {\n                engine: tera,\n                view_path,\n                file_watcher: Box::new(notify::NullWatcher),\n                dirty: false,\n                post_process: Box::new(post_process),\n            }));\n\n            let tera2 = tera.clone();\n\n            // Create file watcher\n            let mut watcher = notify::recommended_watcher(move |event| {\n                use tracing::info;\n\n                let Ok(Event { kind, paths, .. }) = event else {\n                    return;\n                };\n\n                // Only handle sub-directories and .html files\n                if !paths\n                    .iter()\n                    .all(|p| p.is_dir() || p.extension().is_some_and(|ext| ext == \"html\"))\n                {\n                    return;\n                }\n\n                // Set dirty flag if file/directory modified\n                match kind {\n                    // Simple access, no changes\n                    EventKind::Access(_) => return,\n                    // Metadata changes, no content change\n                    EventKind::Modify(ModifyKind::Metadata(_)) => return,\n                    // Content modified\n                    EventKind::Modify(ModifyKind::Data(change)) => {\n                        info!(?paths, ?change, \"View file modified\")\n                    }\n                    // File renamed\n                    EventKind::Modify(ModifyKind::Name(change)) => {\n                        info!(?paths, ?change, \"View file renamed\")\n                    }\n                    // Other modifications\n                    EventKind::Modify(change) => {\n                        info!(?paths, ?change, \"View file modified\")\n                    }\n                    // File created.\n                    EventKind::Create(_) => info!(?paths, \"View file created\"),\n                    // File removed.\n                    EventKind::Remove(_) => info!(?paths, \"View file removed\"),\n                    // All other changes.\n                    change => info!(?paths, ?change, \"View file changed\"),\n                }\n\n                tera2.lock().unwrap().dirty = true;\n            })\n            .map_err(|_| Error::string(\"error creating file watcher\"))?;\n\n            watcher\n                .watch(view_dir, RecursiveMode::Recursive)\n                .map_err(|_| Error::string(\"error watching for file changes in view directory\"))?;\n\n            tera.lock().unwrap().file_watcher = Box::new(watcher);\n            tera\n        };\n\n        #[cfg(not(debug_assertions))]\n        let tera = std::sync::Arc::new(tera);\n\n        Ok(Self(tera))\n    }\n}\n\nimpl ViewRenderer for TeraView {\n    fn render<S: Serialize>(&self, key: &str, data: S) -> Result<String> {\n        let context = tera::Context::from_serialize(data)?;\n\n        #[cfg(debug_assertions)]\n        {\n            let mut tera = self.0.lock().unwrap();\n\n            // Only create a new Tera instance if the view path files have changed\n            if tera.dirty {\n                tracing::warn!(key, \"Hot-reloading Tera view engine\");\n\n                tera.dirty = false;\n\n                let mut new_engine = Self::create_tera_instance(&tera.view_path)?;\n\n                tera.post_process.as_ref()(&mut new_engine)?;\n\n                tera.engine = new_engine;\n            }\n\n            Ok(tera.engine.render(key, &context)?)\n        }\n\n        #[cfg(not(debug_assertions))]\n        Ok(self.0.render(key, &context)?)\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use std::collections::HashMap;\n\n    use serde_json::{json, Value};\n    use tree_fs;\n\n    use super::*;\n    #[test]\n    fn can_render_view() {\n        let tree_fs = tree_fs::TreeBuilder::default()\n            .add_file(\"template/test.html\", \"generate test.html file: {{foo}}\")\n            .add_file(\"template/test2.html\", \"generate test2.html file: {{bar}}\")\n            .create()\n            .unwrap();\n\n        let v = TeraView::from_custom_dir(&tree_fs.root, |_| Ok(())).unwrap();\n\n        assert_eq!(\n            v.render(\"template/test.html\", json!({\"foo\": \"foo-txt\"}))\n                .unwrap(),\n            \"generate test.html file: foo-txt\"\n        );\n\n        assert_eq!(\n            v.render(\"template/test2.html\", json!({\"bar\": \"bar-txt\"}))\n                .unwrap(),\n            \"generate test2.html file: bar-txt\"\n        );\n    }\n\n    #[cfg(debug_assertions)]\n    #[test]\n    fn template_inheritance_hot_reload() {\n        let tree_fs = tree_fs::TreeBuilder::default()\n            .add_file(\n                \"template/base.html\",\n                r\"<!DOCTYPE html>\n            <html>\n            <head>\n                <title>{% block title %}Default Title{% endblock %}</title>\n            </head>\n            <body>\n                <header>Base Header v1: {{ 1 | hello }}</header>\n                {% block content %}\n                Default content\n                {% endblock %}\n                <footer>Base Footer</footer>\n            </body>\n            </html>\",\n            )\n            .add_file(\n                \"template/child.html\",\n                r\"{% extends 'template/base.html' %}\n            {% block title %}Child Page{% endblock %}\n            {% block content %}\n            <div>Child content</div>\n            {% endblock %}\",\n            )\n            .create()\n            .unwrap();\n\n        let tree_dir = tree_fs.root.clone();\n        let v = TeraView::from_custom_dir(&tree_fs.root, |tera| {\n            tera.register_filter(\"hello\", |value: &Value, _: &HashMap<String, Value>| {\n                Ok(format!(\"Hello World v{value}\").into())\n            });\n            Ok(())\n        })\n        .unwrap();\n\n        // Initial render should have the original header from base template\n        let initial_render = v.render(\"template/child.html\", json!({})).unwrap();\n        assert!(initial_render.contains(\"Base Header v1: Hello World v1\"));\n        assert!(initial_render.contains(\"Child Page\"));\n        assert!(initial_render.contains(\"Child content\"));\n\n        // Now modify the base template to change the header\n        let updated_base = r\"<!DOCTYPE html>\n<html>\n<head>\n    <title>{% block title %}Default Title{% endblock %}</title>\n</head>\n<body>\n    <header>Base Header v2: {{ 2 | hello }}</header>\n    {% block content %}\n    Default content\n    {% endblock %}\n    <footer>Base Footer</footer>\n</body>\n</html>\";\n\n        // Update the base template file\n        std::fs::write(\n            Path::new(&tree_dir).join(\"template\").join(\"base.html\"),\n            updated_base,\n        )\n        .unwrap();\n\n        // Wait for file watcher to detect the change\n        std::thread::sleep(std::time::Duration::from_millis(300));\n\n        // Render again - should have the updated header due to hot reload\n        let updated_render = v.render(\"template/child.html\", json!({})).unwrap();\n        assert!(updated_render.contains(\"Base Header v2: Hello World v2\")); // Should have changed\n        assert!(updated_render.contains(\"Child Page\")); // Should be the same\n        assert!(updated_render.contains(\"Child content\")); // Should be the same\n    }\n}\n"
  },
  {
    "path": "src/controller/views/engine_embedded.rs",
    "content": "use super::tera_builtins;\nuse crate::{controller::views::ViewRenderer, Result};\nuse serde::Serialize;\nuse std::collections::BTreeMap;\n\npub static DEFAULT_ASSET_FOLDER: &str = \"assets\";\n\n// Include the generated templates at the module level\ninclude!(concat!(\n    env!(\"OUT_DIR\"),\n    \"/generated_code/view_templates.rs\"\n));\n\n#[derive(Clone, Debug)]\npub struct TeraView {\n    pub tera: tera::Tera,\n    pub default_context: tera::Context,\n}\n\nimpl TeraView {\n    /// Create a Tera view engine\n    ///\n    /// # Errors\n    ///\n    /// This function will return an error if building fails\n    pub fn build() -> Result<Self> {\n        Self::from_embedded_templates()\n    }\n\n    /// Attach the Tera view engine with a post-processing function for subsequent instantiation.\n    ///\n    /// The post-processing function is also run during the call to this method.\n    ///\n    /// # Errors\n    ///\n    /// This function will return an error if the post-processing function fails\n    pub fn post_process(\n        mut self,\n        mut post_process: impl FnMut(&mut tera::Tera) -> Result<()> + Send + Sync + 'static,\n    ) -> Result<Self> {\n        post_process(&mut self.tera)?;\n        Ok(self)\n    }\n\n    /// Load and initialize templates from embedded assets\n    ///\n    /// # Errors\n    ///\n    /// This function will return an error if:\n    /// - Adding templates to Tera fails\n    /// - There are syntax errors in any template\n    pub fn from_embedded_templates() -> Result<Self> {\n        let mut tera = tera::Tera::default();\n\n        // Initialize templates in a separate function to reduce complexity\n        Self::load_templates_into_tera(&mut tera)?;\n\n        tera_builtins::filters::register_filters(&mut tera);\n        let ctx = tera::Context::default();\n\n        Ok(Self {\n            tera,\n            default_context: ctx,\n        })\n    }\n\n    /// Helper function to load all embedded templates into Tera engine\n    ///\n    /// # Errors\n    ///\n    /// Returns an error if adding a template fails\n    fn load_templates_into_tera(tera: &mut tera::Tera) -> Result<()> {\n        let templates_map = get_embedded_templates();\n        let templates: BTreeMap<_, _> = templates_map.into_iter().collect();\n        Self::log_template_info(&templates);\n        Self::add_templates_to_tera(tera, templates)\n    }\n\n    /// Log information about the templates\n    fn log_template_info(templates: &BTreeMap<String, &'static str>) {\n        tracing::info!(\"Initializing embedded templates feature\");\n        tracing::info!(\"Found {} embedded templates\", templates.len());\n    }\n\n    /// Add each template to the Tera engine\n    ///\n    /// # Errors\n    ///\n    /// Returns an error if adding any template fails\n    fn add_templates_to_tera(\n        tera: &mut tera::Tera,\n        templates: BTreeMap<String, &'static str>,\n    ) -> Result<()> {\n        // Add all templates to Tera\n        for (name, content) in templates {\n            tracing::debug!(\"Adding template '{}' to Tera\", name);\n            if let Err(e) = tera.add_raw_template(&name, content) {\n                tracing::error!(\"Failed to add template '{}': {}\", name, e);\n                return Err(e.into());\n            }\n        }\n\n        // Ensure templates are properly configured for inheritance\n        if let Err(e) = tera.build_inheritance_chains() {\n            tracing::error!(\"Failed to build template inheritance chains: {}\", e);\n            return Err(e.into());\n        }\n\n        Ok(())\n    }\n}\n\nimpl ViewRenderer for TeraView {\n    fn render<S: Serialize>(&self, key: &str, data: S) -> Result<String> {\n        let context = tera::Context::from_serialize(data)?;\n\n        // Try to render the requested template\n        match self.tera.render(key, &context) {\n            Ok(result) => Ok(result),\n            Err(e) => {\n                // Log error about missing template\n                if e.to_string().contains(\"not found\") {\n                    tracing::warn!(\"Template '{}' not found\", key);\n                    let template_names: Vec<String> =\n                        self.tera.get_template_names().map(String::from).collect();\n                    tracing::debug!(\"Available templates: {:?}\", template_names);\n                }\n\n                // Return the original error\n                Err(e.into())\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/controller/views/mod.rs",
    "content": "// Choose the correct engine implementation based on the feature flag\n#[cfg(feature = \"embedded_assets\")]\npub mod engine_embedded;\n#[cfg(feature = \"embedded_assets\")]\npub use engine_embedded as engines;\n\n#[cfg(not(feature = \"embedded_assets\"))]\npub mod engine;\n#[cfg(not(feature = \"embedded_assets\"))]\npub use engine as engines;\n\nuse axum::{extract::FromRequestParts, http::request::Parts, Extension};\nuse serde::Serialize;\npub mod tera_builtins;\nuse crate::Result;\n\n#[cfg(feature = \"with-db\")]\npub mod pagination;\n\npub trait ViewRenderer {\n    /// Render a view template located by `key`\n    ///\n    /// # Errors\n    ///\n    /// This function will return an error if render fails\n    fn render<S: Serialize>(&self, key: &str, data: S) -> Result<String>;\n}\n\n#[derive(Debug, PartialEq, Eq, Clone)]\npub struct ViewEngine<E>(pub E);\n\nimpl<E> ViewEngine<E> {\n    /// Creates a new [`Engine`] that wraps the given engine\n    pub fn new(engine: E) -> Self {\n        Self(engine)\n    }\n}\n\n/// A struct representing an inline Tera view renderer.\n///\n/// This struct provides functionality to render templates using the Tera\n/// templating engine directly from raw template strings.\n///\n/// # Example\n/// ```\n/// use serde_json::json;\n/// use loco_rs::controller::views;\n/// let render = views::template(\"{{name}} website\", json!({\"name\": \"Loco\"})).unwrap();\n/// assert_eq!(render, \"Loco website\");\n/// ```\n///\n/// # Errors\n///\n/// This function will return an error if building fails\npub fn template<S>(template: &str, data: S) -> Result<String>\nwhere\n    S: Serialize,\n{\n    let mut tera = tera::Tera::default();\n    Ok(tera.render_str(template, &tera::Context::from_serialize(data)?)?)\n}\n\nimpl<E> From<E> for ViewEngine<E> {\n    fn from(inner: E) -> Self {\n        Self::new(inner)\n    }\n}\n\nimpl<S, E> FromRequestParts<S> for ViewEngine<E>\nwhere\n    S: Send + Sync,\n    E: Clone + Send + Sync + 'static,\n{\n    type Rejection = std::convert::Infallible;\n\n    async fn from_request_parts(\n        parts: &mut Parts,\n        state: &S,\n    ) -> std::result::Result<Self, Self::Rejection> {\n        let Extension(tl): Extension<Self> = Extension::from_request_parts(parts, state)\n            .await\n            .expect(\"TeraLayer missing. Is the TeraLayer installed?\");\n        /*\n        let locale = parts\n            .headers\n            .get(\"Accept-Language\")\n            .unwrap()\n            .to_str()\n            .unwrap();\n        // BUG: this does not mutate or set anything because of clone\n        tl.default_context.clone().insert(\"locale\", &locale);\n        */\n\n        Ok(tl)\n    }\n}\n"
  },
  {
    "path": "src/controller/views/pagination.rs",
    "content": "use serde::{Deserialize, Serialize};\n\n#[derive(Debug, Deserialize, Serialize)]\npub struct Pager<T> {\n    #[serde(rename(serialize = \"results\"))]\n    pub results: T,\n\n    #[serde(rename(serialize = \"pagination\"))]\n    pub info: PagerMeta,\n}\n\n#[derive(Debug, Deserialize, Serialize)]\npub struct PagerMeta {\n    #[serde(rename(serialize = \"page\"))]\n    pub page: u64,\n    #[serde(rename(serialize = \"page_size\"))]\n    pub page_size: u64,\n    #[serde(rename(serialize = \"total_pages\"))]\n    pub total_pages: u64,\n    #[serde(rename(serialize = \"total_items\"))]\n    pub total_items: u64,\n}\n\nimpl<T> Pager<T> {\n    #[must_use]\n    pub const fn new(results: T, meta: PagerMeta) -> Self {\n        Self {\n            results,\n            info: meta,\n        }\n    }\n}\n"
  },
  {
    "path": "src/controller/views/tera_builtins/filters/mod.rs",
    "content": "pub mod number;\n\npub fn register_filters(tera: &mut tera::Tera) {\n    tera.register_filter(\"number_with_delimiter\", number::number_with_delimiter);\n    tera.register_filter(\"number_to_human_size\", number::number_to_human_size);\n    tera.register_filter(\"number_to_percentage\", number::number_to_percentage);\n}\n"
  },
  {
    "path": "src/controller/views/tera_builtins/filters/number.rs",
    "content": "#![allow(clippy::implicit_hasher)]\nuse std::collections::HashMap;\n\nuse byte_unit::Byte;\nuse serde_json::value::Value;\nuse tera::Result;\n\n/// Helper function to add commas as thousands separators\nfn separate_with_commas(num_str: &str) -> String {\n    if let Some((integer_part, decimal_part)) = num_str.split_once('.') {\n        // Handle decimal numbers\n        let formatted_integer = separate_integer_part(integer_part);\n        format!(\"{formatted_integer}.{decimal_part}\")\n    } else {\n        // Handle integers\n        separate_integer_part(num_str)\n    }\n}\n\nfn separate_integer_part(num_str: &str) -> String {\n    let is_negative = num_str.starts_with('-');\n    let num_str = if is_negative { &num_str[1..] } else { num_str };\n\n    let len = num_str.len();\n    let mut result = String::with_capacity(len + (len - 1) / 3);\n\n    for (i, c) in num_str.chars().enumerate() {\n        if i > 0 && (len - i) % 3 == 0 {\n            result.push(',');\n        }\n        result.push(c);\n    }\n\n    if is_negative {\n        format!(\"-{result}\")\n    } else {\n        result\n    }\n}\n\n/// Formats a numeric value by adding commas as thousands separators.\n///\n///\n/// # Examples:\n///\n/// ```ignore\n/// {{1000 | number_with_delimiter}}\n/// ```\n///\n/// # Errors\n///\n/// If the `value` is not a numeric value, the function will return the original\n/// value as a string without any error.\npub fn number_with_delimiter(value: &Value, _: &HashMap<String, Value>) -> Result<Value> {\n    match value {\n        Value::Number(_) => {\n            // Use the original string representation to preserve format\n            let num_str = value.to_string();\n            Ok(Value::String(separate_with_commas(&num_str)))\n        }\n        _ => Ok(value.clone()),\n    }\n}\n\n/// Converts a numeric value (in bytes) into a human-readable size string with\n/// appropriate units.\n///\n/// # Examples:\n///\n/// ```ignore\n/// {{70691577 | number_to_human_size}}\n/// ```\n///\n/// # Errors\n///\n/// If the `value` is not a numeric value, the function will return the original\n/// value as a string without any error.\npub fn number_to_human_size(value: &Value, _: &HashMap<String, Value>) -> Result<Value> {\n    Byte::from_str(value.to_string()).map_or_else(\n        |_| Ok(value.clone()),\n        |byte_unit| {\n            Ok(Value::String(\n                byte_unit.get_appropriate_unit(false).to_string(),\n            ))\n        },\n    )\n}\n\n/// Converts a numeric value into a formatted percentage string.\n///\n/// # Examples:\n///\n/// ```ignore\n/// {{100 | number_to_percentage}}\n/// {{100 | number_to_percentage(format='%n %')}}\n/// ```\n///\n/// # Errors\n///\n/// If the `value` is not a numeric value, the function will return the original\n/// value as a string without any error.\npub fn number_to_percentage(value: &Value, options: &HashMap<String, Value>) -> Result<Value> {\n    match value {\n        Value::Number(number) => {\n            let format = options\n                .get(\"format\")\n                .and_then(|v| v.as_str())\n                .unwrap_or(\"%n%\");\n\n            Ok(Value::String(format.replace(\"%n\", &number.to_string())))\n        }\n        _ => Ok(value.clone()),\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use std::collections::HashMap;\n\n    use rstest::rstest;\n    use serde_json::json;\n\n    use super::*;\n\n    #[rstest]\n    #[case(json!(100), \"100\")]\n    #[case(json!(100.2), \"100.2\")]\n    #[case(json!(1000), \"1,000\")]\n    #[case(json!(10000), \"10,000\")]\n    #[case(json!(10000.1234), \"10,000.1234\")]\n    #[case(json!(-100), \"-100\")]\n    #[case(json!(-100.2), \"-100.2\")]\n    #[case(json!(-1000), \"-1,000\")]\n    #[case(json!(-10000), \"-10,000\")]\n    #[case(json!(-10000.12345), \"-10,000.12345\")]\n    #[case(json!(\"invalid\"), \"invalid\")]\n    #[case(json!(0), \"0\")]\n    #[case(json!(\"0.0\"), \"0.0\")]\n    #[case(json!(0.123), \"0.123\")]\n    #[case(json!(1_000_000), \"1,000,000\")]\n    #[case(json!(1_000_000_000), \"1,000,000,000\")]\n    #[case(json!(1_234_567_890.123_456), \"1,234,567,890.123456\")]\n    #[case(json!(0.000_123), \"0.000123\")]\n    #[case(json!(\"100.00\"), \"100.00\")]\n    #[case(json!(-0.123), \"-0.123\")]\n    #[case(json!(-1_234_567.89), \"-1,234,567.89\")]\n    #[case(json!(100), \"100\")]\n    #[case(json!(\"100.00230\"), \"100.00230\")]\n    #[case(json!(\"0100.00230\"), \"0100.00230\")]\n    #[case(json!(\"\"), \"\")]\n    fn test_number_with_delimiter(#[case] input: Value, #[case] expected: &str) {\n        let result = number_with_delimiter(&input, &HashMap::new()).unwrap();\n        assert_eq!(result, Value::String(expected.to_string()));\n    }\n\n    #[rstest]\n    #[case(json!(1234), \"1.23 KB\")]\n    #[case(json!(70_691_577), \"70.69 MB\")]\n    #[case(json!(\"invalid\"), \"invalid\")]\n    fn test_number_to_human_size(#[case] input: Value, #[case] expected: &str) {\n        let result = number_to_human_size(&input, &HashMap::new()).unwrap();\n        assert_eq!(result, Value::String(expected.to_string()));\n    }\n\n    #[rstest]\n    #[case(json!(100), HashMap::new(), \"100%\")]\n    #[case(json!(100), HashMap::from([(\"format\".to_string(), Value::String(\"%n %\".to_string()))]), \"100 %\")]\n    #[case(json!(\"invalid\"), HashMap::new(), \"invalid\")]\n    fn test_number_to_percentage(\n        #[case] value: Value,\n        #[case] options: HashMap<String, Value>,\n        #[case] expected: &str,\n    ) {\n        assert_eq!(\n            number_to_percentage(&value, &options).unwrap(),\n            Value::String(expected.to_string())\n        );\n    }\n}\n"
  },
  {
    "path": "src/controller/views/tera_builtins/mod.rs",
    "content": "pub mod filters;\n"
  },
  {
    "path": "src/data.rs",
    "content": "use std::{env, path::Path};\n\nuse serde::de::DeserializeOwned;\n\nuse crate::{env_vars, Error, Result};\n\nconst DEFAULT_DATA_FOLDER: &str = \"data\";\nfn data_folder() -> String {\n    env::var(env_vars::LOCO_DATA_FOLDER_ENV).unwrap_or_else(|_| DEFAULT_DATA_FOLDER.to_string())\n}\n\n/// Load a data JSON file synchronously\n///\n/// # Errors\n///\n/// This function will return an error if IO fails\npub fn load_json_file_sync<T: DeserializeOwned>(path: &str) -> Result<T> {\n    let p = Path::new(&data_folder()).join(path);\n    let content = std::fs::read_to_string(&p).map_err(|e| Error::string(&e.to_string()))?;\n    let json_value: T =\n        serde_json::from_str(&content).map_err(|e| Error::string(&e.to_string()))?;\n    Ok(json_value)\n}\n\n/// Load a data JSON file asynchronously\n///\n/// # Errors\n///\n/// This function will return an error if IO fails\npub async fn load_json_file<T: DeserializeOwned>(path: &str) -> Result<T> {\n    let p = Path::new(&data_folder()).join(path);\n    let content = tokio::fs::read_to_string(&p)\n        .await\n        .map_err(|e| Error::string(&e.to_string()))?;\n    let json_value: T =\n        serde_json::from_str(&content).map_err(|e| Error::string(&e.to_string()))?;\n    Ok(json_value)\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use serde::Deserialize;\n    use tree_fs::TreeBuilder;\n\n    #[derive(Deserialize, Debug, PartialEq)]\n    struct TestData {\n        name: String,\n        value: i32,\n    }\n\n    #[test]\n    fn test_load_json_file_sync_success() {\n        // Test successful loading\n        let tree = TreeBuilder::default()\n            .drop(true)\n            .add(\"test_data.json\", r#\"{\"name\": \"test\", \"value\": 42}\"#)\n            .create()\n            .expect(\"Failed to create tree_fs for sync success test\");\n\n        // Test with valid JSON\n        let file_path = tree.root.join(\"test_data.json\");\n        let file_content =\n            std::fs::read_to_string(&file_path).expect(\"Failed to read test_data.json file\");\n        let result: TestData = serde_json::from_str(&file_content)\n            .expect(\"Failed to parse valid JSON in sync success test\");\n\n        assert_eq!(\n            result,\n            TestData {\n                name: \"test\".to_string(),\n                value: 42\n            }\n        );\n    }\n\n    #[test]\n    fn test_load_json_file_sync_file_not_found() {\n        // Test with non-existent file\n        let tree = TreeBuilder::default()\n            .drop(true)\n            .create()\n            .expect(\"Failed to create tree_fs for sync file not found test\");\n\n        let file_path = tree.root.join(\"nonexistent.json\");\n        let result = std::fs::read_to_string(file_path);\n        result.expect_err(\"Reading a non-existent file should fail\");\n    }\n\n    #[test]\n    fn test_load_json_file_sync_invalid_json() {\n        // Test with invalid JSON\n        let tree = TreeBuilder::default()\n            .drop(true)\n            .add(\"invalid.json\", r#\"{\"name\": \"test\", \"value\": not_a_number}\"#)\n            .create()\n            .expect(\"Failed to create tree_fs for sync invalid JSON test\");\n\n        let file_path = tree.root.join(\"invalid.json\");\n        let content = std::fs::read_to_string(file_path).expect(\"Failed to read invalid.json file\");\n        let result: Result<TestData, _> = serde_json::from_str(&content);\n        result.expect_err(\"Parsing invalid JSON should fail\");\n    }\n\n    #[tokio::test]\n    async fn test_load_json_file_async_success() {\n        // Test successful loading\n        let tree = TreeBuilder::default()\n            .drop(true)\n            .add(\"async_data.json\", r#\"{\"name\": \"async_test\", \"value\": 100}\"#)\n            .create()\n            .expect(\"Failed to create tree_fs for async success test\");\n\n        // Test with valid JSON\n        let file_path = tree.root.join(\"async_data.json\");\n        let content = tokio::fs::read_to_string(file_path)\n            .await\n            .expect(\"Failed to read async_data.json file\");\n        let result: TestData = serde_json::from_str(&content)\n            .expect(\"Failed to parse valid JSON in async success test\");\n\n        assert_eq!(\n            result,\n            TestData {\n                name: \"async_test\".to_string(),\n                value: 100\n            }\n        );\n    }\n\n    #[tokio::test]\n    async fn test_load_json_file_async_file_not_found() {\n        // Test with non-existent file\n        let tree = TreeBuilder::default()\n            .drop(true)\n            .create()\n            .expect(\"Failed to create tree_fs for async file not found test\");\n\n        let file_path = tree.root.join(\"nonexistent_async.json\");\n        let result = tokio::fs::read_to_string(file_path).await;\n        result.expect_err(\"Reading a non-existent file asynchronously should fail\");\n    }\n\n    #[tokio::test]\n    async fn test_load_json_file_async_invalid_json() {\n        // Test with invalid JSON\n        let tree = TreeBuilder::default()\n            .drop(true)\n            .add(\n                \"invalid_async.json\",\n                r#\"{\"name\": \"async_test\", \"value\": not_a_number}\"#,\n            )\n            .create()\n            .expect(\"Failed to create tree_fs for async invalid JSON test\");\n\n        let file_path = tree.root.join(\"invalid_async.json\");\n        let content = tokio::fs::read_to_string(file_path)\n            .await\n            .expect(\"Failed to read invalid_async.json file\");\n        let result: Result<TestData, _> = serde_json::from_str(&content);\n        result.expect_err(\"Parsing invalid JSON asynchronously should fail\");\n    }\n}\n"
  },
  {
    "path": "src/db.rs",
    "content": "//! # Database Operations\n//!\n//! This module defines functions and operations related to the application's\n//! database interactions.\n\nuse super::Result as AppResult;\nuse crate::{\n    app::{AppContext, Hooks},\n    cargo_config::CargoConfig,\n    config, doctor, env_vars,\n    errors::Error,\n};\nuse chrono::{DateTime, Utc};\nuse regex::Regex;\nuse sea_orm::{\n    ActiveModelTrait, ConnectOptions, ConnectionTrait, Database, DatabaseBackend,\n    DatabaseConnection, DbBackend, DbConn, DbErr, EntityTrait, IntoActiveModel, Statement,\n};\nuse sea_orm_migration::MigratorTrait;\nuse std::fmt::Write as FmtWrites;\nuse std::{\n    collections::{BTreeMap, HashMap, HashSet},\n    fs,\n    fs::File,\n    io::Write,\n    path::Path,\n    sync::OnceLock,\n    time::Duration,\n};\nuse tracing::info;\n\npub static EXTRACT_DB_NAME: OnceLock<Regex> = OnceLock::new();\nconst IGNORED_TABLES: &[&str] = &[\n    \"seaql_migrations\",\n    \"pg_loco_queue\",\n    \"sqlt_loco_queue\",\n    \"sqlt_loco_queue_lock\",\n];\n\nfn re_extract_db_name() -> &'static Regex {\n    EXTRACT_DB_NAME.get_or_init(|| {\n        Regex::new(r\"^.+://(?:.*?/)?([^/?#]+)(?:[?#]|$)\").expect(\"Extract db regex is correct\")\n    })\n}\n\n#[derive(Default, Clone, Debug)]\npub struct MultiDb {\n    pub db: HashMap<String, DatabaseConnection>,\n}\n\nimpl MultiDb {\n    /// Creating multiple DB connection from the given hashmap\n    ///\n    /// # Errors\n    ///\n    /// When could not create database connection\n    pub async fn new(dbs_config: HashMap<String, config::Database>) -> AppResult<Self> {\n        let mut multi_db = Self::default();\n\n        for (db_name, db_config) in dbs_config {\n            multi_db.db.insert(db_name, connect(&db_config).await?);\n        }\n\n        Ok(multi_db)\n    }\n\n    /// Retrieves a database connection instance based on the specified key\n    /// name.\n    ///\n    /// # Errors\n    ///\n    /// Returns an [`AppResult`] indicating an error if the specified key does\n    /// not correspond to a database connection in the current context.\n    pub fn get(&self, name: &str) -> AppResult<&DatabaseConnection> {\n        self.db\n            .get(name)\n            .map_or_else(|| Err(Error::Message(\"db not found\".to_owned())), Ok)\n    }\n}\n\n/// Verifies a user has access to data within its database\n///\n/// # Errors\n///\n/// This function will return an error if IO fails\n#[allow(clippy::match_wildcard_for_single_variants)]\npub async fn verify_access(db: &DatabaseConnection) -> AppResult<()> {\n    match db {\n        DatabaseConnection::SqlxPostgresPoolConnection(_) => {\n            let res = db\n                .query_all(Statement::from_string(\n                    DatabaseBackend::Postgres,\n                    \"SELECT * FROM pg_catalog.pg_tables WHERE tableowner = current_user;\",\n                ))\n                .await?;\n            if res.is_empty() {\n                return Err(Error::string(\n                    \"current user has no access to tables in the database\",\n                ));\n            }\n        }\n        DatabaseConnection::Disconnected => {\n            return Err(Error::string(\"connection to database has been closed\"));\n        }\n        _ => {}\n    }\n    Ok(())\n}\n/// converge database logic\n///\n/// # Errors\n///\n///  an `AppResult`, which is an alias for `Result<(), AppError>`. It may\n/// return an `AppError` variant representing different database operation\n/// failures.\npub async fn converge<H: Hooks, M: MigratorTrait>(\n    ctx: &AppContext,\n    config: &config::Database,\n) -> AppResult<()> {\n    if config.dangerously_recreate {\n        info!(\"recreating schema\");\n        reset::<M>(&ctx.db).await?;\n        return Ok(());\n    }\n\n    if config.auto_migrate {\n        info!(\"auto migrating\");\n        migrate::<M>(&ctx.db).await?;\n    }\n\n    if config.dangerously_truncate {\n        info!(\"truncating tables\");\n        H::truncate(ctx).await?;\n    }\n    Ok(())\n}\n\n/// Establish a connection to the database using the provided configuration\n/// settings.\n///\n/// # Errors\n///\n/// Returns a [`sea_orm::DbErr`] if an error occurs during the database\n/// connection establishment.\npub async fn connect(config: &config::Database) -> Result<DbConn, sea_orm::DbErr> {\n    let mut opt = ConnectOptions::new(&config.uri);\n    opt.max_connections(config.max_connections)\n        .min_connections(config.min_connections)\n        .connect_timeout(Duration::from_millis(config.connect_timeout))\n        .idle_timeout(Duration::from_millis(config.idle_timeout))\n        .sqlx_logging(config.enable_logging);\n\n    if let Some(acquire_timeout) = config.acquire_timeout {\n        opt.acquire_timeout(Duration::from_millis(acquire_timeout));\n    }\n\n    let db = Database::connect(opt).await?;\n\n    match db.get_database_backend() {\n        DatabaseBackend::Sqlite => {\n            db.execute(Statement::from_string(\n                DatabaseBackend::Sqlite,\n                config.run_on_start.clone().unwrap_or_else(|| {\n                    \"\n            PRAGMA foreign_keys = ON;\n            PRAGMA journal_mode = WAL;\n            PRAGMA synchronous = NORMAL;\n            PRAGMA mmap_size = 134217728;\n            PRAGMA journal_size_limit = 67108864;\n            PRAGMA cache_size = 2000;\n            PRAGMA busy_timeout = 5000;\n            \"\n                    .to_string()\n                }),\n            ))\n            .await?;\n        }\n        DatabaseBackend::Postgres | DatabaseBackend::MySql => {\n            if let Some(run_on_start) = &config.run_on_start {\n                db.execute(Statement::from_string(\n                    db.get_database_backend(),\n                    run_on_start.clone(),\n                ))\n                .await?;\n            }\n        }\n    }\n\n    Ok(db)\n}\n\n/// Extracts the database name from a given connection string.\n///\n/// # Errors\n///\n/// This function returns an error if the connection string does not match the\n/// expected format.\npub fn extract_db_name(conn_str: &str) -> AppResult<&str> {\n    re_extract_db_name()\n        .captures(conn_str)\n        .and_then(|cap| cap.get(1).map(|db| db.as_str()))\n        .ok_or_else(|| Error::string(\"could not extract db_name\"))\n}\n///  Create a new database. This functionality is currently exclusive to Postgre\n/// databases.\n///\n/// # Errors\n///\n/// Returns a [`sea_orm::DbErr`] if an error occurs during run migration up.\npub async fn create(db_uri: &str) -> AppResult<()> {\n    if !db_uri.starts_with(\"postgres://\") {\n        return Err(Error::string(\n            \"Only Postgres databases are supported for table creation\",\n        ));\n    }\n    let db_name = extract_db_name(db_uri).map_err(|_| {\n        Error::string(\"The specified table name was not found in the given Postgres database URI\")\n    })?;\n\n    let conn = db_uri.replace(db_name, \"/postgres\");\n    let db = Database::connect(conn).await?;\n\n    Ok(create_postgres_database(db_name, &db).await?)\n}\n\n/// Apply migrations to the database using the provided migrator.\n///\n/// # Errors\n///\n/// Returns a [`sea_orm::DbErr`] if an error occurs during run migration up.\npub async fn migrate<M: MigratorTrait>(db: &DatabaseConnection) -> Result<(), sea_orm::DbErr> {\n    M::up(db, None).await\n}\n\n/// Revert migrations to the database using the provided migrator.\n///\n/// # Errors\n///\n/// Returns a [`sea_orm::DbErr`] if an error occurs during run migration up.\npub async fn down<M: MigratorTrait>(\n    db: &DatabaseConnection,\n    steps: u32,\n) -> Result<(), sea_orm::DbErr> {\n    M::down(db, Some(steps)).await\n}\n\n/// Check the migration status of the database.\n///\n/// # Errors\n///\n/// Returns a [`sea_orm::DbErr`] if an error occurs during checking status\npub async fn status<M: MigratorTrait>(db: &DatabaseConnection) -> Result<(), sea_orm::DbErr> {\n    M::status(db).await\n}\n\n/// Reset the database, dropping and recreating the schema and applying\n/// migrations.\n///\n/// # Errors\n///\n/// Returns a [`sea_orm::DbErr`] if an error occurs during reset databases.\npub async fn reset<M: MigratorTrait>(db: &DatabaseConnection) -> Result<(), sea_orm::DbErr> {\n    M::fresh(db).await?;\n    migrate::<M>(db).await\n}\n\nuse sea_orm::EntityName;\nuse serde_json::{json, Value};\n/// Seed the database with data from a specified file.\n/// Seeds open the file path and insert all file content into the DB.\n///\n/// The file content should be equal to the DB field parameters.\n///\n/// # Errors\n///\n/// Returns a [`AppResult`] if could not render the path content into\n/// [`Vec<serde_json::Value>`] or could not inset the vector to DB.\n#[allow(clippy::type_repetition_in_bounds)]\npub async fn seed<A>(db: &DatabaseConnection, path: &str) -> crate::Result<()>\nwhere\n    <<A as ActiveModelTrait>::Entity as EntityTrait>::Model: IntoActiveModel<A>,\n    for<'de> <<A as ActiveModelTrait>::Entity as EntityTrait>::Model: serde::de::Deserialize<'de>,\n    A: ActiveModelTrait + Send + Sync,\n    sea_orm::Insert<A>: Send + Sync,\n    <A as ActiveModelTrait>::Entity: EntityName,\n{\n    // Deserialize YAML file into a vector of JSON values\n    let seed_data: Vec<Value> = serde_yaml::from_reader(File::open(path)?)?;\n\n    // Insert each row\n    let mut seed_models = Vec::new();\n    for row in seed_data {\n        let model = A::from_json(row)?;\n        seed_models.push(model);\n    }\n    A::Entity::insert_many(seed_models).exec(db).await?;\n\n    // Get the table name from the entity\n    let table_name = A::Entity::default().table_name().to_string();\n\n    // Get the database backend\n    let db_backend = db.get_database_backend();\n\n    // Reset auto-increment\n    reset_autoincrement(db_backend, &table_name, db).await?;\n\n    Ok(())\n}\n\n/// Checks if the specified table has an 'id' column.\n///\n/// This function checks if the specified table has an 'id' column, which is a\n/// common primary key column. It supports `Postgres`, `SQLite`, and `MySQL`\n/// database backends.\n///\n/// # Arguments\n///\n/// - `db`: A reference to the `DatabaseConnection`.\n/// - `db_backend`: A reference to the `DatabaseBackend`.\n/// - `table_name`: The name of the table to check.\n///\n/// # Returns\n///\n/// A `Result` containing a `bool` indicating whether the table has an 'id'\n/// column.\nasync fn has_id_column(\n    db: &DatabaseConnection,\n    db_backend: &DatabaseBackend,\n    table_name: &str,\n) -> crate::Result<bool> {\n    // First check if 'id' column exists\n    let result = match db_backend {\n        DatabaseBackend::Postgres => {\n            let query = format!(\n                \"SELECT EXISTS (\n              SELECT 1 \n              FROM information_schema.columns \n              WHERE table_name = '{table_name}' \n              AND column_name = 'id'\n          )\"\n            );\n            let result = db\n                .query_one(Statement::from_string(DatabaseBackend::Postgres, query))\n                .await?;\n            result.is_some_and(|row| row.try_get::<bool>(\"\", \"exists\").unwrap_or(false))\n        }\n        DatabaseBackend::Sqlite => {\n            let query = format!(\n                \"SELECT COUNT(*) as count \n          FROM pragma_table_info('{table_name}') \n          WHERE name = 'id'\"\n            );\n            let result = db\n                .query_one(Statement::from_string(DatabaseBackend::Sqlite, query))\n                .await?;\n            result.is_some_and(|row| row.try_get::<i32>(\"\", \"count\").unwrap_or(0) > 0)\n        }\n        DatabaseBackend::MySql => {\n            return Err(Error::Message(\n                \"Unsupported database backend: MySQL\".to_string(),\n            ));\n        }\n    };\n\n    Ok(result)\n}\n\n/// Checks whether the specified table has an auto-increment 'id' column.\n///\n/// # Returns\n///\n/// A `Result` containing a `bool` indicating whether the table has an\n/// auto-increment 'id' column.\nasync fn is_auto_increment(\n    db: &DatabaseConnection,\n    db_backend: &DatabaseBackend,\n    table_name: &str,\n) -> crate::Result<bool> {\n    let result = match db_backend {\n        DatabaseBackend::Postgres => {\n            let query = format!(\n                \"SELECT pg_get_serial_sequence('{table_name}', 'id') IS NOT NULL as is_serial\"\n            );\n            let result = db\n                .query_one(Statement::from_string(DatabaseBackend::Postgres, query))\n                .await?;\n            result.is_some_and(|row| row.try_get::<bool>(\"\", \"is_serial\").unwrap_or(false))\n        }\n        DatabaseBackend::Sqlite => {\n            let query =\n                format!(\"SELECT sql FROM sqlite_master WHERE type='table' AND name='{table_name}'\");\n            let result = db\n                .query_one(Statement::from_string(DatabaseBackend::Sqlite, query))\n                .await?;\n            result.is_some_and(|row| {\n                row.try_get::<String>(\"\", \"sql\")\n                    .is_ok_and(|sql| sql.to_lowercase().contains(\"autoincrement\"))\n            })\n        }\n        DatabaseBackend::MySql => {\n            return Err(Error::Message(\n                \"Unsupported database backend: MySQL\".to_string(),\n            ));\n        }\n    };\n    Ok(result)\n}\n\n/// Function to reset auto-increment\n/// # Errors\n/// Returns error if it fails\npub async fn reset_autoincrement(\n    db_backend: DatabaseBackend,\n    table_name: &str,\n    db: &DatabaseConnection,\n) -> crate::Result<()> {\n    // Check if 'id' column exists\n    let has_id_column = has_id_column(db, &db_backend, table_name).await?;\n    if !has_id_column {\n        return Ok(());\n    }\n    // Check if 'id' column is auto-increment\n    let is_auto_increment = is_auto_increment(db, &db_backend, table_name).await?;\n    if !is_auto_increment {\n        return Ok(());\n    }\n\n    match db_backend {\n        DatabaseBackend::Postgres => {\n            let query_str = format!(\n                \"SELECT setval(pg_get_serial_sequence('{table_name}', 'id'), COALESCE(MAX(id), 0) \\\n                 + 1, false) FROM {table_name}\"\n            );\n            db.execute(Statement::from_sql_and_values(\n                DatabaseBackend::Postgres,\n                &query_str,\n                vec![],\n            ))\n            .await?;\n        }\n        DatabaseBackend::Sqlite => {\n            let query_str = format!(\n                \"UPDATE sqlite_sequence SET seq = (SELECT MAX(id) FROM {table_name}) WHERE name = \\\n                 '{table_name}'\"\n            );\n            db.execute(Statement::from_sql_and_values(\n                DatabaseBackend::Sqlite,\n                &query_str,\n                vec![],\n            ))\n            .await?;\n        }\n        DatabaseBackend::MySql => {\n            return Err(Error::Message(\n                \"Unsupported database backend: MySQL\".to_string(),\n            ));\n        }\n    }\n    Ok(())\n}\n\nstruct EntityCmd {\n    command: Vec<String>,\n    flags: BTreeMap<String, Option<String>>,\n}\n\nimpl EntityCmd {\n    fn new(config: &config::Database) -> Self {\n        Self {\n            command: vec![\"generate\".to_string(), \"entity\".to_string()],\n            flags: BTreeMap::from([\n                (\"--database-url\".to_string(), Some(config.uri.clone())),\n                (\n                    \"--ignore-tables\".to_string(),\n                    Some(IGNORED_TABLES.join(\",\")),\n                ),\n                (\n                    \"--output-dir\".to_string(),\n                    Some(\"src/models/_entities\".to_string()),\n                ),\n                (\"--with-serde\".to_string(), Some(\"both\".to_string())),\n                (\"--with-copy-enums\".to_string(), None),\n            ]),\n        }\n    }\n\n    fn merge_with_config(config: &config::Database, toml_config: &toml::Table) -> Self {\n        let mut flags = Self::new(config).flags;\n\n        for (key, value) in toml_config {\n            let flag_key = format!(\"--{}\", key.replace('_', \"-\"));\n\n            // Handle special cases\n            match flag_key.as_str() {\n                \"--output-dir\" | \"--database-url\" => {\n                    tracing::warn!(\n                        \"Ignoring {} configuration from Cargo.toml as it cannot be overridden\",\n                        key\n                    );\n                    continue;\n                }\n                \"--ignore-tables\" => {\n                    if let (Some(current_str), Some(new_value)) = (\n                        flags.get_mut(&flag_key).and_then(|c| c.as_mut()),\n                        value.as_str(),\n                    ) {\n                        *current_str = format!(\"{current_str},{new_value}\");\n                    }\n                    continue;\n                }\n                _ => {}\n            }\n\n            // Handle regular flags\n            let flag_value = match value {\n                toml::Value::String(s) => Some(s.clone()),\n                toml::Value::Boolean(true) => None,\n                toml::Value::Boolean(false) => continue,\n                _ => Some(value.to_string()),\n            };\n\n            flags.insert(flag_key, flag_value);\n        }\n\n        Self {\n            command: vec![\"generate\".to_string(), \"entity\".to_string()],\n            flags,\n        }\n    }\n\n    fn command(&self) -> Vec<&str> {\n        let mut args: Vec<&str> = self\n            .command\n            .iter()\n            .map(std::string::String::as_str)\n            .collect();\n        for (flag, value) in &self.flags {\n            args.push(flag.as_str());\n            if let Some(val) = value {\n                args.push(val.as_str());\n            }\n        }\n        args\n    }\n}\n\n/// Generate entity model.\n/// This function using sea-orm-cli.\n///\n/// # Errors\n///\n/// Returns a [`AppResult`] if an error occurs during generate model entity.\npub async fn entities<M: MigratorTrait>(ctx: &AppContext) -> AppResult<String> {\n    doctor::check_seaorm_cli()?.to_result()?;\n    doctor::check_db(&ctx.config.database).await.to_result()?;\n\n    let flags = CargoConfig::from_current_dir()?\n        .get_db_entities()\n        .map_or_else(\n            || EntityCmd::new(&ctx.config.database),\n            |entity_config| {\n                tracing::info!(\n                    ?entity_config,\n                    \"Found db.entity configuration in Cargo.toml\"\n                );\n                EntityCmd::merge_with_config(&ctx.config.database, entity_config)\n            },\n        );\n\n    let out = duct::cmd(\"sea-orm-cli\", &flags.command())\n        .stderr_to_stdout()\n        .run()\n        .map_err(|err| {\n            Error::Message(format!(\n                \"failed to generate entity using sea-orm-cli binary. error details: `{err}`\",\n            ))\n        })?;\n\n    fix_entities()?;\n\n    Ok(String::from_utf8_lossy(&out.stdout).to_string())\n}\n\n// see https://github.com/SeaQL/sea-orm/pull/1947\n// also we are generating an extension module from the get go\nfn fix_entities() -> AppResult<()> {\n    let dir = fs::read_dir(\"src/models/_entities\")?\n        .filter_map(|ent| {\n            let ent = ent.unwrap();\n            if ent.path().is_file()\n                && ent.file_name() != \"mod.rs\"\n                && ent.file_name() != \"prelude.rs\"\n            {\n                Some(ent.path())\n            } else {\n                None\n            }\n        })\n        .collect::<Vec<_>>();\n\n    // remove activemodel impl from all generated entities, and make note to\n    // generate a new extension for those who had it\n    let activemodel_exp = \"impl ActiveModelBehavior for ActiveModel {}\";\n    let mut cleaned_entities = Vec::new();\n    for file in &dir {\n        let content = fs::read_to_string(file)?;\n        if content.contains(activemodel_exp) {\n            let content = content\n                .lines()\n                .filter(|line| !line.contains(activemodel_exp))\n                .collect::<Vec<_>>()\n                .join(\"\\n\");\n            fs::write(file, content)?;\n            cleaned_entities.push(file);\n        }\n    }\n\n    // generate an empty extension with impl activemodel behavior\n    let mut models_mod = fs::read_to_string(\"src/models/mod.rs\")?;\n    for entity_file in cleaned_entities {\n        let new_file = Path::new(\"src/models\").join(\n            entity_file\n                .file_name()\n                .ok_or_else(|| Error::string(\"cannot extract file name\"))?,\n        );\n\n        if !new_file.exists() {\n            // Check if the entity has an updated_at field\n            let entity_content = fs::read_to_string(entity_file)?;\n            let has_updated_at = entity_content.contains(\"pub updated_at: DateTimeWithTimeZone\");\n\n            let module = new_file\n                .file_stem()\n                .ok_or_else(|| Error::string(\"cannot extract file stem\"))?\n                .to_str()\n                .ok_or_else(|| Error::string(\"cannot extract file stem\"))?;\n            let module_pascal = heck::AsPascalCase(module);\n\n            // Conditionally generate the ActiveModelBehavior implementation\n            let before_save_impl = if has_updated_at {\n                r\"#[async_trait::async_trait]\nimpl ActiveModelBehavior for ActiveModel {\n    async fn before_save<C>(self, _db: &C, insert: bool) -> std::result::Result<Self, DbErr>\n    where\n        C: ConnectionTrait,\n    {\n        if !insert && self.updated_at.is_unchanged() {\n            let mut this = self;\n            this.updated_at = sea_orm::ActiveValue::Set(chrono::Utc::now().into());\n            Ok(this)\n        } else {\n            Ok(self)\n        }\n    }\n}\"\n            } else {\n                r\"#[async_trait::async_trait]\nimpl ActiveModelBehavior for ActiveModel {\n    async fn before_save<C>(self, _db: &C, _insert: bool) -> std::result::Result<Self, DbErr>\n    where\n        C: ConnectionTrait,\n    {\n        Ok(self)\n    }\n}\"\n            };\n\n            fs::write(\n                &new_file,\n                format!(\n                    r\"use sea_orm::entity::prelude::*;\npub use super::_entities::{module}::{{ActiveModel, Model, Entity}};\npub type {module_pascal} = Entity;\n\n{before_save_impl}\n\n// implement your read-oriented logic here\nimpl Model {{}}\n\n// implement your write-oriented logic here\nimpl ActiveModel {{}}\n\n// implement your custom finders, selectors oriented logic here\nimpl Entity {{}}\n\"\n                ),\n            )?;\n            if !models_mod.contains(&format!(\"mod {module}\")) {\n                let _ = writeln!(models_mod, \"pub mod {module};\");\n            }\n        }\n    }\n\n    fs::write(\"src/models/mod.rs\", models_mod)?;\n\n    Ok(())\n}\n\n/// Truncate a table in the database, effectively deleting all rows.\n///\n/// # Errors\n///\n/// Returns a [`AppResult`] if an error occurs during truncate the given table\npub async fn truncate_table<T>(db: &DatabaseConnection, _: T) -> Result<(), sea_orm::DbErr>\nwhere\n    T: EntityTrait,\n{\n    T::delete_many().exec(db).await?;\n    Ok(())\n}\n\n/// Execute seed from the given path\n///\n/// # Errors\n///\n/// when seed process is fails\npub async fn run_app_seed<H: Hooks>(ctx: &AppContext, path: &Path) -> AppResult<()> {\n    H::seed(ctx, path).await\n}\n\n/// Create a Postgres database from the given db name.\n///\n/// To create the database with `LOCO_POSTGRES_DB_OPTIONS`\nasync fn create_postgres_database(\n    db_name: &str,\n    db: &DatabaseConnection,\n) -> Result<(), sea_orm::DbErr> {\n    let mut select = sea_orm::sea_query::Query::select();\n    select\n        .expr(sea_orm::sea_query::Expr::val(1))\n        .from(sea_orm::sea_query::Alias::new(\"pg_database\"))\n        .and_where(\n            sea_orm::sea_query::Expr::col(sea_orm::sea_query::Alias::new(\"datname\")).eq(db_name),\n        )\n        .limit(1);\n\n    let (sql, values) = select.build(sea_orm::sea_query::PostgresQueryBuilder);\n    let statement = Statement::from_sql_and_values(DatabaseBackend::Postgres, sql, values);\n\n    if db.query_one(statement).await?.is_some() {\n        tracing::info!(db_name, \"database already exists\");\n\n        return Err(sea_orm::DbErr::Custom(\"database already exists\".to_owned()));\n    }\n\n    let with_options = env_vars::get_or_default(env_vars::POSTGRES_DB_OPTIONS, \"ENCODING='UTF8'\");\n\n    let query = format!(\"CREATE DATABASE {db_name} WITH {with_options}\");\n    tracing::info!(query, \"creating postgres database\");\n\n    db.execute(sea_orm::Statement::from_string(\n        sea_orm::DatabaseBackend::Postgres,\n        query,\n    ))\n    .await?;\n    Ok(())\n}\n\n/// Retrieves a list of table names from the database.\n///\n///\n/// # Errors\n///\n/// Returns an error if the operation fails for any reason, such as an\n/// unsupported database backend or a query execution issue.\npub async fn get_tables(db: &DatabaseConnection) -> AppResult<Vec<String>> {\n    let query = match db.get_database_backend() {\n        DatabaseBackend::MySql => {\n            return Err(Error::Message(\n                \"Unsupported database backend: MySQL\".to_string(),\n            ));\n        }\n        DatabaseBackend::Postgres => {\n            \"SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'\"\n        }\n        DatabaseBackend::Sqlite => {\n            \"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'\"\n        }\n    };\n\n    let result = db\n        .query_all(Statement::from_string(\n            db.get_database_backend(),\n            query.to_string(),\n        ))\n        .await?;\n\n    Ok(result\n        .into_iter()\n        .filter_map(|row| {\n            let col = match db.get_database_backend() {\n                sea_orm::DatabaseBackend::MySql | sea_orm::DatabaseBackend::Postgres => {\n                    \"table_name\"\n                }\n                sea_orm::DatabaseBackend::Sqlite => \"name\",\n            };\n\n            if let Ok(table_name) = row.try_get::<String>(\"\", col) {\n                if IGNORED_TABLES.contains(&table_name.as_str()) {\n                    return None;\n                }\n                Some(table_name)\n            } else {\n                None\n            }\n        })\n        .collect())\n}\n\n/// Returns the set of column names that are of boolean type for the given table.\n///\n/// This uses lightweight schema metadata queries and is used by the dump logic\n/// to serialize boolean columns as `true` / `false` instead of `0` / `1`.\nasync fn get_boolean_columns(\n    db: &DatabaseConnection,\n    table_name: &str,\n) -> AppResult<HashSet<String>> {\n    let backend = db.get_database_backend();\n    let mut bool_cols = HashSet::new();\n\n    match backend {\n        DatabaseBackend::Postgres => {\n            let query = \"\n                SELECT column_name\n                FROM information_schema.columns\n                WHERE table_schema = 'public'\n                  AND table_name = $1\n                  AND data_type = 'boolean'\n            \";\n\n            let stmt = Statement::from_sql_and_values(backend, query, vec![table_name.into()]);\n            let rows = db.query_all(stmt).await?;\n            for row in rows {\n                if let Ok(col) = row.try_get::<String>(\"\", \"column_name\") {\n                    bool_cols.insert(col);\n                }\n            }\n        }\n        DatabaseBackend::Sqlite => {\n            let query = format!(\"PRAGMA table_info('{table_name}')\");\n            let stmt = Statement::from_string(backend, query);\n            let rows = db.query_all(stmt).await?;\n            for row in rows {\n                let col_name = row.try_get::<String>(\"\", \"name\")?;\n                let col_type: String = row.try_get::<String>(\"\", \"type\").unwrap_or_default();\n                if col_type.to_ascii_uppercase().contains(\"BOOL\") {\n                    bool_cols.insert(col_name);\n                }\n            }\n        }\n        DatabaseBackend::MySql => {\n            return Err(Error::Message(\n                \"Unsupported database backend: MySQL\".to_string(),\n            ));\n        }\n    }\n\n    Ok(bool_cols)\n}\n\n/// Dumps the contents of specified database tables into YAML files.\n///\n/// # Errors\n/// This function retrieves data from all tables in the database, filters them\n/// if `only_tables` is provided, and writes each table's content to a separate\n/// YAML file in the specified directory.\n///\n/// Returns an error if the operation fails for any reason or could not save the\n/// content into a file.\n#[allow(clippy::cognitive_complexity)]\npub async fn dump_tables(\n    db: &DatabaseConnection,\n    to: &Path,\n    only_tables: Option<Vec<String>>,\n) -> AppResult<()> {\n    tracing::debug!(\"getting tables from the database\");\n\n    let tables = get_tables(db).await?;\n    tracing::info!(tables = ?tables, \"found tables\");\n\n    for table in tables {\n        if let Some(ref only_tables) = only_tables {\n            if !only_tables.contains(&table) {\n                tracing::info!(table, \"skipping table as it is not in the specified list\");\n                continue;\n            }\n        }\n\n        tracing::info!(table, \"get table data\");\n\n        // Discover which columns are booleans so we can dump them as true/false\n        // instead of 0/1 while keeping numeric IDs and other integers intact.\n        let boolean_columns = get_boolean_columns(db, &table).await?;\n\n        let data_result = db\n            .query_all(Statement::from_string(\n                db.get_database_backend(),\n                format!(r#\"SELECT * FROM \"{table}\"\"#),\n            ))\n            .await?;\n\n        tracing::info!(\n            table,\n            rows_fetched = data_result.len(),\n            \"fetched rows from table\"\n        );\n\n        let mut table_data: Vec<BTreeMap<String, serde_json::Value>> = Vec::new();\n\n        if !to.exists() {\n            tracing::info!(\"the specified dump folder does not exist. creating the folder now\");\n            fs::create_dir_all(to)?;\n        }\n\n        for row in data_result {\n            // Use BTreeMap to ensure deterministic key order in YAML snapshots.\n            let mut row_data: BTreeMap<String, serde_json::Value> = BTreeMap::new();\n\n            for col_name in row.column_names() {\n                let value_result = row\n                    // Try native JSON value first so JSON/JSONB columns and JSON-like\n                    // text (e.g. '{\\\"k\\\": \\\"v\\\"}', '[1,2,3]') are captured as structured\n                    // data instead of being double-quoted strings in YAML.\n                    .try_get::<serde_json::Value>(\"\", &col_name)\n                    // Fallback to string and scalar types for non-JSON columns.\n                    .or_else(|_| {\n                        row.try_get::<String>(\"\", &col_name)\n                            .map(serde_json::Value::String)\n                    })\n                    .or_else(|_| {\n                        row.try_get::<i8>(\"\", &col_name)\n                            .map(serde_json::Value::from)\n                    })\n                    .or_else(|_| {\n                        row.try_get::<i16>(\"\", &col_name)\n                            .map(serde_json::Value::from)\n                    })\n                    .or_else(|_| {\n                        row.try_get::<i32>(\"\", &col_name)\n                            .map(serde_json::Value::from)\n                    })\n                    .or_else(|_| {\n                        row.try_get::<i64>(\"\", &col_name)\n                            .map(serde_json::Value::from)\n                    })\n                    .or_else(|_| {\n                        row.try_get::<f32>(\"\", &col_name)\n                            .map(serde_json::Value::from)\n                    })\n                    .or_else(|_| {\n                        row.try_get::<f64>(\"\", &col_name)\n                            .map(serde_json::Value::from)\n                    })\n                    .or_else(|_| {\n                        row.try_get::<uuid::Uuid>(\"\", &col_name)\n                            .map(|v| serde_json::Value::String(v.to_string()))\n                    })\n                    .or_else(|_| {\n                        row.try_get::<DateTime<Utc>>(\"\", &col_name)\n                            .map(|v| serde_json::Value::String(v.to_rfc3339()))\n                    })\n                    .ok();\n\n                if let Some(mut value) = value_result {\n                    if boolean_columns.contains(&col_name) {\n                        if let serde_json::Value::Number(num) = &value {\n                            if let Some(i) = num.as_i64() {\n                                value = serde_json::Value::Bool(i != 0);\n                            }\n                        }\n                    }\n\n                    row_data.insert(col_name, value);\n                }\n            }\n            table_data.push(row_data);\n        }\n\n        let data = serde_yaml::to_string(&table_data)?;\n\n        let file_db_content_path = to.join(format!(\"{table}.yaml\"));\n\n        let mut file = File::create(&file_db_content_path)?;\n        file.write_all(data.as_bytes())?;\n        tracing::info!(table, file_db_content_path = %file_db_content_path.display(), \"table data written to YAML file\");\n    }\n\n    tracing::info!(\"dumping tables process completed successfully\");\n\n    Ok(())\n}\n\n/// dumps the db schema into file.\n///\n/// # Errors\n/// Fails with IO / sql fails\npub async fn dump_schema(ctx: &AppContext, fname: &str) -> crate::Result<()> {\n    let db = &ctx.db;\n\n    // Match the database backend and fetch schema info\n    let schema_info = match db.get_database_backend() {\n        DbBackend::Postgres => {\n            let query = r\"\n                SELECT table_name, column_name, data_type\n                FROM information_schema.columns\n                WHERE table_schema = 'public'\n                ORDER BY table_name, ordinal_position;\n            \";\n            let stmt = Statement::from_string(DbBackend::Postgres, query.to_owned());\n            let rows = db.query_all(stmt).await?;\n            rows.into_iter()\n                .map(|row| {\n                    // Wrap the closure in a Result to handle errors properly\n                    Ok(json!({\n                        \"table\": row.try_get::<String>(\"\", \"table_name\")?,\n                        \"column\": row.try_get::<String>(\"\", \"column_name\")?,\n                        \"type\": row.try_get::<String>(\"\", \"data_type\")?,\n                    }))\n                })\n                .collect::<Result<Vec<serde_json::Value>, DbErr>>()? // Specify error type explicitly\n        }\n        DbBackend::MySql => {\n            let query = r\"\n                SELECT TABLE_NAME, COLUMN_NAME, COLUMN_TYPE\n                FROM INFORMATION_SCHEMA.COLUMNS\n                WHERE TABLE_SCHEMA = DATABASE()\n                ORDER BY TABLE_NAME, ORDINAL_POSITION;\n            \";\n            let stmt = Statement::from_string(DbBackend::MySql, query.to_owned());\n            let rows = db.query_all(stmt).await?;\n            rows.into_iter()\n                .map(|row| {\n                    // Wrap the closure in a Result to handle errors properly\n                    Ok(json!({\n                        \"table\": row.try_get::<String>(\"\", \"TABLE_NAME\")?,\n                        \"column\": row.try_get::<String>(\"\", \"COLUMN_NAME\")?,\n                        \"type\": row.try_get::<String>(\"\", \"COLUMN_TYPE\")?,\n                    }))\n                })\n                .collect::<Result<Vec<serde_json::Value>, DbErr>>()? // Specify error type explicitly\n        }\n        DbBackend::Sqlite => {\n            let query = r\"\n                SELECT name AS table_name, sql AS table_sql\n                FROM sqlite_master\n                WHERE type = 'table' AND name NOT LIKE 'sqlite_%'\n                ORDER BY name;\n            \";\n            let stmt = Statement::from_string(DbBackend::Sqlite, query.to_owned());\n            let rows = db.query_all(stmt).await?;\n            rows.into_iter()\n                .map(|row| {\n                    // Wrap the closure in a Result to handle errors properly\n                    Ok(json!({\n                        \"table\": row.try_get::<String>(\"\", \"table_name\")?,\n                        \"sql\": row.try_get::<String>(\"\", \"table_sql\")?,\n                    }))\n                })\n                .collect::<Result<Vec<serde_json::Value>, DbErr>>()? // Specify error type explicitly\n        }\n    };\n    // Serialize schema info to JSON format\n    let schema_json = serde_json::to_string_pretty(&schema_info)?;\n\n    // Save the schema to a file\n    std::fs::write(fname, schema_json)?;\n    Ok(())\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use crate::tests_cfg::{\n        config::get_database_config, db::get_value, postgres::setup_postgres_container,\n    };\n\n    #[tokio::test]\n    async fn test_sqlite_connect_success() {\n        let (config, _tree_fs) = crate::tests_cfg::config::get_sqlite_test_config(\"test\");\n\n        let result = connect(&config).await;\n        assert!(\n            result.is_ok(),\n            \"Failed to connect to SQLite: {:?}\",\n            result.err()\n        );\n\n        let db = result.unwrap();\n        assert_eq!(db.get_database_backend(), DatabaseBackend::Sqlite);\n    }\n\n    #[tokio::test]\n    async fn test_postgres_connect_success() {\n        let (pg_url, _container) = setup_postgres_container().await;\n\n        let mut config = crate::tests_cfg::config::get_database_config();\n        config.uri = pg_url;\n        config.min_connections = 1;\n        config.max_connections = 5;\n\n        let result = connect(&config).await;\n        assert!(\n            result.is_ok(),\n            \"Failed to connect to PostgreSQL: {:?}\",\n            result.err()\n        );\n\n        let db = result.unwrap();\n        assert_eq!(db.get_database_backend(), DatabaseBackend::Postgres);\n    }\n\n    #[tokio::test]\n    async fn test_sqlite_default_run_on_start() {\n        let (config, _tree_fs) = crate::tests_cfg::config::get_sqlite_test_config(\"test\");\n\n        let db = connect(&config).await.expect(\"Failed to connect to SQLite\");\n\n        let expected_pragmas = [\n            (\"foreign_keys\", \"1\"),\n            (\"journal_mode\", \"wal\"),\n            (\"synchronous\", \"1\"),\n            (\"mmap_size\", \"134217728\"),\n            (\"journal_size_limit\", \"67108864\"),\n            (\"cache_size\", \"2000\"),\n            (\"busy_timeout\", \"5000\"),\n        ];\n\n        for (pragma, expected_value) in expected_pragmas {\n            let query = format!(\"PRAGMA {pragma}\");\n            let actual_value = get_value(&db, &query).await;\n\n            assert_eq!(\n                actual_value,\n                expected_value.to_lowercase(),\n                \"PRAGMA {pragma} value mismatch - expected '{expected_value}', got '{actual_value}'\"\n            );\n        }\n    }\n\n    #[tokio::test]\n    async fn test_sqlite_custom_run_on_start() {\n        let (mut config, _tree_fs) =\n            crate::tests_cfg::config::get_sqlite_test_config(\"test_custom\");\n\n        config.run_on_start = Some(\n            \"\n            PRAGMA foreign_keys = OFF;\n            PRAGMA journal_mode = DELETE;\n            PRAGMA synchronous = OFF;\n            PRAGMA cache_size = -1000;\n            PRAGMA busy_timeout = 2000;\n        \"\n            .to_string(),\n        );\n\n        let db = connect(&config).await.expect(\"Failed to connect to SQLite\");\n\n        let expected_pragmas = [\n            (\"foreign_keys\", \"0\"),\n            (\"journal_mode\", \"delete\"),\n            (\"synchronous\", \"0\"),\n            (\"cache_size\", \"-1000\"),\n            (\"busy_timeout\", \"2000\"),\n        ];\n\n        for (pragma, expected_value) in expected_pragmas {\n            let query = format!(\"PRAGMA {pragma}\");\n            let actual_value = get_value(&db, &query).await;\n\n            assert_eq!(\n                actual_value,\n                expected_value.to_lowercase(),\n                \"PRAGMA {pragma} value mismatch - expected '{expected_value}', got '{actual_value}'\"\n            );\n        }\n    }\n\n    #[tokio::test]\n    async fn test_postgres_run_on_start() {\n        let (pg_url, _container) = setup_postgres_container().await;\n\n        let mut config = crate::tests_cfg::config::get_database_config();\n        config.uri = pg_url;\n        config.run_on_start = Some(\n            \"CREATE TABLE IF NOT EXISTS test_run_on_start (id SERIAL PRIMARY KEY, name TEXT);\"\n                .to_string(),\n        );\n\n        let db = connect(&config)\n            .await\n            .expect(\"Failed to connect to PostgreSQL\");\n\n        assert_eq!(db.get_database_backend(), DatabaseBackend::Postgres);\n\n        let query = \"SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'test_run_on_start'\";\n\n        let value = get_value(&db, query).await;\n        assert_eq!(value, \"1\", \"The test_run_on_start table was not created\");\n    }\n\n    #[cfg(test)]\n    mod extract_db_name_tests {\n        use super::*;\n        use rstest::rstest;\n\n        #[rstest]\n        #[case(\"postgres://localhost:5432/dbname\", \"dbname\")]\n        #[case(\"postgres://username@localhost:5432/dbname\", \"dbname\")]\n        #[case(\"postgres://username:password@localhost:5432/dbname\", \"dbname\")]\n        #[case(\"postgres://localhost:5432/dbname?param1=value1\", \"dbname\")]\n        #[case(\n            \"postgres://username:password@localhost:5432/dbname?param1=value1\",\n            \"dbname\"\n        )]\n        #[case(\n            \"postgres://username:password@localhost:5432/dbname?param1=value1&param2=value2\",\n            \"dbname\"\n        )]\n        #[case(\"postgres://localhost/dbname\", \"dbname\")]\n        #[case(\"postgres://localhost/dbname?\", \"dbname\")]\n        #[case(\"sqlite://dbname.sqlite\", \"dbname.sqlite\")]\n        #[case(\"sqlite://dbname.sqlite?mode=rwc\", \"dbname.sqlite\")]\n        #[case(\"sqlite:///path/to/dbname.sqlite\", \"dbname.sqlite\")]\n        #[case(\"sqlite://./dbname.sqlite\", \"dbname.sqlite\")]\n        #[case(\"sqlite://./path/to/dbname.sqlite?mode=rwc\", \"dbname.sqlite\")]\n        #[case(\n            \"postgres://localhost:5432/db-name-with-hyphens\",\n            \"db-name-with-hyphens\"\n        )]\n        #[case(\n            \"postgres://localhost:5432/db_name_with_underscores\",\n            \"db_name_with_underscores\"\n        )]\n        #[case(\"postgres://localhost:5432/123numeric_db\", \"123numeric_db\")]\n        #[case(\"postgres://localhost:5432/dbname?\", \"dbname\")]\n        #[case(\"postgres://localhost:5432/dbname#fragment\", \"dbname\")]\n        #[case(\n            \"sqlite:///absolute/path/to/db file with spaces.sqlite\",\n            \"db file with spaces.sqlite\"\n        )]\n        #[case(\n            \"sqlite://./relative/path/to/db.sqlite?cache=shared&mode=rwc\",\n            \"db.sqlite\"\n        )]\n        #[case(\"postgres://localhost:5432/dbname?sslmode=require\", \"dbname\")]\n        #[case(\"postgres://localhost:5432/empty-p?assword\", \"empty-p\")]\n        fn test_extract_db_name(#[case] conn_str: &str, #[case] expected: &str) {\n            let result = extract_db_name(conn_str);\n            assert!(result.is_ok(), \"Failed to extract db name from {conn_str}\");\n            assert_eq!(\n                result.unwrap(),\n                expected,\n                \"Extracted incorrect db name from {conn_str}\"\n            );\n        }\n\n        #[rstest]\n        #[case(\"sqlite::memory:\")]\n        #[case(\"postgres://\")]\n        #[case(\"postgres:///\")]\n        #[case(\"postgres://localhost:5432/?param=value\")]\n        #[case(\"sqlite:\")]\n        #[case(\"file:dbname.sqlite\")]\n        #[case(\"://username:password@localhost:5432/dbname\")]\n        fn test_extract_db_name_failure(#[case] conn_str: &str) {\n            let result = extract_db_name(conn_str);\n            assert!(\n                result.is_err(),\n                \"Expected error but got success for {conn_str}\"\n            );\n        }\n    }\n\n    #[tokio::test]\n    async fn test_postgres_create_database() {\n        let (pg_url, _container) = setup_postgres_container().await;\n\n        let base_url = pg_url.split('/').take(3).collect::<Vec<&str>>().join(\"/\");\n\n        let test_db_name = \"test_create_db\";\n        let create_db_url = format!(\"{base_url}/{test_db_name}\");\n\n        let mut config = crate::tests_cfg::config::get_database_config();\n        config.uri = pg_url.clone();\n        let db = connect(&config)\n            .await\n            .expect(\"Failed to connect to default database\");\n\n        let query = format!(\"SELECT COUNT(*) FROM pg_database WHERE datname = '{test_db_name}'\");\n        let count_before = get_value(&db, &query).await;\n        assert_eq!(\n            count_before, \"0\",\n            \"Test database '{test_db_name}' already exists\"\n        );\n\n        let result = create(&create_db_url).await;\n        assert!(\n            result.is_ok(),\n            \"Failed to create PostgreSQL database: {:?}\",\n            result.err()\n        );\n\n        let query = format!(\"SELECT COUNT(*) FROM pg_database WHERE datname = '{test_db_name}'\");\n        let count_before = get_value(&db, &query).await;\n        assert_eq!(\n            count_before, \"1\",\n            \"Test database '{test_db_name}' not exists\"\n        );\n    }\n\n    #[tokio::test]\n    async fn test_postgres_has_id_column() {\n        let (pg_url, _container) = setup_postgres_container().await;\n        let mut config = crate::tests_cfg::config::get_database_config();\n        config.uri = pg_url;\n        let db = connect(&config)\n            .await\n            .expect(\"Failed to connect to PostgreSQL\");\n        let backend = db.get_database_backend();\n\n        let table_no_id = \"test_table_no_id\";\n        db.execute(Statement::from_string(\n            backend,\n            format!(\"CREATE TABLE {table_no_id} (name TEXT);\"),\n        ))\n        .await\n        .expect(\"Failed to create table without id\");\n\n        let has_id = has_id_column(&db, &backend, table_no_id)\n            .await\n            .expect(\"Failed to check for id column\");\n        assert!(\n            !has_id,\n            \"Table '{table_no_id}' should NOT have an 'id' column, but check returned true\"\n        );\n\n        let table_with_id = \"test_table_with_id\";\n        db.execute(Statement::from_string(\n            backend,\n            format!(\"CREATE TABLE {table_with_id} (id INTEGER PRIMARY KEY, name TEXT);\"),\n        ))\n        .await\n        .expect(\"Failed to create table with id\");\n\n        let has_id = has_id_column(&db, &backend, table_with_id)\n            .await\n            .expect(\"Failed to check for id column\");\n        assert!(\n            has_id,\n            \"Table '{table_with_id}' SHOULD have an 'id' column, but check returned false\"\n        );\n\n        let table_with_serial_id = \"test_table_with_serial_id\";\n        db.execute(Statement::from_string(\n            backend,\n            format!(\"CREATE TABLE {table_with_serial_id} (id SERIAL PRIMARY KEY, name TEXT);\"),\n        ))\n        .await\n        .expect(\"Failed to create table with serial id\");\n\n        let has_id = has_id_column(&db, &backend, table_with_serial_id)\n            .await\n            .expect(\"Failed to check for id column\");\n        assert!(\n            has_id,\n            \"Table '{table_with_serial_id}' SHOULD have an 'id' column, but check returned false\"\n        );\n    }\n\n    #[tokio::test]\n    async fn test_sqlite_has_id_column() {\n        let (config, _tree_fs) = crate::tests_cfg::config::get_sqlite_test_config(\"test_has_id\");\n\n        let db = connect(&config).await.expect(\"Failed to connect to SQLite\");\n        let backend = db.get_database_backend();\n        assert_eq!(backend, DatabaseBackend::Sqlite);\n\n        let table_no_id = \"test_table_no_id\";\n        db.execute(Statement::from_string(\n            backend,\n            format!(\"CREATE TABLE {table_no_id} (name TEXT);\"),\n        ))\n        .await\n        .expect(\"Failed to create table without id\");\n\n        let has_id = has_id_column(&db, &backend, table_no_id)\n            .await\n            .expect(\"Failed to check for id column\");\n        assert!(\n            !has_id,\n            \"Table '{table_no_id}' should NOT have an 'id' column, but check returned true\"\n        );\n\n        let table_with_id = \"test_table_with_id\";\n        db.execute(Statement::from_string(\n            backend,\n            // SQLite uses INTEGER PRIMARY KEY for rowid alias\n            format!(\"CREATE TABLE {table_with_id} (id INTEGER PRIMARY KEY, name TEXT);\"),\n        ))\n        .await\n        .expect(\"Failed to create table with id\");\n\n        let has_id = has_id_column(&db, &backend, table_with_id)\n            .await\n            .expect(\"Failed to check for id column\");\n        assert!(\n            has_id,\n            \"Table '{table_with_id}' SHOULD have an 'id' column, but check returned false\"\n        );\n\n        let table_with_auto_id = \"test_table_with_auto_id\";\n        db.execute(Statement::from_string(\n            backend,\n            // AUTOINCREMENT keyword is important for SQLite's sequence behavior\n            format!(\"CREATE TABLE {table_with_auto_id} (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT);\"),\n        ))\n        .await\n        .expect(\"Failed to create table with auto id\");\n\n        let has_id = has_id_column(&db, &backend, table_with_auto_id)\n            .await\n            .expect(\"Failed to check for id column\");\n        assert!(\n            has_id,\n            \"Table '{table_with_auto_id}' SHOULD have an 'id' column, but check returned false\"\n        );\n    }\n\n    #[tokio::test]\n    async fn test_postgres_is_auto_increment() {\n        let (pg_url, _container) = setup_postgres_container().await;\n        let mut config = crate::tests_cfg::config::get_database_config();\n        config.uri = pg_url;\n        let db = connect(&config)\n            .await\n            .expect(\"Failed to connect to PostgreSQL\");\n        let backend = db.get_database_backend();\n\n        let table_no_id = \"test_table_no_id_auto\";\n        db.execute(Statement::from_string(\n            backend,\n            format!(\"CREATE TABLE {table_no_id} (name TEXT);\"),\n        ))\n        .await\n        .expect(\"Failed to create table without id\");\n\n        let has_id = has_id_column(&db, &backend, table_no_id)\n            .await\n            .expect(\"Failed to check for id column existence\");\n        assert!(\n            !has_id,\n            \"Table '{table_no_id}' should not have an 'id' column.\"\n        );\n\n        let auto_inc_result = is_auto_increment(&db, &backend, table_no_id).await;\n        assert!(\n            auto_inc_result.is_err(),\n            \"is_auto_increment should error if 'id' column doesn't exist, but it returned Ok\"\n        );\n\n        let table_with_id_not_auto = \"test_table_id_not_auto\";\n        db.execute(Statement::from_string(\n            backend,\n            format!(\"CREATE TABLE {table_with_id_not_auto} (id INTEGER PRIMARY KEY, name TEXT);\"),\n        ))\n        .await\n        .expect(\"Failed to create table with non-auto id\");\n\n        let is_auto = is_auto_increment(&db, &backend, table_with_id_not_auto)\n            .await\n            .expect(\"Failed to check auto-increment\");\n        assert!(\n            !is_auto,\n            \"Table '{table_with_id_not_auto}' should NOT be auto-increment, but check returned true\"\n        );\n\n        let table_with_serial_id = \"test_table_serial_id_auto\";\n        db.execute(Statement::from_string(\n            backend,\n            format!(\"CREATE TABLE {table_with_serial_id} (id SERIAL PRIMARY KEY, name TEXT);\"),\n        ))\n        .await\n        .expect(\"Failed to create table with serial id\");\n\n        let is_auto = is_auto_increment(&db, &backend, table_with_serial_id)\n            .await\n            .expect(\"Failed to check auto-increment\");\n        assert!(\n            is_auto,\n            \"Table '{table_with_serial_id}' SHOULD be auto-increment, but check returned false\"\n        );\n    }\n\n    #[tokio::test]\n    async fn test_postgres_reset_autoincrement() {\n        // Setup PostgreSQL container\n        let (pg_url, _container) = setup_postgres_container().await;\n        let mut config = crate::tests_cfg::config::get_database_config();\n        config.uri = pg_url;\n        let db = connect(&config)\n            .await\n            .expect(\"Failed to connect to PostgreSQL\");\n        let backend = db.get_database_backend();\n\n        // Create test table with SERIAL id\n        let table_name = \"test_reset_sequence\";\n        db.execute(Statement::from_string(\n            backend,\n            format!(\"CREATE TABLE {table_name} (id SERIAL PRIMARY KEY, name TEXT);\"),\n        ))\n        .await\n        .expect(\"Failed to create test table\");\n\n        // Insert multiple rows in a single query\n        db.execute(Statement::from_string(\n            backend,\n            format!(\"INSERT INTO {table_name} (name) VALUES ('one'), ('two'), ('three');\"),\n        ))\n        .await\n        .expect(\"Failed to insert test data\");\n\n        // Delete all rows\n        db.execute(Statement::from_string(\n            backend,\n            format!(\"DELETE FROM {table_name};\"),\n        ))\n        .await\n        .expect(\"Failed to delete rows\");\n\n        // Insert a new row and check ID (should be 4, continuing the sequence)\n        let result = db\n            .query_one(Statement::from_string(\n                backend,\n                format!(\"INSERT INTO {table_name} (name) VALUES ('test') RETURNING id;\"),\n            ))\n            .await\n            .expect(\"Failed to insert row\")\n            .expect(\"No row returned\");\n\n        let id = result.try_get::<i32>(\"\", \"id\").expect(\"Failed to get ID\");\n        assert_eq!(\n            id, 4,\n            \"ID should be 4 after insert (sequence was not reset)\"\n        );\n\n        // Delete all rows again\n        db.execute(Statement::from_string(\n            backend,\n            format!(\"DELETE FROM {table_name};\"),\n        ))\n        .await\n        .expect(\"Failed to delete rows\");\n\n        // Reset auto-increment sequence\n        reset_autoincrement(backend, table_name, &db)\n            .await\n            .expect(\"Failed to reset sequence\");\n\n        // Insert a new row and check ID (should be 1 after reset)\n        let result = db\n            .query_one(Statement::from_string(\n                backend,\n                format!(\"INSERT INTO {table_name} (name) VALUES ('reset') RETURNING id;\"),\n            ))\n            .await\n            .expect(\"Failed to insert row\")\n            .expect(\"No row returned\");\n\n        let id = result.try_get::<i32>(\"\", \"id\").expect(\"Failed to get ID\");\n        assert_eq!(id, 1, \"ID should be 1 after sequence reset\");\n    }\n\n    #[tokio::test]\n    async fn test_sqlite_reset_autoincrement() {\n        // Setup SQLite database\n        let (config, _tree_fs) = crate::tests_cfg::config::get_sqlite_test_config(\"test_reset\");\n\n        let db = connect(&config).await.expect(\"Failed to connect to SQLite\");\n        let backend = db.get_database_backend();\n\n        // Create test table with auto-incrementing id\n        let table_name = \"test_reset_sequence\";\n        db.execute(Statement::from_string(\n            backend,\n            format!(\"CREATE TABLE {table_name} (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT);\"),\n        ))\n        .await\n        .expect(\"Failed to create test table\");\n\n        // Insert multiple rows in a single query\n        db.execute(Statement::from_string(\n            backend,\n            format!(\"INSERT INTO {table_name} (name) VALUES ('one'), ('two'), ('three');\"),\n        ))\n        .await\n        .expect(\"Failed to insert test data\");\n\n        // Delete all rows\n        db.execute(Statement::from_string(\n            backend,\n            format!(\"DELETE FROM {table_name};\"),\n        ))\n        .await\n        .expect(\"Failed to delete rows\");\n\n        // Insert a new row and check ID (should be 4, continuing the sequence)\n        let result = db\n            .query_one(Statement::from_string(\n                backend,\n                format!(\"INSERT INTO {table_name} (name) VALUES ('test') RETURNING id;\"),\n            ))\n            .await\n            .expect(\"Failed to insert row\")\n            .expect(\"No row returned\");\n\n        let id = result.try_get::<i32>(\"\", \"id\").expect(\"Failed to get ID\");\n        assert_eq!(\n            id, 4,\n            \"ID should be 4 after insert (sequence was not reset)\"\n        );\n\n        // Delete all rows again\n        db.execute(Statement::from_string(\n            backend,\n            format!(\"DELETE FROM {table_name};\"),\n        ))\n        .await\n        .expect(\"Failed to delete rows\");\n\n        // Reset auto-increment sequence\n        reset_autoincrement(backend, table_name, &db)\n            .await\n            .expect(\"Failed to reset sequence\");\n\n        // Insert a new row and check ID (should be 1 after reset)\n        let result = db\n            .query_one(Statement::from_string(\n                backend,\n                format!(\"INSERT INTO {table_name} (name) VALUES ('reset') RETURNING id;\"),\n            ))\n            .await\n            .expect(\"Failed to insert row\")\n            .expect(\"No row returned\");\n\n        let id = result.try_get::<i32>(\"\", \"id\").expect(\"Failed to get ID\");\n        assert_eq!(id, 1, \"ID should be 1 after sequence reset\");\n    }\n\n    // Minimal SeaORM entity for the dump_types test table to exercise the\n    // full dump -> seed -> select roundtrip using the same seed() logic as\n    // the CLI.\n    mod dump_types_entity {\n        use sea_orm::entity::prelude::*;\n\n        #[derive(\n            Clone, Debug, PartialEq, DeriveEntityModel, serde::Serialize, serde::Deserialize,\n        )]\n        #[sea_orm(table_name = \"dump_types\")]\n        pub struct Model {\n            #[sea_orm(primary_key)]\n            pub id: i32,\n            pub active: bool,\n            pub counter: i32,\n            pub rating: f64,\n            pub name: String,\n            pub created_at: String,\n            pub uuid_col: String,\n            pub json_col: Json,\n            pub opt_text: Option<String>,\n            pub opt_int: Option<i32>,\n            pub opt_bool: Option<bool>,\n            pub array_col: Json,\n        }\n\n        #[derive(Copy, Clone, Debug, EnumIter)]\n        pub enum Relation {}\n\n        impl RelationTrait for Relation {\n            fn def(&self) -> RelationDef {\n                panic!(\"no relations for dump_types_entity::Relation\")\n            }\n        }\n\n        impl ActiveModelBehavior for ActiveModel {}\n    }\n\n    #[tokio::test]\n    async fn sqlite_dump_tables_roundtrip() {\n        use crate::tests_cfg::config::get_sqlite_test_config;\n        use dump_types_entity::ActiveModel as DumpTypesActiveModel;\n        use dump_types_entity::Entity as DumpTypesEntity;\n        use insta::assert_snapshot;\n        use sea_orm::QueryOrder;\n\n        // Arrange: create a temporary SQLite database with a table that has\n        // a variety of column types we support in loco.\n        let (config, tree_fs) = get_sqlite_test_config(\"dump_types\");\n        let db = connect(&config)\n            .await\n            .expect(\"Failed to connect to SQLite test database\");\n        let backend = db.get_database_backend();\n        assert_eq!(backend, DatabaseBackend::Sqlite);\n\n        let table_name = \"dump_types\";\n\n        // Create table with representative types:\n        // - integer PK\n        // - boolean (required + optional)\n        // - integer (required + optional)\n        // - real (required)\n        // - text (required + optional)\n        // - created_at (text datetime-like)\n        // - uuid-like text\n        // - json-like text (object)\n        // - array-like text (JSON array)\n        db.execute(Statement::from_string(\n            backend,\n            format!(\n                \"CREATE TABLE {table_name} (\n                    id INTEGER PRIMARY KEY AUTOINCREMENT,\n                    active BOOLEAN NOT NULL,\n                    counter INTEGER NOT NULL,\n                    rating REAL NOT NULL,\n                    name TEXT NOT NULL,\n                    created_at TEXT NOT NULL,\n                    uuid_col TEXT NOT NULL,\n                    json_col TEXT NOT NULL,\n                    opt_text TEXT,\n                    opt_int INTEGER,\n                    opt_bool BOOLEAN,\n                    array_col TEXT NOT NULL\n                );\"\n            ),\n        ))\n        .await\n        .expect(\"Failed to create dump_types table\");\n\n        // Insert a couple of rows. For SQLite, BOOLEAN is typically stored as 0/1\n        // and NULL is allowed for the optional columns.\n        db.execute(Statement::from_string(\n            backend,\n            format!(\n                \"INSERT INTO {table_name} (active, counter, rating, name, created_at, uuid_col, json_col, opt_text, opt_int, opt_bool, array_col) VALUES\n                    (0, 10, 3.14, 'foo', '2025-01-01T10:00:00Z', '11111111-1111-1111-1111-111111111111', '{{\\\"k\\\": \\\"v\\\"}}', NULL, NULL, NULL, '[1, 2, 3]'),\n                    (1, 20, 6.28, 'bar', '2025-01-02T11:30:00Z', '22222222-2222-2222-2222-222222222222', '{{\\\"n\\\": 42}}', 'opt', 99, 1, '[\\\"a\\\", \\\"b\\\"]');\"\n            ),\n        ))\n        .await\n        .expect(\"Failed to insert test data into dump_types table\");\n\n        // Act: dump the table into a YAML file in the temp tree_fs folder.\n        let dump_dir = tree_fs.root.join(\"dump\");\n        std::fs::create_dir_all(&dump_dir).expect(\"Failed to create dump directory\");\n\n        dump_tables(&db, dump_dir.as_path(), Some(vec![table_name.to_string()]))\n            .await\n            .expect(\"dump_tables failed\");\n\n        let yaml_path = dump_dir.join(format!(\"{table_name}.yaml\"));\n        let yaml_content = std::fs::read_to_string(&yaml_path)\n            .unwrap_or_else(|e| panic!(\"Failed to read YAML dump at {yaml_path:?}: {e}\"));\n\n        // Snapshot the actual YAML file contents, exactly as written by dump_tables.\n        assert_snapshot!(\"dump_tables_sqlite_all_types\", yaml_content);\n\n        // Round-trip validation:\n        // 1) Truncate the table\n        db.execute(Statement::from_string(\n            backend,\n            format!(\"DELETE FROM {table_name};\"),\n        ))\n        .await\n        .expect(\"Failed to truncate dump_types table\");\n\n        // 2) Seed it back from the dumped YAML using the same seed() logic\n        seed::<DumpTypesActiveModel>(\n            &db,\n            yaml_path.to_str().expect(\"YAML path should be valid UTF-8\"),\n        )\n        .await\n        .expect(\"seed from dumped YAML failed\");\n\n        // 3) Select rows back in a deterministic order and snapshot their JSON form.\n        let models = DumpTypesEntity::find()\n            .order_by_asc(dump_types_entity::Column::Id)\n            .all(&db)\n            .await\n            .expect(\"select after seed failed\");\n\n        let roundtripped: Vec<serde_json::Value> = models\n            .into_iter()\n            .map(|m| serde_json::to_value(m).expect(\"serialize model\"))\n            .collect();\n\n        assert_snapshot!(\n            \"dump_tables_sqlite_all_types_roundtrip\",\n            serde_json::to_string_pretty(&roundtripped).unwrap()\n        );\n    }\n\n    #[test]\n    fn test_entity_cmd_new() {\n        let cmd = EntityCmd::new(&get_database_config());\n\n        let expected = \"generate entity --database-url sqlite::memory: --ignore-tables \\\n            seaql_migrations,pg_loco_queue,sqlt_loco_queue,sqlt_loco_queue_lock --output-dir \\\n            src/models/_entities --with-copy-enums --with-serde both\";\n        assert_eq!(cmd.command().join(\" \"), expected);\n    }\n\n    #[test]\n    fn test_entity_cmd_merge_with_config() {\n        let config_str = r#\"\nmax-connections = \"1\"\nignore-tables = \"table1,table2\"\nwith-serde = \"none\"\nmodel-extra-derives = \"ts_rs::Ts\"\n\"#;\n        let config: toml::Table = toml::from_str(config_str).unwrap();\n\n        let cmd = EntityCmd::merge_with_config(&get_database_config(), &config);\n\n        let expected = \"generate entity --database-url sqlite::memory: --ignore-tables \\\n            seaql_migrations,pg_loco_queue,sqlt_loco_queue,sqlt_loco_queue_lock,table1,table2 \\\n            --max-connections 1 --model-extra-derives ts_rs::Ts --output-dir src/models/_entities \\\n            --with-copy-enums --with-serde none\";\n        assert_eq!(cmd.command().join(\" \"), expected);\n    }\n}\n"
  },
  {
    "path": "src/depcheck.rs",
    "content": "use std::collections::HashMap;\n\nuse semver::{Version, VersionReq};\nuse thiserror::Error;\n\nuse crate::cargo_config::CargoConfig;\n\n#[derive(Debug, PartialEq, Eq, Ord, PartialOrd)]\npub enum VersionStatus {\n    NotFound,\n    Invalid {\n        version: String,\n        min_version: String,\n    },\n    Ok(String),\n}\n\n#[derive(Debug, PartialEq, Eq, Ord, PartialOrd)]\npub struct CrateStatus {\n    pub crate_name: String,\n    pub status: VersionStatus,\n}\n\n#[derive(Error, Debug)]\npub enum VersionCheckError {\n    #[error(\"Failed to parse Cargo.lock: {0}\")]\n    ParseError(#[from] toml::de::Error),\n\n    #[error(\"Error with crate {crate_name}: {msg}\")]\n    CrateError { crate_name: String, msg: String },\n}\n\npub type Result<T> = std::result::Result<T, VersionCheckError>;\n\npub fn check_crate_versions(\n    lock_file: &CargoConfig,\n    min_versions: HashMap<&str, &str>,\n) -> Result<Vec<CrateStatus>> {\n    let packages = lock_file\n        .get_package_array()\n        .map_err(|e| VersionCheckError::ParseError(serde::de::Error::custom(e.to_string())))?;\n\n    let mut results = Vec::new();\n\n    for (crate_name, min_version) in min_versions {\n        let min_version_req =\n            VersionReq::parse(min_version).map_err(|_| VersionCheckError::CrateError {\n                crate_name: crate_name.to_string(),\n                msg: format!(\"Invalid minimum version format: {min_version}\"),\n            })?;\n\n        let mut found = false;\n        for package in packages {\n            if let Some(name) = package.get(\"name\").and_then(|v| v.as_str()) {\n                if name == crate_name {\n                    found = true;\n                    let version_str =\n                        package\n                            .get(\"version\")\n                            .and_then(|v| v.as_str())\n                            .ok_or_else(|| VersionCheckError::CrateError {\n                                crate_name: crate_name.to_string(),\n                                msg: \"Invalid version format in Cargo.lock\".to_string(),\n                            })?;\n\n                    let version =\n                        Version::parse(version_str).map_err(|_| VersionCheckError::CrateError {\n                            crate_name: crate_name.to_string(),\n                            msg: format!(\"Invalid version format in Cargo.lock: {version_str}\"),\n                        })?;\n\n                    let status = if min_version_req.matches(&version) {\n                        VersionStatus::Ok(version.to_string())\n                    } else {\n                        VersionStatus::Invalid {\n                            version: version.to_string(),\n                            min_version: min_version.to_string(),\n                        }\n                    };\n                    results.push(CrateStatus {\n                        crate_name: crate_name.to_string(),\n                        status,\n                    });\n                    break;\n                }\n            }\n        }\n\n        if !found {\n            results.push(CrateStatus {\n                crate_name: crate_name.to_string(),\n                status: VersionStatus::NotFound,\n            });\n        }\n    }\n\n    Ok(results)\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use tree_fs::{Tree, TreeBuilder};\n\n    fn setup_test_dir(cargo_lock_content: &str) -> Tree {\n        TreeBuilder::default()\n            .add_file(\"Cargo.lock\", cargo_lock_content)\n            .create()\n            .expect(\"Failed to create test directory structure\")\n    }\n\n    #[test]\n    fn test_multiple_crates_mixed_results() {\n        let cargo_lock_content = r#\"\n            [[package]]\n            name = \"serde\"\n            version = \"1.0.130\"\n\n            [[package]]\n            name = \"tokio\"\n            version = \"0.3.0\"\n\n            [[package]]\n            name = \"rand\"\n            version = \"0.8.4\"\n        \"#;\n\n        let tree = setup_test_dir(cargo_lock_content);\n        let config = CargoConfig::from_path(tree.root.join(\"Cargo.lock\")).unwrap();\n\n        let mut min_versions = HashMap::new();\n        min_versions.insert(\"serde\", \"1.0.130\");\n        min_versions.insert(\"tokio\", \"1.0\");\n        min_versions.insert(\"rand\", \"0.8.0\");\n\n        let mut result = check_crate_versions(&config, min_versions).unwrap();\n        result.sort();\n        assert_eq!(\n            result,\n            vec![\n                CrateStatus {\n                    crate_name: \"rand\".to_string(),\n                    status: VersionStatus::Ok(\"0.8.4\".to_string())\n                },\n                CrateStatus {\n                    crate_name: \"serde\".to_string(),\n                    status: VersionStatus::Ok(\"1.0.130\".to_string())\n                },\n                CrateStatus {\n                    crate_name: \"tokio\".to_string(),\n                    status: VersionStatus::Invalid {\n                        version: \"0.3.0\".to_string(),\n                        min_version: \"1.0\".to_string()\n                    }\n                }\n            ]\n        );\n    }\n\n    #[test]\n    fn test_invalid_version_format_in_cargo_lock() {\n        let cargo_lock_content = r#\"\n            [[package]]\n            name = \"serde\"\n            version = \"1.0.x\"\n        \"#;\n\n        let tree = setup_test_dir(cargo_lock_content);\n        let config = CargoConfig::from_path(tree.root.join(\"Cargo.lock\")).unwrap();\n\n        let mut min_versions = HashMap::new();\n        min_versions.insert(\"serde\", \"1.0.0\");\n\n        let result = check_crate_versions(&config, min_versions);\n        assert!(matches!(\n            result,\n            Err(VersionCheckError::CrateError { crate_name, msg }) if crate_name == \"serde\" && msg.contains(\"Invalid version format\")\n        ));\n    }\n\n    #[test]\n    fn test_no_package_section_in_cargo_lock() {\n        let cargo_lock_content = r\"\n            # No packages listed in this Cargo.lock\n        \";\n\n        let tree = setup_test_dir(cargo_lock_content);\n        let config = CargoConfig::from_path(tree.root.join(\"Cargo.lock\")).unwrap();\n\n        let mut min_versions = HashMap::new();\n        min_versions.insert(\"serde\", \"1.0.130\");\n\n        let result = check_crate_versions(&config, min_versions);\n        assert!(matches!(result, Err(VersionCheckError::ParseError(_))));\n    }\n\n    #[test]\n    fn test_exact_version_match_for_minimum_requirement() {\n        let cargo_lock_content = r#\"\n            [[package]]\n            name = \"serde\"\n            version = \"1.0.130\"\n        \"#;\n\n        let tree = setup_test_dir(cargo_lock_content);\n        let config = CargoConfig::from_path(tree.root.join(\"Cargo.lock\")).unwrap();\n\n        let mut min_versions = HashMap::new();\n        min_versions.insert(\"serde\", \"1.0.130\");\n\n        let mut result = check_crate_versions(&config, min_versions).unwrap();\n        result.sort();\n        assert_eq!(\n            result,\n            vec![CrateStatus {\n                crate_name: \"serde\".to_string(),\n                status: VersionStatus::Ok(\"1.0.130\".to_string()),\n            }]\n        );\n    }\n\n    #[test]\n    fn test_no_crates_in_min_versions_map() {\n        let cargo_lock_content = r#\"\n            [[package]]\n            name = \"serde\"\n            version = \"1.0.130\"\n        \"#;\n\n        let tree = setup_test_dir(cargo_lock_content);\n        let config = CargoConfig::from_path(tree.root.join(\"Cargo.lock\")).unwrap();\n\n        let min_versions = HashMap::new(); // Empty map\n\n        let result = check_crate_versions(&config, min_versions).unwrap();\n        assert!(result.is_empty());\n    }\n}\n"
  },
  {
    "path": "src/doctor.rs",
    "content": "//! Doctor module for health checks and diagnostics.\n//!\n//! This module provides health checks for various components of a Loco application.\n//!\n//! # Initializer Health Checks\n//!\n//! Initializers can now provide their own health checks by implementing the `check` method\n//! on the `Initializer` trait. This allows each initializer to validate its configuration\n//! and test its connections during the doctor command.\n//!\n//! When you run `cargo loco doctor`, any initializers that implement the `check` method\n//! will have their health checks executed and displayed in the output.\n\nuse colored::Colorize;\nuse regex::Regex;\nuse semver::Version;\nuse std::fmt::Write;\nuse std::{\n    collections::{BTreeMap, HashMap},\n    process::Command,\n    sync::OnceLock,\n};\n\nuse crate::{\n    bgworker,\n    cargo_config::CargoConfig,\n    config::{self, Config},\n    depcheck, Error, Result,\n};\n\nconst SEAORM_INSTALLED: &str = \"SeaORM CLI is installed\";\nconst SEAORM_NOT_INSTALLED: &str = \"SeaORM CLI was not found\";\nconst SEAORM_NOT_FIX: &str = r\"To fix, run:\n      $ cargo install sea-orm-cli\";\nconst QUEUE_CONN_OK: &str = \"queue connection: success\";\nconst QUEUE_CONN_FAILED: &str = \"queue connection: failed\";\nconst QUEUE_NOT_CONFIGURED: &str = \"queue not configured?\";\n\n// versions health\nconst MIN_SEAORMCLI_VER: &str = \"1.1.0\";\nstatic MIN_DEP_VERSIONS: OnceLock<HashMap<&'static str, &'static str>> = OnceLock::new();\nstatic RE_CRATE_VERSION: OnceLock<Regex> = OnceLock::new();\n\nfn get_re_crate_version() -> &'static Regex {\n    RE_CRATE_VERSION.get_or_init(|| Regex::new(r#\"(?m)^[^\"]*\"([^\"]+)\"\"#).unwrap())\n}\n\nfn get_min_dep_versions() -> &'static HashMap<&'static str, &'static str> {\n    MIN_DEP_VERSIONS.get_or_init(|| {\n        let mut min_vers = HashMap::new();\n\n        min_vers.insert(\"tokio\", \"1.33.0\");\n        min_vers.insert(\"sea-orm\", \"1.1.0\");\n        min_vers.insert(\"validator\", \"0.20.0\");\n        min_vers.insert(\"axum\", \"0.8.1\");\n\n        min_vers\n    })\n}\n\n/// Check latest crate version in crates.io\n///\n/// # Errors\n///\n/// This function will return an error if it fails\npub fn check_cratesio_version(crate_name: &str, current_version: &str) -> Result<Option<String>> {\n    // Use cargo search to get the latest version\n    let output = Command::new(\"cargo\")\n        .args([\"search\", crate_name, \"--limit\", \"1\"])\n        .output()\n        .map_err(|e| Error::Message(format!(\"Failed to run cargo search: {e}\")))?;\n\n    let output_str = String::from_utf8(output.stdout)\n        .map_err(|e| Error::Message(format!(\"Invalid output from cargo search: {e}\")))?;\n\n    // Parse the version from cargo search output\n    // Output format is: crate_name = \"version\"\n    let latest_version = get_re_crate_version()\n        .captures(&output_str)\n        .and_then(|cap| cap.get(1))\n        .map(|m| m.as_str())\n        .ok_or_else(|| {\n            Error::Message(\"Could not find version in cargo search output\".to_string())\n        })?;\n\n    // Parse versions for comparison\n    let current = Version::parse(current_version)\n        .map_err(|e| Error::Message(format!(\"Invalid current version: {e}\")))?;\n    let latest = Version::parse(latest_version)\n        .map_err(|e| Error::Message(format!(\"Invalid latest version: {e}\")))?;\n\n    // Compare versions\n    if latest > current {\n        Ok(Some(latest_version.to_string()))\n    } else {\n        Ok(None)\n    }\n}\n\n/// Represents different resources that can be checked.\n#[derive(PartialOrd, PartialEq, Eq, Ord, Debug)]\npub enum Resource {\n    SeaOrmCLI,\n    Database,\n    Queue,\n    Deps,\n    PublishedLocoVersion,\n    Initializer(String),\n}\n\n/// Represents the status of a resource check.\n#[derive(Debug, PartialEq, Eq)]\npub enum CheckStatus {\n    Ok,\n    NotOk,\n    NotConfigure,\n}\n\n/// Represents the result of a resource check.\n#[derive(Debug)]\npub struct Check {\n    /// The status of the check.\n    pub status: CheckStatus,\n    /// A message describing the result of the check.\n    pub message: String,\n    /// Additional information or instructions related to the check.\n    pub description: Option<String>,\n}\n\nimpl Check {\n    #[must_use]\n    pub fn valid(&self) -> bool {\n        self.status != CheckStatus::NotOk\n    }\n    /// Convert to a Result type\n    ///\n    /// # Errors\n    ///\n    /// This function will return an error if Check fails\n    pub fn to_result(&self) -> Result<()> {\n        if self.valid() {\n            Ok(())\n        } else {\n            Err(Error::Message(format!(\n                \"{} {}\",\n                self.message,\n                self.description.clone().unwrap_or_default()\n            )))\n        }\n    }\n}\n\nimpl std::fmt::Display for Check {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        let icon = match self.status {\n            CheckStatus::Ok => \"✅\",\n            CheckStatus::NotOk => \"❌\",\n            CheckStatus::NotConfigure => \"⚠️\",\n        };\n\n        write!(\n            f,\n            \"{} {}{}\",\n            icon,\n            self.message,\n            self.description\n                .as_ref()\n                .map(|d| format!(\"\\n   {d}\"))\n                .unwrap_or_default()\n        )\n    }\n}\n\n/// Runs checks for all configured resources.\n/// # Errors\n/// Error when one of the checks fail\npub async fn run_all<H: crate::app::Hooks>(\n    app_context: &crate::app::AppContext,\n    production: bool,\n) -> Result<BTreeMap<Resource, Check>> {\n    let mut checks = BTreeMap::from(\n        #[cfg(feature = \"with-db\")]\n        [(\n            Resource::Database,\n            check_db(&app_context.config.database).await,\n        )],\n        #[cfg(not(feature = \"with-db\"))]\n        [],\n    );\n\n    if app_context.config.workers.mode == config::WorkerMode::BackgroundQueue {\n        checks.insert(Resource::Queue, check_queue(&app_context.config).await);\n    }\n\n    // Add initializer checks\n    if let Ok(initializers) = H::initializers(app_context).await {\n        for initializer in initializers {\n            if let Ok(Some(mut check)) = initializer.check(app_context).await {\n                // Format the message to include \"Initializer [name]: \" prefix\n                check.message = format!(\"Initializer {}: {}\", initializer.name(), check.message);\n                checks.insert(Resource::Initializer(initializer.name()), check);\n            }\n        }\n    }\n\n    if !production {\n        checks.insert(Resource::Deps, check_deps()?);\n        checks.insert(Resource::SeaOrmCLI, check_seaorm_cli()?);\n        checks.insert(\n            Resource::PublishedLocoVersion,\n            check_published_loco_version()?,\n        );\n    }\n\n    Ok(checks)\n}\n\n/// Checks \"blessed\" / major dependencies in a Loco app Cargo.toml, and\n/// recommend to update.\n/// Only if a dep exists, we check it against a min version\n/// # Errors\n/// Returns error if fails\npub fn check_deps() -> Result<Check> {\n    let cargolock = CargoConfig::lock_from_current_dir()?;\n\n    let crate_statuses =\n        depcheck::check_crate_versions(&cargolock, get_min_dep_versions().clone())?;\n    let mut report = String::new();\n    let _ = write!(report, \"Dependencies\");\n    let mut all_ok = true;\n\n    for status in &crate_statuses {\n        if let depcheck::VersionStatus::Invalid {\n            version,\n            min_version,\n        } = &status.status\n        {\n            let _ = writeln!(\n                report,\n                \"     {}: version {} does not meet minimum version {}\",\n                status.crate_name.yellow(),\n                version.red(),\n                min_version.green()\n            );\n\n            all_ok = false;\n        }\n    }\n    Ok(Check {\n        status: if all_ok {\n            CheckStatus::Ok\n        } else {\n            CheckStatus::NotOk\n        },\n        message: report,\n        description: None,\n    })\n}\n\n/// Checks the database connection.\n#[cfg(feature = \"with-db\")]\npub async fn check_db(config: &crate::config::Database) -> Check {\n    let db_connection_failed = \"DB connection: fails\";\n    let db_connection_success = \"DB connection: success\";\n    match crate::db::connect(config).await {\n        Ok(conn) => match conn.ping().await {\n            Ok(()) => match crate::db::verify_access(&conn).await {\n                Ok(()) => Check {\n                    status: CheckStatus::Ok,\n                    message: db_connection_success.to_string(),\n                    description: None,\n                },\n                Err(err) => Check {\n                    status: CheckStatus::NotOk,\n                    message: db_connection_failed.to_string(),\n                    description: Some(err.to_string()),\n                },\n            },\n            Err(err) => Check {\n                status: CheckStatus::NotOk,\n                message: db_connection_failed.to_string(),\n                description: Some(err.to_string()),\n            },\n        },\n        Err(err) => Check {\n            status: CheckStatus::NotOk,\n            message: db_connection_failed.to_string(),\n            description: Some(err.to_string()),\n        },\n    }\n}\n\n/// Checks the Redis connection.\npub async fn check_queue(config: &Config) -> Check {\n    if let Ok(Some(queue)) = bgworker::create_queue_provider(config).await {\n        match queue.ping().await {\n            Ok(()) => Check {\n                status: CheckStatus::Ok,\n                message: format!(\"{}: {}\", queue.describe(), QUEUE_CONN_OK),\n                description: None,\n            },\n            Err(err) => Check {\n                status: CheckStatus::NotOk,\n                message: format!(\"{}: {}\", queue.describe(), QUEUE_CONN_FAILED),\n                description: Some(err.to_string()),\n            },\n        }\n    } else {\n        Check {\n            status: CheckStatus::NotConfigure,\n            message: QUEUE_NOT_CONFIGURED.to_string(),\n            description: None,\n        }\n    }\n}\n\n/// Checks the presence and version of `SeaORM` CLI.\n/// # Panics\n/// On illegal regex\n/// # Errors\n/// Fails when cannot check version\npub fn check_seaorm_cli() -> Result<Check> {\n    match Command::new(\"sea-orm-cli\").arg(\"--version\").output() {\n        Ok(out) => {\n            let input = String::from_utf8_lossy(&out.stdout);\n            // Extract the version from the input string\n            let re = Regex::new(r\"(\\d+\\.\\d+\\.\\d+)\").unwrap();\n\n            let version_str = re\n                .captures(&input)\n                .and_then(|caps| caps.get(0))\n                .map(|m| m.as_str())\n                .ok_or(\"SeaORM CLI version not found\")\n                .map_err(Box::from)?;\n\n            // Parse the extracted version using semver\n            let version = Version::parse(version_str).map_err(Box::from)?;\n\n            // Parse the minimum version for comparison\n            let min_version = Version::parse(MIN_SEAORMCLI_VER).map_err(Box::from)?;\n\n            // Compare the extracted version with the minimum version\n            if version >= min_version {\n                Ok(Check {\n                    status: CheckStatus::Ok,\n                    message: SEAORM_INSTALLED.to_string(),\n                    description: None,\n                })\n            } else {\n                Ok(Check {\n                    status: CheckStatus::NotOk,\n                    message: format!(\n                        \"SeaORM CLI minimal version is `{min_version}` (you have `{version}`). \\\n                         Run `cargo install sea-orm-cli` to update.\"\n                    ),\n                    description: Some(SEAORM_NOT_FIX.to_string()),\n                })\n            }\n        }\n        Err(_) => Ok(Check {\n            status: CheckStatus::NotOk,\n            message: SEAORM_NOT_INSTALLED.to_string(),\n            description: Some(SEAORM_NOT_FIX.to_string()),\n        }),\n    }\n}\n\n/// Check for the latest Loco version\n///\n/// # Errors\n///\n/// This function will return an error if it fails\npub fn check_published_loco_version() -> Result<Check> {\n    let compiled_version = env!(\"CARGO_PKG_VERSION\");\n    match check_cratesio_version(\"loco-rs\", compiled_version) {\n        Ok(Some(v)) => Ok(Check {\n            status: CheckStatus::NotOk,\n            message: format!(\"Loco version: `{compiled_version}`, latest version: `{v}`\"),\n            description: Some(\"It is recommended to upgrade your main Loco version.\".to_string()),\n        }),\n        Ok(None) => Ok(Check {\n            status: CheckStatus::Ok,\n            message: \"Loco version: latest\".to_string(),\n            description: None,\n        }),\n        Err(e) => Ok(Check {\n            status: CheckStatus::NotOk,\n            message: format!(\"Checking Loco version failed: {e}\"),\n            description: None,\n        }),\n    }\n}\n"
  },
  {
    "path": "src/env_vars.rs",
    "content": "//! This module contains utility functions and constants for working with\n//! environment variables in the application. It centralizes the logic for\n//! fetching environment variables, ensuring that keys are easily accessible\n//! from a single location in the codebase.\n\n#[cfg(feature = \"with-db\")]\n/// The key for `PostgreSQL` database options environment variable.\npub const POSTGRES_DB_OPTIONS: &str = \"LOCO_POSTGRES_DB_OPTIONS\";\n/// The key for the application's environment (e.g., development, production).\npub const LOCO_ENV: &str = \"LOCO_ENV\";\n/// The key for the application's environment (e.g., development, production).\npub const RAILS_ENV: &str = \"RAILS_ENV\";\n/// The key for the application's environment (e.g., development, production).\npub const NODE_ENV: &str = \"NODE_ENV\";\n// The key for the application environment configuration\npub const CONFIG_FOLDER: &str = \"LOCO_CONFIG_FOLDER\";\n// The key for the scheduler configuration\npub const SCHEDULER_CONFIG: &str = \"SCHEDULER_CONFIG\";\n/// The key for the data folder path\npub const LOCO_DATA_FOLDER_ENV: &str = \"LOCO_DATA\";\n\n/// Fetches the value of the given environment variable.\npub fn get(key: &str) -> Result<String, std::env::VarError> {\n    std::env::var(key)\n}\n\n#[allow(dead_code)]\n/// Retrieves the value of the given environment variable, or returns a default value if the variable is not set.\npub fn get_or_default(key: &str, default: &str) -> String {\n    get(key).unwrap_or_else(|_| default.to_string())\n}\n"
  },
  {
    "path": "src/environment.rs",
    "content": "//! Defines the application environment.\n//! By given the environment you can also load the application configuration\n//!\n//! # Example:\n//!\n//! ```rust\n//! use std::str::FromStr;\n//! use loco_rs::environment::Environment;\n//!\n//! pub fn load(environment: &str) {\n//!  let environment = Environment::from_str(environment).unwrap_or(Environment::Any(environment.to_string()));\n//!  let config = environment.load().expect(\"failed to load environment\");\n//! }\n//! ```\nuse super::config::Config;\nuse crate::{env_vars, Result};\nuse serde::{Deserialize, Serialize};\nuse serde_variant::to_variant_name;\nuse std::{path::Path, str::FromStr};\n\npub const DEFAULT_ENVIRONMENT: &str = \"development\";\npub const LOCO_ENV: &str = \"LOCO_ENV\";\npub const RAILS_ENV: &str = \"RAILS_ENV\";\npub const NODE_ENV: &str = \"NODE_ENV\";\n\nimpl From<String> for Environment {\n    fn from(env: String) -> Self {\n        Self::from_str(&env).unwrap_or(Self::Any(env))\n    }\n}\n\n#[must_use]\npub fn resolve_from_env() -> String {\n    env_vars::get(env_vars::LOCO_ENV)\n        .or_else(|_| env_vars::get(env_vars::RAILS_ENV))\n        .or_else(|_| env_vars::get(env_vars::NODE_ENV))\n        .unwrap_or_else(|_| DEFAULT_ENVIRONMENT.to_string())\n}\n\n/// Application environment\n#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]\npub enum Environment {\n    #[serde(rename = \"production\")]\n    Production,\n    #[serde(rename = \"development\")]\n    Development,\n    #[serde(rename = \"test\")]\n    Test,\n    Any(String),\n}\n\nimpl Environment {\n    /// Load environment variables from local configuration\n    ///\n    /// # Errors\n    ///\n    /// Returns error if an error occurs during loading\n    /// configuration file an parse into [`Config`] struct.\n    pub fn load(&self) -> Result<Config> {\n        env_vars::get(env_vars::CONFIG_FOLDER).map_or_else(\n            |_| Config::new(self),\n            |config_folder| self.load_from_folder(Path::new(&config_folder)),\n        )\n    }\n\n    /// Load environment variables from the given config path\n    ///\n    /// # Errors\n    ///\n    /// Returns error if an error occurs during loading\n    /// configuration file an parse into [`Config`] struct.\n    pub fn load_from_folder(&self, path: &Path) -> Result<Config> {\n        Config::from_folder(self, path)\n    }\n}\n\nimpl std::fmt::Display for Environment {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        match self {\n            Self::Any(s) => s.fmt(f),\n            _ => to_variant_name(self).expect(\"only enum supported\").fmt(f),\n        }\n    }\n}\n\nimpl FromStr for Environment {\n    type Err = &'static str;\n\n    fn from_str(input: &str) -> std::result::Result<Self, Self::Err> {\n        match input {\n            \"production\" => Ok(Self::Production),\n            \"development\" => Ok(Self::Development),\n            \"test\" => Ok(Self::Test),\n            s => Ok(Self::Any(s.to_string())),\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use std::env;\n\n    use super::*;\n    #[test]\n    fn test_resolve_env() {\n        let original = env::var(\"LOCO_ENV\");\n\n        env::remove_var(LOCO_ENV);\n        env::remove_var(RAILS_ENV);\n        env::remove_var(NODE_ENV);\n        assert_eq!(resolve_from_env(), \"development\");\n        env::set_var(\"LOCO_ENV\", \"custom\");\n        assert_eq!(resolve_from_env(), \"custom\");\n\n        if let Ok(v) = original {\n            env::set_var(LOCO_ENV, v);\n        }\n    }\n\n    #[test]\n    fn test_display() {\n        assert_eq!(\"production\", Environment::Production.to_string());\n        assert_eq!(\"custom\", Environment::Any(\"custom\".to_string()).to_string());\n    }\n\n    #[test]\n    fn test_into() {\n        let e: Environment = \"production\".to_string().into();\n        assert_eq!(e, Environment::Production);\n        let e: Environment = \"custom\".to_string().into();\n        assert_eq!(e, Environment::Any(\"custom\".to_string()));\n    }\n}\n"
  },
  {
    "path": "src/errors.rs",
    "content": "//! # Application Error Handling\n\nuse axum::{\n    extract::rejection::JsonRejection,\n    http::{\n        header::{InvalidHeaderName, InvalidHeaderValue},\n        method::InvalidMethod,\n        StatusCode,\n    },\n};\nuse lettre::{address::AddressError, transport::smtp};\n\nuse crate::{controller::ErrorDetail, depcheck, validation::ModelValidationErrors};\n\n/*\nbacktrace principles:\n- use a plan warapper variant with no 'from' conversion\n- hand-code \"From\" conversion and force capture there with 'bt', which\n  will wrap and create backtrace only if RUST_BACKTRACE=1.\ncosts:\n- when RUST_BACKTRACE is not set, we don't pay for the capture and we dont pay for printing.\n\n */\nimpl From<serde_json::Error> for Error {\n    fn from(val: serde_json::Error) -> Self {\n        Self::JSON(val).bt()\n    }\n}\n\n#[derive(thiserror::Error, Debug)]\npub enum Error {\n    #[error(\"{inner}\\n{backtrace}\")]\n    WithBacktrace {\n        inner: Box<Self>,\n        backtrace: Box<std::backtrace::Backtrace>,\n    },\n\n    #[error(\"{0}\")]\n    Message(String),\n\n    #[error(\n        \"error while running worker: no queue provider populated in context. Did you configure \\\n         BackgroundQueue and connection details in `queue` in your config file?\"\n    )]\n    QueueProviderMissing,\n\n    #[error(\"task not found: '{0}'\")]\n    TaskNotFound(String),\n\n    #[error(transparent)]\n    Scheduler(#[from] crate::scheduler::Error),\n\n    #[error(transparent)]\n    Axum(#[from] axum::http::Error),\n\n    #[error(transparent)]\n    Tera(#[from] tera::Error),\n\n    #[error(transparent)]\n    JSON(serde_json::Error),\n\n    #[error(transparent)]\n    JsonRejection(#[from] JsonRejection),\n\n    #[error(\"cannot parse `{1}`: {0}\")]\n    YAMLFile(#[source] serde_yaml::Error, String),\n\n    #[error(transparent)]\n    YAML(#[from] serde_yaml::Error),\n\n    #[error(transparent)]\n    EnvVar(#[from] std::env::VarError),\n\n    #[error(\"Error sending email: '{0}'\")]\n    EmailSender(#[from] lettre::error::Error),\n\n    #[error(\"Error sending email (smtp): '{0}'\")]\n    Smtp(#[from] smtp::Error),\n\n    #[error(\"Worker error: {0}\")]\n    Worker(String),\n\n    #[error(transparent)]\n    IO(#[from] std::io::Error),\n\n    #[cfg(feature = \"with-db\")]\n    #[error(transparent)]\n    DB(#[from] sea_orm::DbErr),\n\n    #[error(transparent)]\n    ParseAddress(#[from] AddressError),\n\n    #[error(\"{0}\")]\n    Hash(String),\n\n    // API\n    #[error(\"{0}\")]\n    Unauthorized(String),\n\n    // API\n    #[error(\"not found\")]\n    NotFound,\n\n    #[error(\"{0}\")]\n    BadRequest(String),\n\n    #[error(\"\")]\n    CustomError(StatusCode, ErrorDetail),\n\n    #[error(\"internal server error\")]\n    InternalServerError,\n\n    #[error(transparent)]\n    InvalidHeaderValue(#[from] InvalidHeaderValue),\n\n    #[error(transparent)]\n    InvalidHeaderName(#[from] InvalidHeaderName),\n\n    #[error(transparent)]\n    InvalidMethod(#[from] InvalidMethod),\n\n    #[error(transparent)]\n    TaskJoinError(#[from] tokio::task::JoinError),\n\n    #[cfg(feature = \"with-db\")]\n    // Model\n    #[error(transparent)]\n    Model(#[from] crate::model::ModelError),\n\n    #[cfg(feature = \"bg_redis\")]\n    #[error(transparent)]\n    Redis(#[from] redis::RedisError),\n\n    #[cfg(any(feature = \"bg_pg\", feature = \"bg_sqlt\"))]\n    #[error(transparent)]\n    Sqlx(#[from] sqlx::Error),\n\n    #[error(transparent)]\n    Storage(#[from] crate::storage::StorageError),\n\n    #[error(transparent)]\n    Cache(#[from] crate::cache::CacheError),\n\n    #[cfg(debug_assertions)]\n    #[error(transparent)]\n    Generators(#[from] loco_gen::Error),\n\n    #[error(transparent)]\n    VersionCheck(#[from] depcheck::VersionCheckError),\n\n    #[error(transparent)]\n    SemVer(#[from] semver::Error),\n\n    #[error(transparent)]\n    Any(#[from] Box<dyn std::error::Error + Send + Sync>),\n\n    #[error(transparent)]\n    Validation(#[from] ModelValidationErrors),\n\n    #[error(transparent)]\n    AxumFormRejection(#[from] axum::extract::rejection::FormRejection),\n}\n\nimpl Error {\n    pub fn wrap(err: impl std::error::Error + Send + Sync + 'static) -> Self {\n        Self::Any(Box::new(err)) //.bt()\n    }\n\n    pub fn msg(err: impl std::error::Error + Send + Sync + 'static) -> Self {\n        Self::Message(err.to_string()) //.bt()\n    }\n    #[must_use]\n    pub fn string(s: &str) -> Self {\n        Self::Message(s.to_string())\n    }\n    #[must_use]\n    pub fn bt(self) -> Self {\n        let backtrace = std::backtrace::Backtrace::capture();\n        match backtrace.status() {\n            std::backtrace::BacktraceStatus::Disabled\n            | std::backtrace::BacktraceStatus::Unsupported => self,\n            _ => Self::WithBacktrace {\n                inner: Box::new(self),\n                backtrace: Box::new(backtrace),\n            },\n        }\n    }\n}\n"
  },
  {
    "path": "src/hash.rs",
    "content": "use crate::{Error, Result};\nuse argon2::{\n    password_hash::{rand_core::OsRng, SaltString},\n    Argon2, Params, PasswordHash, PasswordHasher, PasswordVerifier, Version,\n};\nuse rand::{distr::Alphanumeric, rng, Rng};\n\n/// Hashes a plain text password and returns the hashed result.\n///\n/// # Errors\n///\n/// Return [`argon2::password_hash::Result`] when could not hash the given\n/// password.\n///\n/// # Example\n/// ```rust\n/// use loco_rs::hash;\n///\n/// hash::hash_password(\"password-to-hash\");\n/// ```\npub fn hash_password(pass: &str) -> Result<String> {\n    let arg2 = Argon2::new(\n        argon2::Algorithm::Argon2id,\n        argon2::Version::V0x13,\n        Params::default(),\n    );\n    let salt = SaltString::generate(&mut OsRng);\n\n    Ok(arg2\n        .hash_password(pass.as_bytes(), &salt)\n        .map_err(|err| Error::Hash(err.to_string()))?\n        .to_string())\n}\n\n/// Verifies a plain text password against a hashed password.\n///\n/// # Errors\n///\n/// Return [`argon2::password_hash::Result`] when could verify the given data.\n///\n/// # Example\n/// ```rust\n/// use loco_rs::hash;\n///\n/// hash::verify_password(\"password\", \"hashed-password\");\n/// ```\n#[must_use]\npub fn verify_password(pass: &str, hashed_password: &str) -> bool {\n    let arg2 = Argon2::new(\n        argon2::Algorithm::Argon2id,\n        Version::V0x13,\n        Params::default(),\n    );\n    let Ok(hash) = PasswordHash::new(hashed_password) else {\n        return false;\n    };\n    arg2.verify_password(pass.as_bytes(), &hash).is_ok()\n}\n\n/// Generates a random alphanumeric string of the specified length.\n///\n/// # Example\n///\n/// ```rust\n/// use loco_rs::hash;\n///\n/// let rand_str = hash::random_string(10);\n/// assert_eq!(rand_str.len(), 10);\n/// assert_ne!(rand_str, hash::random_string(10));\n///\n/// ```\npub fn random_string(length: usize) -> String {\n    rng()\n        .sample_iter(&Alphanumeric)\n        .take(length)\n        .map(char::from)\n        .collect()\n}\n\n#[cfg(test)]\nmod tests {\n\n    use super::*;\n\n    #[test]\n    fn can_hah_password() {\n        let pass = \"password-1234\";\n\n        let hash_pass = hash_password(pass).unwrap();\n\n        assert!(verify_password(pass, &hash_pass));\n    }\n\n    #[test]\n    fn can_random_string() {\n        let random_length = 32;\n        let first = random_string(random_length);\n        assert_eq!(first.len(), random_length);\n        let second: String = random_string(random_length);\n        assert_eq!(second.len(), random_length);\n        assert_ne!(first, second);\n    }\n}\n"
  },
  {
    "path": "src/initializers/extra_db.rs",
    "content": "use async_trait::async_trait;\nuse axum::{Extension, Router as AxumRouter};\n\nuse crate::{\n    app::{AppContext, Initializer},\n    db, Error, Result,\n};\n\n#[allow(clippy::module_name_repetitions)]\npub struct ExtraDbInitializer;\n\n#[async_trait]\nimpl Initializer for ExtraDbInitializer {\n    fn name(&self) -> String {\n        \"extra_db\".to_string()\n    }\n\n    async fn after_routes(&self, router: AxumRouter, ctx: &AppContext) -> Result<AxumRouter> {\n        println!(\"1\");\n        let extra_db_config = ctx\n            .config\n            .initializers\n            .clone()\n            .ok_or_else(|| Error::Message(\"initializers config not configured\".to_string()))?;\n        println!(\"2\");\n        let extra_db_value = extra_db_config\n            .get(\"extra_db\")\n            .ok_or_else(|| Error::Message(\"initializers config not configured\".to_string()))?;\n\n        println!(\"3\");\n        let extra_db = serde_json::from_value(extra_db_value.clone())?;\n\n        let db = db::connect(&extra_db).await?;\n        Ok(router.layer(Extension(db)))\n    }\n}\n"
  },
  {
    "path": "src/initializers/mod.rs",
    "content": "#[cfg(feature = \"with-db\")]\npub mod extra_db;\n\n#[cfg(feature = \"with-db\")]\npub mod multi_db;\n"
  },
  {
    "path": "src/initializers/multi_db.rs",
    "content": "use async_trait::async_trait;\nuse axum::{Extension, Router as AxumRouter};\n\nuse crate::{\n    app::{AppContext, Initializer},\n    db, Error, Result,\n};\n\n#[allow(clippy::module_name_repetitions)]\npub struct MultiDbInitializer;\n\n#[async_trait]\nimpl Initializer for MultiDbInitializer {\n    fn name(&self) -> String {\n        \"multi_db\".to_string()\n    }\n\n    async fn after_routes(&self, router: AxumRouter, ctx: &AppContext) -> Result<AxumRouter> {\n        let settings = ctx\n            .config\n            .initializers\n            .clone()\n            .ok_or_else(|| Error::Message(\"settings config not configured\".to_string()))?;\n\n        let multi_db = settings\n            .get(\"multi_db\")\n            .ok_or_else(|| Error::Message(\"multi_db not configured\".to_string()))?;\n\n        let multi_db = db::MultiDb::new(serde_json::from_value(multi_db.clone())?).await?;\n        Ok(router.layer(Extension(multi_db)))\n    }\n}\n"
  },
  {
    "path": "src/lib.rs",
    "content": "#![allow(clippy::missing_const_for_fn)]\n#![allow(clippy::module_name_repetitions)]\n#![doc = include_str!(\"../README.md\")]\n\npub use self::errors::Error;\n\nmod banner;\npub mod bgworker;\nmod depcheck;\npub mod initializers;\npub mod prelude;\n\npub mod data;\npub mod doctor;\n\n#[cfg(feature = \"with-db\")]\npub mod db;\n#[cfg(feature = \"with-db\")]\npub mod model;\n#[cfg(feature = \"with-db\")]\npub mod schema;\nmod tera;\n\npub mod app;\npub mod auth;\npub mod boot;\npub mod cache;\n#[cfg(feature = \"cli\")]\npub mod cli;\npub mod config;\npub mod controller;\nmod env_vars;\npub mod environment;\npub mod errors;\npub mod hash;\npub mod logger;\npub mod mailer;\npub mod scheduler;\npub mod task;\n#[cfg(feature = \"testing\")]\npub mod testing;\n#[cfg(feature = \"testing\")]\npub use axum_test::TestServer;\npub mod storage;\n#[cfg(feature = \"testing\")]\npub mod tests_cfg;\npub mod validation;\npub use validator;\npub mod cargo_config;\n\n/// Application results options list\npub type Result<T, E = Error> = std::result::Result<T, E>;\n"
  },
  {
    "path": "src/logger.rs",
    "content": "//! initialization application logger.\n\nuse std::sync::OnceLock;\n\nuse serde::{Deserialize, Serialize};\nuse serde_variant::to_variant_name;\nuse tracing_appender::non_blocking::WorkerGuard;\nuse tracing_subscriber::{\n    fmt, fmt::MakeWriter, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer, Registry,\n};\n\nuse crate::{app::Hooks, config, Error, Result};\n\n// Define an enumeration for log levels\n#[derive(Debug, Default, Clone, Deserialize, Serialize)]\npub enum LogLevel {\n    /// The \"off\" level.\n    #[serde(rename = \"off\")]\n    Off,\n    /// The \"trace\" level.\n    #[serde(rename = \"trace\")]\n    Trace,\n    /// The \"debug\" level.\n    #[serde(rename = \"debug\")]\n    Debug,\n    /// The \"info\" level.\n    #[serde(rename = \"info\")]\n    #[default]\n    Info,\n    /// The \"warn\" level.\n    #[serde(rename = \"warn\")]\n    Warn,\n    /// The \"error\" level.\n    #[serde(rename = \"error\")]\n    Error,\n}\n\n// Define an enumeration for log formats\n#[derive(Debug, Default, Clone, Deserialize, Serialize)]\npub enum Format {\n    #[serde(rename = \"compact\")]\n    #[default]\n    Compact,\n    #[serde(rename = \"pretty\")]\n    Pretty,\n    #[serde(rename = \"json\")]\n    Json,\n}\n\n// Define an enumeration for log file appender rotation\n#[derive(Debug, Default, Clone, Deserialize, Serialize)]\npub enum Rotation {\n    #[serde(rename = \"minutely\")]\n    Minutely,\n    #[serde(rename = \"hourly\")]\n    #[default]\n    Hourly,\n    #[serde(rename = \"daily\")]\n    Daily,\n    #[serde(rename = \"never\")]\n    Never,\n}\n\n// Implement Display trait for LogLevel to enable pretty printing\nimpl std::fmt::Display for LogLevel {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        to_variant_name(self).expect(\"only enum supported\").fmt(f)\n    }\n}\n\n// Function to initialize the logger based on the provided configuration\nconst MODULE_WHITELIST: &[&str] = &[\n    \"loco_rs\",\n    \"sea_orm_migration\",\n    \"tower_http\",\n    \"sqlx::query\",\n    \"playground\",\n    \"loco_gen\",\n];\n\n// Keep nonblocking file appender work guard\nstatic NONBLOCKING_WORK_GUARD_KEEP: OnceLock<WorkerGuard> = OnceLock::new();\n\n///\n/// Tracing filtering rules:\n/// 1. if `RUST_LOG`, use that filter\n/// 2. if we have a config, and in it `override_filter` use that filter (ignore\n///    all else)\n/// 3. take `MODULE_WHITELIST` and filter only events from these modules, use\n///    `config.level` on each to filter their events\n///\n/// use cases:\n/// 1. mostly, people will set the level and will trust *us* to decide which\n///    modules to stream events from\n/// 2. people who will disagree with us, will set the `override_filter`\n///    permanently, or make up their own whitelist filtering (or suggest it to\n///    use via PR)\n/// 3. regardless of (1) and (2) operators in production, or elsewhere can\n///    always use `RUST_LOG` to quickly diagnose a service\n///\n/// # Errors\n/// Fails if cannot initialize logger or set up an appender (in case the option\n/// is enabled)\npub fn init<H: Hooks>(config: &config::Logger) -> Result<()> {\n    let mut layers: Vec<Box<dyn Layer<Registry> + Sync + Send>> = Vec::new();\n\n    if let Some(file_appender_config) = config.file_appender.as_ref() {\n        if file_appender_config.enable {\n            let dir = file_appender_config\n                .dir\n                .as_ref()\n                .map_or_else(|| \"./logs\".to_string(), ToString::to_string);\n\n            let mut rolling_builder = tracing_appender::rolling::Builder::default()\n                .max_log_files(file_appender_config.max_log_files);\n\n            rolling_builder = match file_appender_config.rotation {\n                Rotation::Minutely => {\n                    rolling_builder.rotation(tracing_appender::rolling::Rotation::MINUTELY)\n                }\n                Rotation::Hourly => {\n                    rolling_builder.rotation(tracing_appender::rolling::Rotation::HOURLY)\n                }\n                Rotation::Daily => {\n                    rolling_builder.rotation(tracing_appender::rolling::Rotation::DAILY)\n                }\n                Rotation::Never => {\n                    rolling_builder.rotation(tracing_appender::rolling::Rotation::NEVER)\n                }\n            };\n\n            let file_appender = rolling_builder\n                .filename_prefix(\n                    file_appender_config\n                        .filename_prefix\n                        .as_ref()\n                        .map_or_else(String::new, ToString::to_string),\n                )\n                .filename_suffix(\n                    file_appender_config\n                        .filename_suffix\n                        .as_ref()\n                        .map_or_else(String::new, ToString::to_string),\n                )\n                .build(dir)\n                .map_err(Error::msg)?;\n\n            let file_appender_layer = if file_appender_config.non_blocking {\n                let (non_blocking_file_appender, work_guard) =\n                    tracing_appender::non_blocking(file_appender);\n                NONBLOCKING_WORK_GUARD_KEEP\n                    .set(work_guard)\n                    .map_err(|_| Error::string(\"cannot lock for appender\"))?;\n                init_layer(\n                    non_blocking_file_appender,\n                    &file_appender_config.format,\n                    false,\n                )\n            } else {\n                init_layer(file_appender, &file_appender_config.format, false)\n            };\n            layers.push(file_appender_layer);\n        }\n    }\n\n    if config.enable {\n        let stdout_layer = init_layer(std::io::stdout, &config.format, true);\n        layers.push(stdout_layer);\n    }\n\n    if !layers.is_empty() {\n        let env_filter = init_env_filter::<H>(config.override_filter.as_ref(), &config.level);\n        tracing_subscriber::registry()\n            .with(layers)\n            .with(env_filter)\n            .init();\n    }\n    Ok(())\n}\n\nfn init_env_filter<H: Hooks>(override_filter: Option<&String>, level: &LogLevel) -> EnvFilter {\n    EnvFilter::try_from_default_env()\n        .or_else(|_| {\n            // user wanted a specific filter, don't care about our internal whitelist\n            // or, if no override give them the default whitelisted filter (most common)\n            override_filter.map_or_else(\n                || {\n                    EnvFilter::try_new(\n                        MODULE_WHITELIST\n                            .iter()\n                            .map(|m| format!(\"{m}={level}\"))\n                            .chain(std::iter::once(format!(\"{}={}\", H::app_name(), level)))\n                            .collect::<Vec<_>>()\n                            .join(\",\"),\n                    )\n                },\n                EnvFilter::try_new,\n            )\n        })\n        .expect(\"logger initialization failed\")\n}\n\nfn init_layer<W2>(\n    make_writer: W2,\n    format: &Format,\n    ansi: bool,\n) -> Box<dyn Layer<Registry> + Sync + Send>\nwhere\n    W2: for<'writer> MakeWriter<'writer> + Sync + Send + 'static,\n{\n    match format {\n        Format::Compact => fmt::Layer::default()\n            .with_ansi(ansi)\n            .with_writer(make_writer)\n            .compact()\n            .boxed(),\n        Format::Pretty => fmt::Layer::default()\n            .with_ansi(ansi)\n            .with_writer(make_writer)\n            .pretty()\n            .boxed(),\n        Format::Json => fmt::Layer::default()\n            .with_ansi(ansi)\n            .with_writer(make_writer)\n            .json()\n            .boxed(),\n    }\n}\n"
  },
  {
    "path": "src/mailer/email_sender.rs",
    "content": "//! This module defines an [`EmailSender`] responsible for sending emails using\n//! either the SMTP protocol. It includes an asynchronous method `mail` for\n//! sending emails with options like sender, recipient, subject, and content.\n\nuse lettre::{\n    message::{header, MultiPart},\n    transport::smtp::{authentication::Credentials, extension::ClientId},\n    AsyncTransport, Message, Tokio1Executor, Transport,\n};\nuse tracing::error;\n\nuse super::{Email, Result, DEFAULT_FROM_SENDER};\nuse crate::{config, errors::Error};\n\n/// An enumeration representing the possible transport methods for sending\n/// emails.\n#[derive(Clone, Debug)]\npub enum EmailTransport {\n    /// SMTP (Simple Mail Transfer Protocol) transport.\n    Smtp(lettre::AsyncSmtpTransport<lettre::Tokio1Executor>),\n    /// Test/stub transport for testing purposes.\n    Test(lettre::transport::stub::StubTransport),\n}\n\n/// A structure representing the email sender, encapsulating the chosen\n/// transport method.\n#[derive(Clone, Debug)]\npub struct EmailSender {\n    pub transport: EmailTransport,\n}\n\n#[cfg(feature = \"testing\")]\n#[derive(Default, Debug)]\npub struct Deliveries {\n    pub count: usize,\n    pub messages: Vec<String>,\n}\n\nimpl EmailSender {\n    /// Creates a new `EmailSender` using the SMTP transport method based on the\n    /// provided SMTP configuration.\n    ///\n    /// # Errors\n    ///\n    /// when could not initialize SMTP transport\n    pub fn smtp(config: &config::SmtpMailer) -> Result<Self> {\n        let mut email_builder = if config.secure {\n            lettre::AsyncSmtpTransport::<Tokio1Executor>::starttls_relay(&config.host)\n                .map_err(|error| {\n                    error!(err.msg = %error, err.detail = ?error, \"smtp_init_error\");\n                    error\n                })?\n                .port(config.port)\n        } else {\n            lettre::AsyncSmtpTransport::<Tokio1Executor>::builder_dangerous(&config.host)\n                .port(config.port)\n        };\n\n        if let Some(auth) = config.auth.as_ref() {\n            email_builder = email_builder\n                .credentials(Credentials::new(auth.user.clone(), auth.password.clone()));\n        }\n\n        if let Some(hello_name) = config.hello_name.as_ref() {\n            email_builder = email_builder.hello_name(ClientId::Domain(hello_name.clone()));\n        }\n\n        Ok(Self {\n            transport: EmailTransport::Smtp(email_builder.build()),\n        })\n    }\n\n    #[must_use]\n    pub fn stub() -> Self {\n        Self {\n            transport: EmailTransport::Test(lettre::transport::stub::StubTransport::new_ok()),\n        }\n    }\n\n    #[cfg(feature = \"testing\")]\n    #[must_use]\n    pub fn deliveries(&self) -> Deliveries {\n        if let EmailTransport::Test(stub) = &self.transport {\n            return Deliveries {\n                count: stub.messages().len(),\n                messages: stub\n                    .messages()\n                    .iter()\n                    .map(|(_, content)| content.clone())\n                    .collect(),\n            };\n        }\n\n        Deliveries::default()\n    }\n\n    /// Sends an email using the configured transport method.\n    ///\n    /// # Errors\n    ///\n    /// When email doesn't send successfully or has an error to build the\n    /// message\n    pub async fn mail(&self, email: &Email) -> Result<()> {\n        let content = MultiPart::alternative_plain_html(email.text.clone(), email.html.clone());\n        let mut builder = Message::builder()\n            .from(\n                email\n                    .from\n                    .clone()\n                    .unwrap_or_else(|| DEFAULT_FROM_SENDER.to_string())\n                    .parse()?,\n            )\n            .to(email.to.parse()?);\n\n        if let Some(bcc) = &email.bcc {\n            builder = builder.bcc(bcc.parse()?);\n        }\n\n        if let Some(cc) = &email.cc {\n            builder = builder.cc(cc.parse()?);\n        }\n\n        if let Some(reply_to) = &email.reply_to {\n            builder = builder.reply_to(reply_to.parse()?);\n        }\n\n        if let Some(headers) = &email.headers {\n            if let Some(references) = &headers.references {\n                builder = builder.header(header::References::from(references.clone()));\n            }\n            if let Some(in_reply_to) = &headers.in_reply_to {\n                builder = builder.header(header::InReplyTo::from(in_reply_to.clone()));\n            }\n            if let Some(message_id) = &headers.message_id {\n                builder = builder.header(header::MessageId::from(message_id.clone()));\n            }\n        }\n\n        let msg = builder\n            .subject(email.subject.clone())\n            .multipart(content)\n            .map_err(|error| {\n                error!(err.msg = %error, err.detail = ?error, \"email_building_error\");\n                error\n            })?;\n\n        match &self.transport {\n            EmailTransport::Smtp(xp) => {\n                xp.send(msg).await?;\n            }\n            EmailTransport::Test(xp) => {\n                xp.send(&msg)\n                    .map_err(|e| Error::Message(format!(\"sending email error: {e}\")))?;\n            }\n        }\n        Ok(())\n    }\n}\n\n#[cfg(test)]\nmod tests {\n\n    use insta::{assert_debug_snapshot, with_settings};\n    use lettre::transport::stub::StubTransport;\n\n    use super::*;\n\n    #[tokio::test]\n    async fn can_send_email() {\n        let stub = StubTransport::new_ok();\n\n        let sender = EmailSender {\n            transport: EmailTransport::Test(stub.clone()),\n        };\n\n        let html = r\"\n;<html>\n    <body>\n        Test Message\n    </body>\n</html>\";\n\n        let data = Email {\n            from: Some(\"test@framework.com\".to_string()),\n            to: \"user1@framework.com\".to_string(),\n            reply_to: None,\n            subject: \"Email Subject\".to_string(),\n            text: \"Welcome\".to_string(),\n            html: html.to_string(),\n            bcc: None,\n            cc: None,\n            headers: None,\n        };\n        assert!(sender.mail(&data).await.is_ok());\n\n        with_settings!({filters => vec![\n            (r\"[0-9A-Za-z]+{40}\", \"IDENTIFIER\"),\n            (r\"\\w+, \\d{1,2} \\w+ \\d{4} \\d{2}:\\d{2}:\\d{2} [+-]\\d{4}\", \"DATE\")\n        ]}, {\n            assert_debug_snapshot!(stub.messages());\n        });\n    }\n\n    #[tokio::test]\n    async fn can_send_email_with_custom_headers() {\n        let stub = StubTransport::new_ok();\n\n        let sender = EmailSender {\n            transport: EmailTransport::Test(stub.clone()),\n        };\n\n        let html = r\"\n<html>\n    <body>\n        Test Message with Headers\n    </body>\n</html>\";\n\n        let headers = crate::mailer::EmailHeaders {\n            references: Some(\"<notification-item-123@example.com>\".to_string()),\n            in_reply_to: Some(\"<notification-item-123@example.com>\".to_string()),\n            message_id: Some(\"<notification-item-123-1234567890@example.com>\".to_string()),\n        };\n\n        let data = Email {\n            from: Some(\"test@framework.com\".to_string()),\n            to: \"user1@framework.com\".to_string(),\n            reply_to: None,\n            subject: \"Email Subject with Headers\".to_string(),\n            text: \"Welcome with headers\".to_string(),\n            html: html.to_string(),\n            bcc: None,\n            cc: None,\n            headers: Some(headers),\n        };\n        assert!(sender.mail(&data).await.is_ok());\n\n        with_settings!({filters => vec![\n            (r\"[0-9A-Za-z]+{40}\", \"IDENTIFIER\"),\n            (r\"\\w+, \\d{1,2} \\w+ \\d{4} \\d{2}:\\d{2}:\\d{2} [+-]\\d{4}\", \"DATE\")\n        ]}, {\n            assert_debug_snapshot!(stub.messages());\n        });\n    }\n}\n"
  },
  {
    "path": "src/mailer/mod.rs",
    "content": "//! This module defines the email-related functionality, including the `Mailer`\n//! trait and its implementation, `Email` structure, and the `MailerWorker` for\n//! asynchronous email processing.\n\nmod email_sender;\nmod template;\n\nuse async_trait::async_trait;\npub use email_sender::EmailSender;\nuse include_dir::Dir;\nuse serde::{Deserialize, Serialize};\nuse tracing::error;\n\nuse self::template::Template;\nuse super::{app::AppContext, Result};\nuse crate::prelude::BackgroundWorker;\n\npub const DEFAULT_FROM_SENDER: &str = \"System <system@example.com>\";\n\n#[derive(Debug, Clone, Serialize, Deserialize, Default)]\npub struct EmailHeaders {\n    pub references: Option<String>,\n    pub in_reply_to: Option<String>,\n    pub message_id: Option<String>,\n}\n\n/// The arguments struct for specifying email details such as sender, recipient,\n/// reply-to, and locals.\n#[derive(Debug, Clone, Default)]\npub struct Args {\n    pub from: Option<String>,\n    pub to: String,\n    pub reply_to: Option<String>,\n    pub locals: serde_json::Value,\n    pub bcc: Option<String>,\n    pub cc: Option<String>,\n    pub headers: Option<EmailHeaders>,\n}\n\n/// The structure representing an email details.\n#[derive(Serialize, Deserialize, Debug, Clone, Default)]\npub struct Email {\n    /// Mailbox to `From` header\n    pub from: Option<String>,\n    /// Mailbox to `To` header\n    pub to: String,\n    /// Mailbox to `ReplyTo` header\n    pub reply_to: Option<String>,\n    /// Subject header to message\n    pub subject: String,\n    /// Plain text message\n    pub text: String,\n    /// HTML template\n    pub html: String,\n    /// BCC header to message\n    pub bcc: Option<String>,\n    /// CC header to message\n    pub cc: Option<String>,\n    /// Custom headers for the email (e.g., References, In-Reply-To, Message-ID)\n    pub headers: Option<EmailHeaders>,\n}\n\n/// The options struct for configuring the email sender.\n#[derive(Default, Debug)]\n#[allow(clippy::module_name_repetitions)]\npub struct MailerOpts {\n    pub from: String,\n    pub reply_to: Option<String>,\n}\n\n/// The `Mailer` trait defines methods for sending emails and processing email\n/// templates.\n#[async_trait]\npub trait Mailer {\n    /// Returns default options for the mailer.\n    #[must_use]\n    fn opts() -> MailerOpts {\n        MailerOpts {\n            from: DEFAULT_FROM_SENDER.to_string(),\n            ..Default::default()\n        }\n    }\n\n    /// Sends an email using the provided [`AppContext`] and email details.\n    async fn mail(ctx: &AppContext, email: &Email) -> Result<()> {\n        let opts = Self::opts();\n        let mut email = email.clone();\n\n        email.from = Some(email.from.unwrap_or_else(|| opts.from.clone()));\n        email.reply_to = email.reply_to.or_else(|| opts.reply_to.clone());\n\n        MailerWorker::perform_later(ctx, email.clone()).await?;\n        Ok(())\n    }\n\n    /// Renders and sends an email using the provided [`AppContext`], template\n    /// directory, and arguments.\n    async fn mail_template(ctx: &AppContext, dir: &Dir<'_>, args: Args) -> Result<()> {\n        let content = Template::new(dir).render(&args.locals)?;\n        Self::mail(\n            ctx,\n            &Email {\n                from: args.from.clone(),\n                to: args.to.clone(),\n                reply_to: args.reply_to.clone(),\n                subject: content.subject,\n                text: content.text,\n                html: content.html,\n                bcc: args.bcc.clone(),\n                cc: args.cc.clone(),\n                headers: args.headers.clone(),\n            },\n        )\n        .await\n    }\n}\n\n/// The [`MailerWorker`] struct represents a worker responsible for asynchronous\n/// email processing.\n#[allow(clippy::module_name_repetitions)]\npub struct MailerWorker {\n    pub ctx: AppContext,\n}\n\n/// Implementation of the [`Worker`] trait for the [`MailerWorker`].\n#[async_trait]\nimpl BackgroundWorker<Email> for MailerWorker {\n    fn queue() -> Option<String> {\n        Some(\"mailer\".to_string())\n    }\n\n    fn build(ctx: &AppContext) -> Self {\n        Self { ctx: ctx.clone() }\n    }\n\n    /// Performs the email sending operation using the provided [`AppContext`]\n    /// and email details.\n    async fn perform(&self, email: Email) -> crate::Result<()> {\n        if let Some(mailer) = &self.ctx.mailer {\n            let res = mailer.mail(&email).await;\n            match res {\n                Ok(res) => Ok(res),\n                Err(err) => {\n                    error!(err = err.to_string(), \"mailer error\");\n                    Err(err)\n                }\n            }\n        } else {\n            let err = crate::Error::Message(\n                \"attempting to send email but no email sender configured\".to_string(),\n            );\n            error!(err = err.to_string(), \"mailer error\");\n            Err(err)\n        }\n    }\n}\n"
  },
  {
    "path": "src/mailer/snapshots/loco_rs__mailer__email_sender__tests__can_send_email.snap",
    "content": "---\nsource: src/mailer/email_sender.rs\nexpression: stub.messages()\n---\n[\n    (\n        Envelope {\n            forward_path: [\n                Address {\n                    serialized: \"user1@framework.com\",\n                    at_start: 5,\n                },\n            ],\n            reverse_path: Some(\n                Address {\n                    serialized: \"test@framework.com\",\n                    at_start: 4,\n                },\n            ),\n        },\n        \"From: test@framework.com\\r\\nTo: user1@framework.com\\r\\nSubject: Email Subject\\r\\nMIME-Version: 1.0\\r\\nDate: DATE\\r\\nContent-Type: multipart/alternative;\\r\\n boundary=\\\"IDENTIFIER\\\"\\r\\n\\r\\n--IDENTIFIER\\r\\nContent-Type: text/plain; charset=utf-8\\r\\nContent-Transfer-Encoding: 7bit\\r\\n\\r\\nWelcome\\r\\n--IDENTIFIER\\r\\nContent-Type: text/html; charset=utf-8\\r\\nContent-Transfer-Encoding: 7bit\\r\\n\\r\\n\\r\\n;<html>\\r\\n    <body>\\r\\n        Test Message\\r\\n    </body>\\r\\n</html>\\r\\n--IDENTIFIER--\\r\\n\",\n    ),\n]\n"
  },
  {
    "path": "src/mailer/snapshots/loco_rs__mailer__email_sender__tests__can_send_email_with_custom_headers.snap",
    "content": "---\nsource: src/mailer/email_sender.rs\nexpression: stub.messages()\n---\n[\n    (\n        Envelope {\n            forward_path: [\n                Address {\n                    serialized: \"user1@framework.com\",\n                    at_start: 5,\n                },\n            ],\n            reverse_path: Some(\n                Address {\n                    serialized: \"test@framework.com\",\n                    at_start: 4,\n                },\n            ),\n        },\n        \"From: test@framework.com\\r\\nTo: user1@framework.com\\r\\nReferences: <notification-item-123@example.com>\\r\\nIn-Reply-To: <notification-item-123@example.com>\\r\\nMessage-ID: <notification-item-123-1234567890@example.com>\\r\\nSubject: Email Subject with Headers\\r\\nMIME-Version: 1.0\\r\\nDate: DATE\\r\\nContent-Type: multipart/alternative;\\r\\n boundary=\\\"IDENTIFIER\\\"\\r\\n\\r\\n--IDENTIFIER\\r\\nContent-Type: text/plain; charset=utf-8\\r\\nContent-Transfer-Encoding: 7bit\\r\\n\\r\\nWelcome with headers\\r\\n--IDENTIFIER\\r\\nContent-Type: text/html; charset=utf-8\\r\\nContent-Transfer-Encoding: 7bit\\r\\n\\r\\n\\r\\n<html>\\r\\n    <body>\\r\\n        Test Message with Headers\\r\\n    </body>\\r\\n</html>\\r\\n--IDENTIFIER--\\r\\n\",\n    ),\n]\n"
  },
  {
    "path": "src/mailer/snapshots/loco_rs__mailer__template__tests__can_render_template.snap",
    "content": "---\nsource: src/mailer/template.rs\nexpression: \"Template::new(&include_dir!(\\\"tests/fixtures/email_template/test\\\")).render(&args)\"\n---\nOk(\n    Content {\n        subject: \"Test Can render test template\\n\",\n        text: \"Welcome to test: Can render test template,\\n\\n  http://localhost/verify/<%= verifyToken %>\\n\",\n        html: \";<html>\\n\\n<body>\\n  This is a test content\\n  <a href=\\\"http://localhost:/verify/1111-2222-3333-4444\\\">\\n    Some test\\n  </a>\\n</body>\\n\\n</html>\\n\",\n    },\n)\n"
  },
  {
    "path": "src/mailer/template.rs",
    "content": "//! This module defines a template rendering mechanism for generating email\n//! content using Tera templates. It includes functions to read embedded\n//! template files, a `Content` struct to hold email content, and a `Template`\n//! struct to manage template rendering.\n//!\n//! # Example\n//!\n//! ```rust, ignore\n//! use include_dir::{include_dir, Dir};\n//! use loco_rs::mailer::template::Template;\n//!\n//! static welcome: Dir<'_> = include_dir!(\"src/mailers/auth/welcome\");\n//! let args = serde_json::json!({\"name\": \"framework\"});\n//! let content = Template::new(\"contnt\").render(&args);\n//! ```\n\nuse include_dir::Dir;\n\nuse crate::{errors::Error, tera, Result};\n\n/// The filename for the subject template file.\nconst SUBJECT: &str = \"subject.t\";\n/// The filename for the HTML template file.\nconst HTML: &str = \"html.t\";\n/// The filename for the plain text template file.\nconst TEXT: &str = \"text.t\";\n\n/// Reads an embedded file from the provided directory and returns its content\n/// as a string.\nfn embedded_file(dir: &Dir<'_>, name: &str) -> Result<String> {\n    Ok(String::from_utf8_lossy(\n        dir.get_file(name)\n            .ok_or_else(|| Error::Message(format!(\"no mailer template file found {name}\")))?\n            .contents(),\n    )\n    .to_string())\n}\n\n/// A structure representing the content of an email, including subject, text,\n/// and HTML.\n#[derive(Clone, Debug)]\npub struct Content {\n    pub subject: String,\n    pub text: String,\n    pub html: String,\n}\n\n/// A structure for managing template rendering using Tera.\n#[derive(Debug, Clone)]\npub struct Template<'a> {\n    /// The directory containing the embedded template files.\n    dir: &'a Dir<'a>,\n}\n\nimpl<'a> Template<'a> {\n    /// Creates a new `Template` instance with the provided directory.\n    pub const fn new(dir: &'a Dir<'_>) -> Self {\n        Self { dir }\n    }\n\n    /// Renders the email content based on the provided locals using the\n    /// embedded templates.\n    pub fn render(&self, locals: &serde_json::Value) -> Result<Content> {\n        let subject_t = embedded_file(self.dir, SUBJECT)?;\n        let text_t = embedded_file(self.dir, TEXT)?;\n        let html_t = embedded_file(self.dir, HTML)?;\n\n        // TODO(consider): check+consider offloading to tokio async this work\n        let text = tera::render_string(&text_t, locals)?;\n        let html = tera::render_string(&html_t, locals)?;\n        let subject = tera::render_string(&subject_t, locals)?;\n        Ok(Content {\n            subject,\n            text,\n            html,\n        })\n    }\n}\n\n#[cfg(test)]\nmod tests {\n\n    use include_dir::include_dir;\n    use insta::assert_debug_snapshot;\n\n    use super::*;\n\n    #[test]\n    fn can_render_template() {\n        let args = serde_json::json!({\n            \"verifyToken\": \"1111-2222-3333-4444\",\n            \"name\": \"Can render test template\",\n        });\n        assert_debug_snapshot!(\n            Template::new(&include_dir!(\"tests/fixtures/email_template/test\")).render(&args)\n        );\n    }\n}\n"
  },
  {
    "path": "src/model/mod.rs",
    "content": "//! # Model Error Handling\n//!\n//! Useful when using `sea_orm` and want to propagate errors\n\npub mod query;\nuse async_trait::async_trait;\nuse sea_orm::DatabaseConnection;\n\nuse crate::validation::ModelValidationErrors;\n\n#[derive(thiserror::Error, Debug)]\n#[allow(clippy::module_name_repetitions)]\npub enum ModelError {\n    #[error(\"Entity already exists\")]\n    EntityAlreadyExists,\n\n    #[error(\"Entity not found\")]\n    EntityNotFound,\n\n    #[error(transparent)]\n    Validation(#[from] ModelValidationErrors),\n\n    #[cfg(feature = \"auth_jwt\")]\n    #[error(\"jwt error\")]\n    Jwt(#[from] jsonwebtoken::errors::Error),\n\n    #[error(transparent)]\n    DbErr(#[from] sea_orm::DbErr),\n\n    #[error(transparent)]\n    Any(#[from] Box<dyn std::error::Error + Send + Sync>),\n\n    #[error(\"{0}\")]\n    Message(String),\n}\n\n#[allow(clippy::module_name_repetitions)]\npub type ModelResult<T, E = ModelError> = std::result::Result<T, E>;\n\nimpl ModelError {\n    #[must_use]\n    pub fn wrap(err: impl std::error::Error + Send + Sync + 'static) -> Self {\n        Self::Any(Box::new(err))\n    }\n\n    #[must_use]\n    pub fn to_msg(err: impl std::error::Error + Send + Sync + 'static) -> Self {\n        Self::Message(err.to_string())\n    }\n\n    #[must_use]\n    pub fn msg(s: &str) -> Self {\n        Self::Message(s.to_string())\n    }\n}\n#[async_trait]\npub trait Authenticable: Clone {\n    async fn find_by_api_key(db: &DatabaseConnection, api_key: &str) -> ModelResult<Self>;\n    async fn find_by_claims_key(db: &DatabaseConnection, claims_key: &str) -> ModelResult<Self>;\n}\n"
  },
  {
    "path": "src/model/query/dsl/date_range.rs",
    "content": "use chrono::NaiveDateTime;\nuse sea_orm::ColumnTrait;\n\nuse super::{with, ConditionBuilder};\n\n#[derive(Debug)]\npub struct DateRangeBuilder<T: ColumnTrait> {\n    col: T,\n    condition_builder: ConditionBuilder,\n    from_date: Option<NaiveDateTime>,\n    to_date: Option<NaiveDateTime>,\n}\n\nimpl<T: ColumnTrait> DateRangeBuilder<T> {\n    pub const fn new(condition_builder: ConditionBuilder, col: T) -> Self {\n        Self {\n            col,\n            condition_builder,\n            from_date: None,\n            to_date: None,\n        }\n    }\n\n    #[must_use]\n    pub fn dates(self, from: Option<&NaiveDateTime>, to: Option<&NaiveDateTime>) -> Self {\n        Self {\n            col: self.col,\n            condition_builder: self.condition_builder,\n            from_date: from.copied(),\n            to_date: to.copied(),\n        }\n    }\n\n    #[must_use]\n    pub fn from(self, from: &NaiveDateTime) -> Self {\n        Self {\n            col: self.col,\n            condition_builder: self.condition_builder,\n            from_date: Some(*from),\n            to_date: self.to_date,\n        }\n    }\n\n    #[must_use]\n    pub fn to(self, to: &NaiveDateTime) -> Self {\n        Self {\n            col: self.col,\n            condition_builder: self.condition_builder,\n            from_date: self.from_date,\n            to_date: Some(*to),\n        }\n    }\n\n    pub fn build(self) -> ConditionBuilder {\n        let con = match (self.from_date, self.to_date) {\n            (None, None) => self.condition_builder.condition,\n            (None, Some(to)) => self.condition_builder.condition.add(self.col.lt(to)),\n            (Some(from), None) => self.condition_builder.condition.add(self.col.gt(from)),\n            (Some(from), Some(to)) => self\n                .condition_builder\n                .condition\n                .add(self.col.between(from, to)),\n        };\n        with(con)\n    }\n}\n\n#[cfg(test)]\nmod tests {\n\n    use sea_orm::{EntityTrait, QueryFilter, QuerySelect, QueryTrait};\n\n    use crate::{prelude::model::query::*, tests_cfg::db::*};\n\n    #[test]\n    fn condition_date_range_from() {\n        let date =\n            chrono::NaiveDateTime::parse_from_str(\"2024-03-01 22:10:57\", \"%Y-%m-%d %H:%M:%S\")\n                .unwrap();\n\n        let condition = dsl::condition()\n            .date_range(test_db::Column::CreatedAt)\n            .from(&date)\n            .build();\n\n        let query_str = test_db::Entity::find()\n            .select_only()\n            .column(test_db::Column::Id)\n            .filter(condition.build())\n            .build(sea_orm::DatabaseBackend::Postgres)\n            .to_string();\n\n        assert_eq!(\n            query_str,\n            \"SELECT \\\"loco\\\".\\\"id\\\" FROM \\\"loco\\\" WHERE \\\"loco\\\".\\\"created_at\\\" > '2024-03-01 \\\n             22:10:57.000000'\"\n        );\n    }\n\n    #[test]\n    fn condition_date_range_to() {\n        let date =\n            chrono::NaiveDateTime::parse_from_str(\"2024-03-01 22:10:57\", \"%Y-%m-%d %H:%M:%S\")\n                .unwrap();\n\n        let condition = dsl::condition()\n            .date_range(test_db::Column::CreatedAt)\n            .to(&date)\n            .build();\n\n        let query_str = test_db::Entity::find()\n            .select_only()\n            .column(test_db::Column::Id)\n            .filter(condition.build())\n            .build(sea_orm::DatabaseBackend::Postgres)\n            .to_string();\n\n        assert_eq!(\n            query_str,\n            \"SELECT \\\"loco\\\".\\\"id\\\" FROM \\\"loco\\\" WHERE \\\"loco\\\".\\\"created_at\\\" < '2024-03-01 \\\n             22:10:57.000000'\"\n        );\n    }\n\n    #[test]\n    fn condition_date_both() {\n        let from_date =\n            chrono::NaiveDateTime::parse_from_str(\"2024-03-01 22:10:57\", \"%Y-%m-%d %H:%M:%S\")\n                .unwrap();\n        let to_date =\n            chrono::NaiveDateTime::parse_from_str(\"2024-03-25 22:10:57\", \"%Y-%m-%d %H:%M:%S\")\n                .unwrap();\n\n        let condition = dsl::condition()\n            .date_range(test_db::Column::CreatedAt)\n            .dates(Some(&from_date), Some(&to_date))\n            .build();\n\n        let query_str = test_db::Entity::find()\n            .select_only()\n            .column(test_db::Column::Id)\n            .filter(condition.build())\n            .build(sea_orm::DatabaseBackend::Postgres)\n            .to_string();\n\n        assert_eq!(\n            query_str,\n            \"SELECT \\\"loco\\\".\\\"id\\\" FROM \\\"loco\\\" WHERE \\\"loco\\\".\\\"created_at\\\" BETWEEN \\\n             '2024-03-01 22:10:57.000000' AND '2024-03-25 22:10:57.000000'\"\n        );\n    }\n}\n"
  },
  {
    "path": "src/model/query/dsl/mod.rs",
    "content": "use sea_orm::{\n    sea_query::{IntoCondition, Order},\n    ColumnTrait, Condition, Value,\n};\nuse serde::{Deserialize, Serialize};\n\nmod date_range;\n\n// pub mod pagination;\n\n#[derive(Debug)]\npub struct ConditionBuilder {\n    condition: Condition,\n}\n/// Enum representing sorting directions, with serialization and deserialization\n/// support.\n#[derive(Debug, Deserialize, Serialize)]\npub enum SortDirection {\n    #[serde(rename = \"desc\")]\n    Desc,\n    #[serde(rename = \"asc\")]\n    Asc,\n}\n\nimpl SortDirection {\n    /// Returns the corresponding `Order` enum variant based on the current\n    /// `SortDirection`.\n    #[must_use]\n    pub const fn order(&self) -> Order {\n        match self {\n            Self::Desc => Order::Desc,\n            Self::Asc => Order::Asc,\n        }\n    }\n}\n\n#[must_use]\npub fn condition() -> ConditionBuilder {\n    ConditionBuilder {\n        condition: Condition::all(),\n    }\n}\n\n#[must_use]\npub const fn with(condition: Condition) -> ConditionBuilder {\n    ConditionBuilder { condition }\n}\n\n/// See [`ConditionBuilder::eq`]\n#[must_use]\npub fn eq<T: ColumnTrait, V: Into<Value>>(col: T, value: V) -> ConditionBuilder {\n    condition().eq(col, value)\n}\n\n/// See [`ConditionBuilder::ne`]\n#[must_use]\npub fn not_equal<T: ColumnTrait, V: Into<Value>>(col: T, value: V) -> ConditionBuilder {\n    condition().ne(col, value)\n}\n\n/// See [`ConditionBuilder::gt`]\n#[must_use]\npub fn gt<T: ColumnTrait, V: Into<Value>>(col: T, value: V) -> ConditionBuilder {\n    condition().gt(col, value)\n}\n\n/// See [`ConditionBuilder::gte`]\n#[must_use]\npub fn gt_equal<T: ColumnTrait, V: Into<Value>>(col: T, value: V) -> ConditionBuilder {\n    condition().gte(col, value)\n}\n\n/// See [`ConditionBuilder::lt`]\n#[must_use]\npub fn lt<T: ColumnTrait, V: Into<Value>>(col: T, value: V) -> ConditionBuilder {\n    condition().lt(col, value)\n}\n\n/// See [`ConditionBuilder::lte`]\n#[must_use]\npub fn lt_equal<T: ColumnTrait, V: Into<Value>>(col: T, value: V) -> ConditionBuilder {\n    condition().lte(col, value)\n}\n\n/// See [`ConditionBuilder::between`]\n#[must_use]\npub fn between<T: ColumnTrait, V: Into<Value>>(col: T, a: V, b: V) -> ConditionBuilder {\n    condition().between(col, a, b)\n}\n\n/// See [`ConditionBuilder::not_between`]\n#[must_use]\npub fn not_between<T: ColumnTrait, V: Into<Value>>(col: T, a: V, b: V) -> ConditionBuilder {\n    condition().not_between(col, a, b)\n}\n\n/// See [`ConditionBuilder::like`]\n#[must_use]\npub fn like<T: ColumnTrait, V: Into<String>>(col: T, a: V) -> ConditionBuilder {\n    condition().like(col, a)\n}\n\n/// See [`ConditionBuilder::not_like`]\n#[must_use]\npub fn not_like<T: ColumnTrait, V: Into<String>>(col: T, a: V) -> ConditionBuilder {\n    condition().not_like(col, a)\n}\n\n/// See [`ConditionBuilder::starts_with`]\n#[must_use]\npub fn starts_with<T: ColumnTrait, V: Into<String>>(col: T, a: V) -> ConditionBuilder {\n    condition().starts_with(col, a)\n}\n\n/// See [`ConditionBuilder::ends_with`]\n#[must_use]\npub fn ends_with<T: ColumnTrait, V: Into<String>>(col: T, a: V) -> ConditionBuilder {\n    condition().ends_with(col, a)\n}\n\n/// See [`ConditionBuilder::contains`]\n#[must_use]\npub fn contains<T: ColumnTrait, V: Into<String>>(col: T, a: V) -> ConditionBuilder {\n    condition().contains(col, a)\n}\n\n/// See [`ConditionBuilder::is_null`]\n#[must_use]\n#[allow(clippy::wrong_self_convention)]\npub fn is_null<T: ColumnTrait>(col: T) -> ConditionBuilder {\n    condition().is_null(col)\n}\n\n/// See [`ConditionBuilder::is_not_null`]\n#[must_use]\n#[allow(clippy::wrong_self_convention)]\npub fn is_not_null<T: ColumnTrait>(col: T) -> ConditionBuilder {\n    condition().is_not_null(col)\n}\n\n/// See [`ConditionBuilder::is_in`]\n#[must_use]\n#[allow(clippy::wrong_self_convention)]\npub fn is_in<T: ColumnTrait, V: Into<Value>, I: IntoIterator<Item = V>>(\n    col: T,\n    values: I,\n) -> ConditionBuilder {\n    condition().is_in(col, values)\n}\n\n/// See [`ConditionBuilder::is_not_in`]\n#[must_use]\n#[allow(clippy::wrong_self_convention)]\npub fn is_not_in<T: ColumnTrait, V: Into<Value>, I: IntoIterator<Item = V>>(\n    col: T,\n    values: I,\n) -> ConditionBuilder {\n    condition().is_not_in(col, values)\n}\n\n/// See [`ConditionBuilder::date_range`]\n#[must_use]\npub fn date_range<T: ColumnTrait>(col: T) -> date_range::DateRangeBuilder<T> {\n    date_range::DateRangeBuilder::new(condition(), col)\n}\n\nimpl IntoCondition for ConditionBuilder {\n    fn into_condition(self) -> Condition {\n        self.build()\n    }\n}\n\n/// Builder query condition\n///\n/// # Examples\n/// ```\n/// use loco_rs::tests_cfg::db::test_db;\n/// use sea_orm::{EntityTrait, QueryFilter, QuerySelect, QueryTrait};\n/// use loco_rs::prelude::*;\n/// let date = chrono::NaiveDateTime::parse_from_str(\"2024-03-01 22:10:57\", \"%Y-%m-%d %H:%M:%S\").unwrap();\n///\n/// let query_str = test_db::Entity::find()\n///         .select_only()\n///         .column(test_db::Column::Id)\n///         .filter(query::condition().date_range(test_db::Column::CreatedAt).from(&date).build().like(test_db::Column::Name, \"%lo\").build())\n///         .build(sea_orm::DatabaseBackend::Postgres)\n///         .to_string();\n///\n///     assert_eq!(\n///         query_str,\n///         \"SELECT \\\"loco\\\".\\\"id\\\" FROM \\\"loco\\\" WHERE \\\"loco\\\".\\\"created_at\\\" > '2024-03-01 22:10:57.000000' AND \\\"loco\\\".\\\"name\\\" LIKE '%lo'\"\n///     );\n/// ````\nimpl ConditionBuilder {\n    /// where condition the given column equals the given value\n    ///\n    /// # Examples\n    /// ```\n    /// use loco_rs::tests_cfg::db::test_db;\n    /// use sea_orm::{EntityTrait, QueryFilter, QuerySelect, QueryTrait};\n    /// use loco_rs::prelude::*;\n    ///\n    /// let query_str = test_db::Entity::find()\n    ///         .select_only()\n    ///         .column(test_db::Column::Id)\n    ///         .filter(query::condition().eq(test_db::Column::Id, 1).build())\n    ///         .build(sea_orm::DatabaseBackend::Postgres)\n    ///         .to_string();\n    ///\n    ///     assert_eq!(\n    ///         query_str,\n    ///         \"SELECT \\\"loco\\\".\\\"id\\\" FROM \\\"loco\\\" WHERE \\\"loco\\\".\\\"id\\\" = 1\"\n    ///     );\n    /// ````\n    ///\n    /// On string field\n    /// ```\n    /// use loco_rs::tests_cfg::db::test_db;\n    /// use sea_orm::{EntityTrait, QueryFilter, QuerySelect, QueryTrait};\n    /// use loco_rs::prelude::*;\n    ///\n    /// let query_str = test_db::Entity::find()\n    ///         .select_only()\n    ///         .column(test_db::Column::Id)\n    ///         .filter(query::condition().eq(test_db::Column::Name, \"loco\").build())\n    ///         .build(sea_orm::DatabaseBackend::Postgres)\n    ///         .to_string();\n    ///\n    ///     assert_eq!(\n    ///         query_str,\n    ///         \"SELECT \\\"loco\\\".\\\"id\\\" FROM \\\"loco\\\" WHERE \\\"loco\\\".\\\"name\\\" = 'loco'\"\n    ///     );\n    /// ````\n    #[must_use]\n    pub fn eq<T: ColumnTrait, V: Into<Value>>(self, col: T, value: V) -> Self {\n        with(self.condition.add(col.eq(value)))\n    }\n\n    /// where condition the given column not equals the given value\n    ///\n    /// # Examples\n    /// ```\n    /// use loco_rs::tests_cfg::db::test_db;\n    /// use sea_orm::{EntityTrait, QueryFilter, QuerySelect, QueryTrait};\n    /// use loco_rs::prelude::*;\n    ///\n    /// let query_str = test_db::Entity::find()\n    ///         .select_only()\n    ///         .column(test_db::Column::Id)\n    ///         .filter(query::condition().ne(test_db::Column::Id, 1).build())\n    ///         .build(sea_orm::DatabaseBackend::Postgres)\n    ///         .to_string();\n    ///\n    ///     assert_eq!(\n    ///         query_str,\n    ///         \"SELECT \\\"loco\\\".\\\"id\\\" FROM \\\"loco\\\" WHERE \\\"loco\\\".\\\"id\\\" <> 1\"\n    ///     );\n    /// ````\n    #[must_use]\n    pub fn ne<T: ColumnTrait, V: Into<Value>>(self, col: T, value: V) -> Self {\n        with(self.condition.add(col.ne(value)))\n    }\n\n    /// where condition the given column greater than the given value\n    ///\n    /// # Examples\n    /// ```\n    /// use loco_rs::tests_cfg::db::test_db;\n    /// use sea_orm::{EntityTrait, QueryFilter, QuerySelect, QueryTrait};\n    /// use loco_rs::prelude::*;\n    ///\n    /// let query_str = test_db::Entity::find()\n    ///         .select_only()\n    ///         .column(test_db::Column::Id)\n    ///         .filter(query::condition().gt(test_db::Column::Id, 1).build())\n    ///         .build(sea_orm::DatabaseBackend::Postgres)\n    ///         .to_string();\n    ///\n    ///     assert_eq!(\n    ///         query_str,\n    ///         \"SELECT \\\"loco\\\".\\\"id\\\" FROM \\\"loco\\\" WHERE \\\"loco\\\".\\\"id\\\" > 1\"\n    ///     );\n    /// ````\n    #[must_use]\n    pub fn gt<T: ColumnTrait, V: Into<Value>>(self, col: T, value: V) -> Self {\n        with(self.condition.add(col.gt(value)))\n    }\n\n    /// where condition the given column greater than or equal to the given\n    /// value\n    ///\n    /// # Examples\n    /// ```\n    /// use loco_rs::tests_cfg::db::test_db;\n    /// use sea_orm::{EntityTrait, QueryFilter, QuerySelect, QueryTrait};\n    /// use loco_rs::prelude::*;\n    ///\n    /// let query_str = test_db::Entity::find()\n    ///         .select_only()\n    ///         .column(test_db::Column::Id)\n    ///         .filter(query::condition().gte(test_db::Column::Id, 1).build())\n    ///         .build(sea_orm::DatabaseBackend::Postgres)\n    ///         .to_string();\n    ///\n    ///     assert_eq!(\n    ///         query_str,\n    ///         \"SELECT \\\"loco\\\".\\\"id\\\" FROM \\\"loco\\\" WHERE \\\"loco\\\".\\\"id\\\" >= 1\"\n    ///     );\n    /// ````\n    #[must_use]\n    pub fn gte<T: ColumnTrait, V: Into<Value>>(self, col: T, value: V) -> Self {\n        with(self.condition.add(col.gte(value)))\n    }\n\n    /// where condition the given column smaller than to the given\n    /// value\n    ///\n    /// # Examples\n    /// ```\n    /// use loco_rs::tests_cfg::db::test_db;\n    /// use sea_orm::{EntityTrait, QueryFilter, QuerySelect, QueryTrait};\n    /// use loco_rs::prelude::*;\n    ///\n    /// let query_str = test_db::Entity::find()\n    ///         .select_only()\n    ///         .column(test_db::Column::Id)\n    ///         .filter(query::condition().lt(test_db::Column::Id, 1).build())\n    ///         .build(sea_orm::DatabaseBackend::Postgres)\n    ///         .to_string();\n    ///\n    ///     assert_eq!(\n    ///         query_str,\n    ///         \"SELECT \\\"loco\\\".\\\"id\\\" FROM \\\"loco\\\" WHERE \\\"loco\\\".\\\"id\\\" < 1\"\n    ///     );\n    /// ````\n    #[must_use]\n    pub fn lt<T: ColumnTrait, V: Into<Value>>(self, col: T, value: V) -> Self {\n        with(self.condition.add(col.lt(value)))\n    }\n\n    /// where condition the given column smaller than or equal to the given\n    /// value\n    ///\n    /// # Examples\n    /// ```\n    /// use loco_rs::tests_cfg::db::test_db;\n    /// use sea_orm::{EntityTrait, QueryFilter, QuerySelect, QueryTrait};\n    /// use loco_rs::prelude::*;\n    ///\n    /// let query_str = test_db::Entity::find()\n    ///         .select_only()\n    ///         .column(test_db::Column::Id)\n    ///         .filter(query::condition().lte(test_db::Column::Id, 1).build())\n    ///         .build(sea_orm::DatabaseBackend::Postgres)\n    ///         .to_string();\n    ///\n    ///     assert_eq!(\n    ///         query_str,\n    ///         \"SELECT \\\"loco\\\".\\\"id\\\" FROM \\\"loco\\\" WHERE \\\"loco\\\".\\\"id\\\" <= 1\"\n    ///     );\n    /// ````\n    #[must_use]\n    pub fn lte<T: ColumnTrait, V: Into<Value>>(self, col: T, value: V) -> Self {\n        with(self.condition.add(col.lte(value)))\n    }\n\n    /// where condition the given column between the given values\n    /// value\n    ///\n    /// # Examples\n    /// ```\n    /// use loco_rs::tests_cfg::db::test_db;\n    /// use sea_orm::{EntityTrait, QueryFilter, QuerySelect, QueryTrait};\n    /// use loco_rs::prelude::*;\n    ///\n    /// let query_str = test_db::Entity::find()\n    ///         .select_only()\n    ///         .column(test_db::Column::Id)\n    ///         .filter(query::condition().between(test_db::Column::Id, 1, 2).build())\n    ///         .build(sea_orm::DatabaseBackend::Postgres)\n    ///         .to_string();\n    ///\n    ///     assert_eq!(\n    ///         query_str,\n    ///         \"SELECT \\\"loco\\\".\\\"id\\\" FROM \\\"loco\\\" WHERE \\\"loco\\\".\\\"id\\\" BETWEEN 1 AND 2\"\n    ///     );\n    /// ````\n    #[must_use]\n    pub fn between<T: ColumnTrait, V: Into<Value>>(self, col: T, a: V, b: V) -> Self {\n        with(self.condition.add(col.between(a, b)))\n    }\n\n    /// where condition the given column not between the given values\n    /// value\n    ///\n    /// # Examples\n    /// ```\n    /// use loco_rs::tests_cfg::db::test_db;\n    /// use sea_orm::{EntityTrait, QueryFilter, QuerySelect, QueryTrait};\n    /// use loco_rs::prelude::*;\n    ///\n    /// let query_str = test_db::Entity::find()\n    ///         .select_only()\n    ///         .column(test_db::Column::Id)\n    ///         .filter(query::condition().not_between(test_db::Column::Id, 1, 2).build())\n    ///         .build(sea_orm::DatabaseBackend::Postgres)\n    ///         .to_string();\n    ///\n    ///     assert_eq!(\n    ///         query_str,\n    ///         \"SELECT \\\"loco\\\".\\\"id\\\" FROM \\\"loco\\\" WHERE \\\"loco\\\".\\\"id\\\" NOT BETWEEN 1 AND 2\"\n    ///     );\n    /// ````\n    #[must_use]\n    pub fn not_between<T: ColumnTrait, V: Into<Value>>(self, col: T, a: V, b: V) -> Self {\n        with(self.condition.add(col.not_between(a, b)))\n    }\n\n    /// where condition the given column like given values\n    /// value\n    ///\n    /// # Examples\n    /// ```\n    /// use loco_rs::tests_cfg::db::test_db;\n    /// use sea_orm::{EntityTrait, QueryFilter, QuerySelect, QueryTrait};\n    /// use loco_rs::prelude::*;\n    ///\n    /// let query_str = test_db::Entity::find()\n    ///         .select_only()\n    ///         .column(test_db::Column::Id)\n    ///         .filter(query::condition().like(test_db::Column::Name, \"%lo\").build())\n    ///         .build(sea_orm::DatabaseBackend::Postgres)\n    ///         .to_string();\n    ///\n    ///     assert_eq!(\n    ///         query_str,\n    ///         \"SELECT \\\"loco\\\".\\\"id\\\" FROM \\\"loco\\\" WHERE \\\"loco\\\".\\\"name\\\" LIKE '%lo'\"\n    ///     );\n    /// ````\n    #[must_use]\n    pub fn like<T: ColumnTrait, V: Into<String>>(self, col: T, a: V) -> Self {\n        with(self.condition.add(col.like(a)))\n    }\n\n    /// where condition the given column not like given values\n    /// value\n    ///\n    /// # Examples\n    /// ```\n    /// use loco_rs::tests_cfg::db::test_db;\n    /// use sea_orm::{EntityTrait, QueryFilter, QuerySelect, QueryTrait};\n    /// use loco_rs::prelude::*;\n    ///\n    /// let query_str = test_db::Entity::find()\n    ///         .select_only()\n    ///         .column(test_db::Column::Id)\n    ///         .filter(query::condition().not_like(test_db::Column::Name, \"%lo\").build())\n    ///         .build(sea_orm::DatabaseBackend::Postgres)\n    ///         .to_string();\n    ///\n    ///     assert_eq!(\n    ///         query_str,\n    ///         \"SELECT \\\"loco\\\".\\\"id\\\" FROM \\\"loco\\\" WHERE \\\"loco\\\".\\\"name\\\" NOT LIKE '%lo'\"\n    ///     );\n    /// ````\n    #[must_use]\n    pub fn not_like<T: ColumnTrait, V: Into<String>>(self, col: T, a: V) -> Self {\n        with(self.condition.add(col.not_like(a)))\n    }\n\n    /// where condition the given column start with given values\n    /// value\n    ///\n    /// # Examples\n    /// ```\n    /// use loco_rs::tests_cfg::db::test_db;\n    /// use sea_orm::{EntityTrait, QueryFilter, QuerySelect, QueryTrait};\n    /// use loco_rs::prelude::*;\n    ///\n    /// let query_str = test_db::Entity::find()\n    ///         .select_only()\n    ///         .column(test_db::Column::Id)\n    ///         .filter(query::condition().starts_with(test_db::Column::Name, \"lo\").build())\n    ///         .build(sea_orm::DatabaseBackend::Postgres)\n    ///         .to_string();\n    ///\n    ///     assert_eq!(\n    ///         query_str,\n    ///         \"SELECT \\\"loco\\\".\\\"id\\\" FROM \\\"loco\\\" WHERE \\\"loco\\\".\\\"name\\\" LIKE 'lo%'\"\n    ///     );\n    /// ````\n    #[must_use]\n    pub fn starts_with<T: ColumnTrait, V: Into<String>>(self, col: T, a: V) -> Self {\n        with(self.condition.add(col.starts_with(a)))\n    }\n\n    /// where condition the given column end with given values\n    /// value\n    ///\n    /// # Examples\n    /// ```\n    /// use loco_rs::tests_cfg::db::test_db;\n    /// use sea_orm::{EntityTrait, QueryFilter, QuerySelect, QueryTrait};\n    /// use loco_rs::prelude::*;\n    ///\n    /// let query_str = test_db::Entity::find()\n    ///         .select_only()\n    ///         .column(test_db::Column::Id)\n    ///         .filter(query::condition().ends_with(test_db::Column::Name, \"lo\").build())\n    ///         .build(sea_orm::DatabaseBackend::Postgres)\n    ///         .to_string();\n    ///\n    ///     assert_eq!(\n    ///         query_str,\n    ///         \"SELECT \\\"loco\\\".\\\"id\\\" FROM \\\"loco\\\" WHERE \\\"loco\\\".\\\"name\\\" LIKE '%lo'\"\n    ///     );\n    /// ````\n    #[must_use]\n    pub fn ends_with<T: ColumnTrait, V: Into<String>>(self, col: T, a: V) -> Self {\n        with(self.condition.add(col.ends_with(a)))\n    }\n\n    /// where condition the given column end with given values\n    /// value\n    ///\n    /// # Examples\n    /// ```\n    /// use loco_rs::tests_cfg::db::test_db;\n    /// use sea_orm::{EntityTrait, QueryFilter, QuerySelect, QueryTrait};\n    /// use loco_rs::prelude::*;\n    ///\n    /// let query_str = test_db::Entity::find()\n    ///         .select_only()\n    ///         .column(test_db::Column::Id)\n    ///         .filter(query::condition().contains(test_db::Column::Name, \"lo\").build())\n    ///         .build(sea_orm::DatabaseBackend::Postgres)\n    ///         .to_string();\n    ///\n    ///     assert_eq!(\n    ///         query_str,\n    ///         \"SELECT \\\"loco\\\".\\\"id\\\" FROM \\\"loco\\\" WHERE \\\"loco\\\".\\\"name\\\" LIKE '%lo%'\"\n    ///     );\n    /// ````\n    #[must_use]\n    pub fn contains<T: ColumnTrait, V: Into<String>>(self, col: T, a: V) -> Self {\n        with(self.condition.add(col.contains(a)))\n    }\n\n    /// where condition the given column is null\n    /// value\n    ///\n    /// # Examples\n    /// ```\n    /// use loco_rs::tests_cfg::db::test_db;\n    /// use sea_orm::{EntityTrait, QueryFilter, QuerySelect, QueryTrait};\n    /// use loco_rs::prelude::*;\n    ///\n    /// let query_str = test_db::Entity::find()\n    ///         .select_only()\n    ///         .column(test_db::Column::Id)\n    ///         .filter(query::condition().is_null(test_db::Column::Name).build())\n    ///         .build(sea_orm::DatabaseBackend::Postgres)\n    ///         .to_string();\n    ///\n    ///     assert_eq!(\n    ///         query_str,\n    ///         \"SELECT \\\"loco\\\".\\\"id\\\" FROM \\\"loco\\\" WHERE \\\"loco\\\".\\\"name\\\" IS NULL\"\n    ///     );\n    /// ````\n    #[must_use]\n    #[allow(clippy::wrong_self_convention)]\n    pub fn is_null<T: ColumnTrait>(self, col: T) -> Self {\n        with(self.condition.add(col.is_null()))\n    }\n\n    /// where condition the given column is not null\n    /// value\n    ///\n    /// # Examples\n    /// ```\n    /// use loco_rs::tests_cfg::db::test_db;\n    /// use sea_orm::{EntityTrait, QueryFilter, QuerySelect, QueryTrait};\n    /// use loco_rs::prelude::*;\n    ///\n    /// let query_str = test_db::Entity::find()\n    ///         .select_only()\n    ///         .column(test_db::Column::Id)\n    ///         .filter(query::condition().is_not_null(test_db::Column::Name).build())\n    ///         .build(sea_orm::DatabaseBackend::Postgres)\n    ///         .to_string();\n    ///\n    ///     assert_eq!(\n    ///         query_str,\n    ///         \"SELECT \\\"loco\\\".\\\"id\\\" FROM \\\"loco\\\" WHERE \\\"loco\\\".\\\"name\\\" IS NOT NULL\"\n    ///     );\n    /// ````\n    #[must_use]\n    #[allow(clippy::wrong_self_convention)]\n    pub fn is_not_null<T: ColumnTrait>(self, col: T) -> Self {\n        with(self.condition.add(col.is_not_null()))\n    }\n\n    /// where condition the given column is in\n    /// value\n    ///\n    /// # Examples\n    /// ```\n    /// use loco_rs::tests_cfg::db::test_db;\n    /// use sea_orm::{EntityTrait, QueryFilter, QuerySelect, QueryTrait};\n    /// use loco_rs::prelude::*;\n    ///\n    /// let query_str = test_db::Entity::find()\n    ///         .select_only()\n    ///         .column(test_db::Column::Id)\n    ///         .filter(query::condition().is_in(test_db::Column::Id, [1]).build())\n    ///         .build(sea_orm::DatabaseBackend::Postgres)\n    ///         .to_string();\n    ///\n    ///     assert_eq!(\n    ///         query_str,\n    ///         \"SELECT \\\"loco\\\".\\\"id\\\" FROM \\\"loco\\\" WHERE \\\"loco\\\".\\\"id\\\" IN (1)\"\n    ///     );\n    /// ````\n    #[must_use]\n    #[allow(clippy::wrong_self_convention)]\n    pub fn is_in<T: ColumnTrait, V: Into<Value>, I: IntoIterator<Item = V>>(\n        self,\n        col: T,\n        values: I,\n    ) -> Self {\n        with(self.condition.add(col.is_in(values)))\n    }\n\n    /// where condition the given column is not in\n    /// value\n    ///\n    /// # Examples\n    /// ```\n    /// use loco_rs::tests_cfg::db::test_db;\n    /// use sea_orm::{EntityTrait, QueryFilter, QuerySelect, QueryTrait};\n    /// use loco_rs::prelude::*;\n    ///\n    /// let query_str = test_db::Entity::find()\n    ///         .select_only()\n    ///         .column(test_db::Column::Id)\n    ///         .filter(query::condition().is_not_in(test_db::Column::Id, [1]).build())\n    ///         .build(sea_orm::DatabaseBackend::Postgres)\n    ///         .to_string();\n    ///\n    ///     assert_eq!(\n    ///         query_str,\n    ///         \"SELECT \\\"loco\\\".\\\"id\\\" FROM \\\"loco\\\" WHERE \\\"loco\\\".\\\"id\\\" NOT IN (1)\"\n    ///     );\n    /// ````\n    #[must_use]\n    #[allow(clippy::wrong_self_convention)]\n    pub fn is_not_in<T: ColumnTrait, V: Into<Value>, I: IntoIterator<Item = V>>(\n        self,\n        col: T,\n        values: I,\n    ) -> Self {\n        with(self.condition.add(col.is_not_in(values)))\n    }\n\n    /// where condition the given column is not null\n    /// value\n    ///\n    /// # Examples\n    /// ```\n    /// use loco_rs::tests_cfg::db::test_db;\n    /// use sea_orm::{EntityTrait, QueryFilter, QuerySelect, QueryTrait};\n    /// use loco_rs::prelude::*;\n    ///\n    /// let from_date = chrono::NaiveDateTime::parse_from_str(\"2024-03-01\n    /// 22:10:57\", \"%Y-%m-%d %H:%M:%S\").unwrap(); let to_date =\n    /// chrono::NaiveDateTime::parse_from_str(\"2024-03-25 22:10:57\", \"%Y-%m-%d\n    /// %H:%M:%S\").unwrap();\n    ///\n    /// let condition = query::condition()\n    ///     .date_range(test_db::Column::CreatedAt)\n    ///     .dates(Some(&from_date), Some(&to_date))\n    ///     .build();\n    ///\n    /// let query_str = test_db::Entity::find()\n    ///     .select_only()\n    ///     .column(test_db::Column::Id)\n    ///     .filter(condition.build())\n    ///     .build(sea_orm::DatabaseBackend::Postgres)\n    ///     .to_string();\n    ///\n    /// assert_eq!(\n    ///     query_str,\n    ///     \"SELECT \\\"loco\\\".\\\"id\\\" FROM \\\"loco\\\" WHERE \\\"loco\\\".\\\"created_at\\\" BETWEEN '2024-03-01 22:10:57.000000' AND '2024-03-25 22:10:57.000000'\" );\n    /// ````\n    #[must_use]\n    pub fn date_range<T: ColumnTrait>(self, col: T) -> date_range::DateRangeBuilder<T> {\n        date_range::DateRangeBuilder::new(self, col)\n    }\n\n    #[must_use]\n    pub fn build(&self) -> Condition {\n        self.condition.clone().into_condition()\n    }\n}\n\n#[cfg(test)]\nmod tests {\n\n    use sea_orm::{EntityTrait, QueryFilter, QuerySelect, QueryTrait};\n\n    use super::*;\n    use crate::tests_cfg::db::*;\n\n    #[test]\n    fn condition_eq() {\n        let query_str = test_db::Entity::find()\n            .select_only()\n            .column(test_db::Column::Id)\n            .filter(condition().eq(test_db::Column::Id, 1).build())\n            .build(sea_orm::DatabaseBackend::Postgres)\n            .to_string();\n\n        assert_eq!(\n            query_str,\n            \"SELECT \\\"loco\\\".\\\"id\\\" FROM \\\"loco\\\" WHERE \\\"loco\\\".\\\"id\\\" = 1\"\n        );\n    }\n\n    #[test]\n    fn condition_ne() {\n        let query_str = test_db::Entity::find()\n            .select_only()\n            .column(test_db::Column::Id)\n            .filter(condition().ne(test_db::Column::Name, \"loco\").build())\n            .build(sea_orm::DatabaseBackend::Postgres)\n            .to_string();\n\n        assert_eq!(\n            query_str,\n            \"SELECT \\\"loco\\\".\\\"id\\\" FROM \\\"loco\\\" WHERE \\\"loco\\\".\\\"name\\\" <> 'loco'\"\n        );\n    }\n\n    #[test]\n    fn condition_gt() {\n        let query_str = test_db::Entity::find()\n            .select_only()\n            .column(test_db::Column::Id)\n            .filter(condition().gt(test_db::Column::Id, 1).build())\n            .build(sea_orm::DatabaseBackend::Postgres)\n            .to_string();\n\n        assert_eq!(\n            query_str,\n            \"SELECT \\\"loco\\\".\\\"id\\\" FROM \\\"loco\\\" WHERE \\\"loco\\\".\\\"id\\\" > 1\"\n        );\n    }\n\n    #[test]\n    fn condition_gte() {\n        let query_str = test_db::Entity::find()\n            .select_only()\n            .column(test_db::Column::Id)\n            .filter(condition().gte(test_db::Column::Id, 1).build())\n            .build(sea_orm::DatabaseBackend::Postgres)\n            .to_string();\n\n        assert_eq!(\n            query_str,\n            \"SELECT \\\"loco\\\".\\\"id\\\" FROM \\\"loco\\\" WHERE \\\"loco\\\".\\\"id\\\" >= 1\"\n        );\n    }\n\n    #[test]\n    fn condition_lt() {\n        let query_str = test_db::Entity::find()\n            .select_only()\n            .column(test_db::Column::Id)\n            .filter(condition().lt(test_db::Column::Id, 1).build())\n            .build(sea_orm::DatabaseBackend::Postgres)\n            .to_string();\n\n        assert_eq!(\n            query_str,\n            \"SELECT \\\"loco\\\".\\\"id\\\" FROM \\\"loco\\\" WHERE \\\"loco\\\".\\\"id\\\" < 1\"\n        );\n    }\n\n    #[test]\n    fn condition_lte() {\n        let query_str = test_db::Entity::find()\n            .select_only()\n            .column(test_db::Column::Id)\n            .filter(condition().lte(test_db::Column::Id, 1).build())\n            .build(sea_orm::DatabaseBackend::Postgres)\n            .to_string();\n\n        assert_eq!(\n            query_str,\n            \"SELECT \\\"loco\\\".\\\"id\\\" FROM \\\"loco\\\" WHERE \\\"loco\\\".\\\"id\\\" <= 1\"\n        );\n    }\n\n    #[test]\n    fn condition_between() {\n        let query_str = test_db::Entity::find()\n            .select_only()\n            .column(test_db::Column::Id)\n            .filter(condition().between(test_db::Column::Id, 1, 2).build())\n            .build(sea_orm::DatabaseBackend::Postgres)\n            .to_string();\n\n        assert_eq!(\n            query_str,\n            \"SELECT \\\"loco\\\".\\\"id\\\" FROM \\\"loco\\\" WHERE \\\"loco\\\".\\\"id\\\" BETWEEN 1 AND 2\"\n        );\n    }\n\n    #[test]\n    fn condition_not_between() {\n        let query_str = test_db::Entity::find()\n            .select_only()\n            .column(test_db::Column::Id)\n            .filter(condition().not_between(test_db::Column::Id, 1, 2).build())\n            .build(sea_orm::DatabaseBackend::Postgres)\n            .to_string();\n\n        assert_eq!(\n            query_str,\n            \"SELECT \\\"loco\\\".\\\"id\\\" FROM \\\"loco\\\" WHERE \\\"loco\\\".\\\"id\\\" NOT BETWEEN 1 AND 2\"\n        );\n    }\n\n    #[test]\n    fn condition_like() {\n        let query_str = test_db::Entity::find()\n            .select_only()\n            .column(test_db::Column::Id)\n            .filter(condition().like(test_db::Column::Name, \"%lo\").build())\n            .build(sea_orm::DatabaseBackend::Postgres)\n            .to_string();\n\n        assert_eq!(\n            query_str,\n            \"SELECT \\\"loco\\\".\\\"id\\\" FROM \\\"loco\\\" WHERE \\\"loco\\\".\\\"name\\\" LIKE '%lo'\"\n        );\n    }\n\n    #[test]\n    fn condition_not_like() {\n        let query_str = test_db::Entity::find()\n            .select_only()\n            .column(test_db::Column::Id)\n            .filter(condition().not_like(test_db::Column::Name, \"%lo%\").build())\n            .build(sea_orm::DatabaseBackend::Postgres)\n            .to_string();\n\n        assert_eq!(\n            query_str,\n            \"SELECT \\\"loco\\\".\\\"id\\\" FROM \\\"loco\\\" WHERE \\\"loco\\\".\\\"name\\\" NOT LIKE '%lo%'\"\n        );\n    }\n\n    #[test]\n    fn condition_starts_with() {\n        let query_str = test_db::Entity::find()\n            .select_only()\n            .column(test_db::Column::Id)\n            .filter(condition().starts_with(test_db::Column::Name, \"lo\").build())\n            .build(sea_orm::DatabaseBackend::Postgres)\n            .to_string();\n\n        assert_eq!(\n            query_str,\n            \"SELECT \\\"loco\\\".\\\"id\\\" FROM \\\"loco\\\" WHERE \\\"loco\\\".\\\"name\\\" LIKE 'lo%'\"\n        );\n    }\n\n    #[test]\n    fn condition_ends_with() {\n        let query_str = test_db::Entity::find()\n            .select_only()\n            .column(test_db::Column::Id)\n            .filter(condition().ends_with(test_db::Column::Name, \"lo\").build())\n            .build(sea_orm::DatabaseBackend::Postgres)\n            .to_string();\n\n        assert_eq!(\n            query_str,\n            \"SELECT \\\"loco\\\".\\\"id\\\" FROM \\\"loco\\\" WHERE \\\"loco\\\".\\\"name\\\" LIKE '%lo'\"\n        );\n    }\n\n    #[test]\n    fn condition_contains() {\n        let query_str = test_db::Entity::find()\n            .select_only()\n            .column(test_db::Column::Id)\n            .filter(condition().contains(test_db::Column::Name, \"lo\").build())\n            .build(sea_orm::DatabaseBackend::Postgres)\n            .to_string();\n\n        assert_eq!(\n            query_str,\n            \"SELECT \\\"loco\\\".\\\"id\\\" FROM \\\"loco\\\" WHERE \\\"loco\\\".\\\"name\\\" LIKE '%lo%'\"\n        );\n    }\n\n    #[test]\n    fn condition_is_null() {\n        let query_str = test_db::Entity::find()\n            .select_only()\n            .column(test_db::Column::Id)\n            .filter(condition().is_null(test_db::Column::Name).build())\n            .build(sea_orm::DatabaseBackend::Postgres)\n            .to_string();\n\n        assert_eq!(\n            query_str,\n            \"SELECT \\\"loco\\\".\\\"id\\\" FROM \\\"loco\\\" WHERE \\\"loco\\\".\\\"name\\\" IS NULL\"\n        );\n    }\n\n    #[test]\n    fn condition_is_not_null() {\n        let query_str = test_db::Entity::find()\n            .select_only()\n            .column(test_db::Column::Id)\n            .filter(condition().is_not_null(test_db::Column::Name).build())\n            .build(sea_orm::DatabaseBackend::Postgres)\n            .to_string();\n\n        assert_eq!(\n            query_str,\n            \"SELECT \\\"loco\\\".\\\"id\\\" FROM \\\"loco\\\" WHERE \\\"loco\\\".\\\"name\\\" IS NOT NULL\"\n        );\n    }\n\n    #[test]\n    fn condition_is_in() {\n        let query_str = test_db::Entity::find()\n            .select_only()\n            .column(test_db::Column::Id)\n            .filter(condition().is_in(test_db::Column::Id, [1]).build())\n            .build(sea_orm::DatabaseBackend::Postgres)\n            .to_string();\n\n        assert_eq!(\n            query_str,\n            \"SELECT \\\"loco\\\".\\\"id\\\" FROM \\\"loco\\\" WHERE \\\"loco\\\".\\\"id\\\" IN (1)\"\n        );\n    }\n\n    #[test]\n    fn condition_is_not_in() {\n        let query_str = test_db::Entity::find()\n            .select_only()\n            .column(test_db::Column::Id)\n            .filter(condition().is_not_in(test_db::Column::Id, [1]).build())\n            .build(sea_orm::DatabaseBackend::Postgres)\n            .to_string();\n\n        assert_eq!(\n            query_str,\n            \"SELECT \\\"loco\\\".\\\"id\\\" FROM \\\"loco\\\" WHERE \\\"loco\\\".\\\"id\\\" NOT IN (1)\"\n        );\n    }\n}\n"
  },
  {
    "path": "src/model/query/mod.rs",
    "content": "mod dsl;\nmod paginate;\n\npub use dsl::*;\npub use paginate::*;\n"
  },
  {
    "path": "src/model/query/paginate/mod.rs",
    "content": "use sea_orm::{prelude::*, Condition, DatabaseConnection, EntityTrait, QueryFilter, SelectorTrait};\nuse serde::{Deserialize, Serialize};\n\n/// Set the default pagination page size.\nconst fn default_page_size() -> u64 {\n    25\n}\n\n/// Set the default pagination page.\nconst fn default_page() -> u64 {\n    1\n}\n\n/// Structure representing the pagination query parameters.\n/// This struct allows to get the struct parameters from the query parameters.\n///\n/// # Example\n///\n/// ```\n/// use serde::{Deserialize, Serialize};\n/// use loco_rs::prelude::model::*;\n///\n/// #[derive(Debug, Deserialize)]\n/// pub struct ListQueryParams {\n///     pub title: Option<String>,\n///     pub content: Option<String>,\n///     #[serde(flatten)]\n///     pub pagination: query::PaginationQuery,\n/// }\n/// ````\n#[derive(Debug, Deserialize, Serialize)]\npub struct PaginationQuery {\n    #[serde(\n        default = \"default_page_size\",\n        rename = \"page_size\",\n        deserialize_with = \"deserialize_pagination_filter\"\n    )]\n    pub page_size: u64,\n    #[serde(\n        default = \"default_page\",\n        rename = \"page\",\n        deserialize_with = \"deserialize_pagination_filter\"\n    )]\n    pub page: u64,\n}\n\nimpl PaginationQuery {\n    #[must_use]\n    pub fn page(page: u64) -> Self {\n        Self {\n            page,\n            ..Default::default()\n        }\n    }\n}\n\n/// Default implementation for `PaginationQuery`.\nimpl Default for PaginationQuery {\n    fn default() -> Self {\n        Self {\n            page_size: default_page_size(),\n            page: default_page(),\n        }\n    }\n}\n\n/// Deserialize pagination filter from string to u64 following a bug in\n/// `serde_urlencoded`.\nfn deserialize_pagination_filter<'de, D>(deserializer: D) -> Result<u64, D::Error>\nwhere\n    D: serde::Deserializer<'de>,\n{\n    let s: String = Deserialize::deserialize(deserializer)?;\n    s.parse().map_err(serde::de::Error::custom)\n}\n\n#[derive(Debug)]\npub struct PageResponse<T> {\n    pub page: Vec<T>,\n    pub total_pages: u64,\n    pub total_items: u64,\n}\n\nuse crate::Result as LocoResult;\n\n/// Paginate function for fetching paginated data from the database.\n///\n/// # Examples\n///\n/// Without conditions\n/// ```\n/// use loco_rs::tests_cfg::db;\n/// use sea_orm::{EntityTrait, QueryFilter, QuerySelect, QueryTrait};\n/// use loco_rs::prelude::*;\n///\n/// async fn example() {\n///     let db = db::dummy_connection().await;\n///     let pagination_query = query::PaginationQuery {\n///         page_size: 100,\n///         page: 1,\n///     };\n///     \n///     let res = query::paginate(&db, db::test_db::Entity::find(), None, &pagination_query).await;\n/// }\n/// ````\n/// With conditions\n/// ```\n/// use loco_rs::tests_cfg::db;\n/// use sea_orm::{EntityTrait, QueryFilter, QuerySelect, QueryTrait};\n/// use loco_rs::prelude::*;\n///\n/// async fn example() {\n///     let db = db::dummy_connection().await;\n///     let pagination_query = query::PaginationQuery {\n///         page_size: 100,\n///         page: 1,\n///     };\n///     let condition = query::condition().contains(db::test_db::Column::Name, \"loco\").build();\n///     let res = query::paginate(&db, db::test_db::Entity::find(), Some(condition), &pagination_query).await;\n/// }\n/// ````\n/// With Order By\n/// ```\n/// use loco_rs::tests_cfg::db;\n/// use sea_orm::{EntityTrait, QueryFilter, QuerySelect, QueryTrait, sea_query::Order, QueryOrder};\n/// use loco_rs::prelude::*;\n///\n/// async fn example() {\n///     let db = db::dummy_connection().await;\n///     let pagination_query = query::PaginationQuery {\n///         page_size: 100,\n///         page: 1,\n///     };\n///     \n///     let condition = query::condition().contains(db::test_db::Column::Name, \"loco\").build();\n///     let entity = db::test_db::Entity::find().order_by(db::test_db::Column::Name, Order::Desc);\n///     let res = query::paginate(&db, entity, Some(condition), &pagination_query).await;\n/// }\n/// ````\n///\n/// # Errors\n///\n/// Returns a `LocoResult` indicating any errors that occur\n/// during pagination.\npub async fn paginate<E>(\n    db: &DatabaseConnection,\n    entity: Select<E>,\n    condition: Option<Condition>,\n    pagination_query: &PaginationQuery,\n) -> LocoResult<PageResponse<E::Model>>\nwhere\n    E: EntityTrait,\n    <E as EntityTrait>::Model: Sync,\n{\n    let page = pagination_query.page.saturating_sub(1);\n    let entity = if let Some(condition) = condition {\n        entity.filter(condition)\n    } else {\n        entity\n    };\n\n    let query = entity.paginate(db, pagination_query.page_size);\n    let total_pages_and_items = query.num_items_and_pages().await?;\n    let page: Vec<<E as EntityTrait>::Model> = query.fetch_page(page).await?;\n\n    let paginated_response = PageResponse {\n        page,\n        total_pages: total_pages_and_items.number_of_pages,\n        total_items: total_pages_and_items.number_of_items,\n    };\n\n    Ok(paginated_response)\n}\n\n/// Fetching a page from a selector.\n///\n/// # Examples\n///\n/// From Entity\n/// ```\n/// use loco_rs::tests_cfg::db;\n/// use sea_orm::{EntityTrait, QueryFilter, QuerySelect, QueryTrait};\n/// use loco_rs::prelude::*;\n///\n/// async fn example() {\n///     let db = db::dummy_connection().await;\n///     let pagination_query = query::PaginationQuery {\n///         page_size: 100,\n///         page: 1,\n///     };\n///     let res = query::fetch_page(&db, db::test_db::Entity::find(), &query::PaginationQuery::page(2)).await;\n/// }\n/// ``````\n///\n/// # Errors\n///\n/// Returns a `LocoResult` indicating any errors that occur\n/// during the fetch.\npub async fn fetch_page<'db, C, S>(\n    db: &'db C,\n    selector: S,\n    pagination_query: &PaginationQuery,\n) -> LocoResult<PageResponse<<<S as PaginatorTrait<'db, C>>::Selector as SelectorTrait>::Item>>\nwhere\n    C: ConnectionTrait + Sync,\n    S: PaginatorTrait<'db, C> + Send,\n{\n    let page = pagination_query.page.saturating_sub(1);\n\n    let query = selector.paginate(db, pagination_query.page_size);\n    let total_pages_and_items = query.num_items_and_pages().await?;\n    let page = query.fetch_page(page).await?;\n\n    Ok(PageResponse {\n        page,\n        total_pages: total_pages_and_items.number_of_pages,\n        total_items: total_pages_and_items.number_of_items,\n    })\n}\n"
  },
  {
    "path": "src/prelude.rs",
    "content": "pub use async_trait::async_trait;\npub use axum::{\n    debug_handler,\n    extract::{Form, Multipart, Path, Query, State},\n    response::{IntoResponse, Response},\n    routing::{delete, get, head, options, patch, post, put, trace},\n};\npub use axum_extra::extract::cookie;\npub use chrono::NaiveDateTime as DateTime;\npub use include_dir::{include_dir, Dir};\n// some types required for controller generators\n#[cfg(feature = \"with-db\")]\npub use sea_orm::prelude::{Date, DateTimeUtc, DateTimeWithTimeZone, Decimal, Uuid};\n#[cfg(feature = \"with-db\")]\npub use sea_orm::{\n    ActiveModelBehavior, ActiveModelTrait, ActiveValue, ColumnTrait, ConnectionTrait,\n    DatabaseConnection, DbErr, EntityTrait, IntoActiveModel, ModelTrait, QueryFilter, Set,\n    TransactionTrait,\n};\n// sugar for controller views to use `data!({\"item\": ..})` instead of `json!`\npub use serde_json::json as data;\n\n#[cfg(feature = \"auth_jwt\")]\npub use crate::controller::extractor::auth;\npub use crate::controller::extractor::{\n    shared_store::SharedStore,\n    validate::{JsonValidate, JsonValidateWithMessage},\n};\n#[cfg(feature = \"with-db\")]\npub use crate::model::{query, Authenticable, ModelError, ModelResult};\npub use crate::{\n    app::{AppContext, Initializer},\n    bgworker::{BackgroundWorker, Queue},\n    controller::{\n        bad_request, format,\n        middleware::{\n            format::{Format, RespondTo},\n            remote_ip::RemoteIP,\n        },\n        not_found, unauthorized,\n        views::{engines::TeraView, ViewEngine, ViewRenderer},\n        Json, Routes,\n    },\n    errors::Error,\n    mailer,\n    mailer::Mailer,\n    task::{self, Task, TaskInfo},\n    validation::{self, Validatable, ValidatorTrait},\n    Result,\n};\npub use validator::Validate;\n#[cfg(feature = \"with-db\")]\npub mod model {\n    pub use crate::model::query;\n}\n#[cfg(feature = \"testing\")]\npub use crate::testing::prelude::*;\n"
  },
  {
    "path": "src/scheduler.rs",
    "content": "//! # Scheduler Module\n//! TBD\n\nuse std::{\n    collections::HashMap,\n    fmt, io,\n    path::{Path, PathBuf},\n    sync::OnceLock,\n    time::{Duration, Instant},\n};\n\nuse regex::Regex;\nuse serde::{Deserialize, Serialize};\nuse tokio_cron_scheduler::{JobScheduler, JobSchedulerError};\nuse uuid::Uuid;\n\nuse crate::{app::Hooks, environment::Environment, task::Tasks};\n\nstatic RE_IS_CRON_SYNTAX: OnceLock<Regex> = OnceLock::new();\n\nfn get_re_is_cron_syntax() -> &'static Regex {\n    RE_IS_CRON_SYNTAX.get_or_init(|| Regex::new(r\"^[\\*\\d]\").unwrap())\n}\n\n/// Errors that may occur while operating the scheduler.\n#[derive(thiserror::Error, Debug)]\npub enum Error {\n    #[error(\"schedulers not configured\")]\n    Empty,\n\n    #[error(\"task `{0}` not found\")]\n    TaskNotFound(String),\n\n    #[error(\"Scheduler config file not found in path: '{}'\", path.display())]\n    ConfigNotFound { path: PathBuf, error: io::Error },\n\n    #[error(\"Invalid scheduler config schema. err: '{}'\", error.as_display())]\n    InvalidConfigSchema { error: serde_yaml::Error },\n\n    #[error(\"Invalid cron {cron}. err: '{}'\", error.as_display())]\n    InvalidCronSyntax { cron: String, error: String },\n\n    #[error(transparent)]\n    Question(#[from] JobSchedulerError),\n\n    #[error(transparent)]\n    IO(#[from] std::io::Error),\n}\n\n/// Result type used in the module, with a custom error type.\npub type Result<T, E = Error> = std::result::Result<T, E>;\n\n/// Configuration structure for the scheduler.\n#[derive(Clone, Debug, Serialize, Deserialize)]\n#[serde(deny_unknown_fields)]\npub struct Config {\n    /// A list of jobs to be scheduled.\n    pub jobs: HashMap<String, Job>,\n    /// The default output setting for the jobs.\n    #[serde(default)]\n    pub output: Output,\n}\n\n/// Representing a single job in the scheduler.\n#[derive(Debug, Clone, Serialize, Deserialize)]\n#[serde(deny_unknown_fields)]\npub struct Job {\n    /// The command to run.\n    /// In case of task: it should be a task name and also task arguments\n    pub run: String,\n    #[serde(default)]\n    pub shell: bool,\n    #[serde(default)]\n    pub run_on_start: bool,\n    #[serde(rename = \"schedule\")]\n    /// The cron expression defining the job's schedule.\n    ///\n    /// The format is as follows:\n    /// sec   min   hour   day of month   month   day of week   year\n    /// * *     *      *              *       *             *\n    pub cron: String,\n    /// Tags for tagging the job.\n    pub tags: Option<Vec<String>>,\n    /// Output settings for the job.\n    pub output: Option<Output>,\n}\n\nimpl fmt::Display for Scheduler {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        writeln!(\n            f,\n            \"#      job_name       run_on_start      schedule               tags               run\"\n        )?;\n\n        let mut job_names: Vec<&String> = self.jobs.keys().collect();\n        job_names.sort();\n\n        for (index, &job_name) in job_names.iter().enumerate() {\n            if let Some(job) = self.jobs.get(job_name) {\n                writeln!(\n                    f,\n                    \"{:<6} {:<15} {:<12} {:<22} {:<18} {:?}\",\n                    index + 1,\n                    job_name,\n                    job.run_on_start,\n                    job.cron,\n                    job.tags\n                        .as_ref()\n                        .map_or_else(|| \"-\".to_string(), |tags| tags.join(\", \")),\n                    job.run,\n                )?;\n            }\n        }\n\n        Ok(())\n    }\n}\n\n/// Representing the scheduler itself.\n#[derive(Clone, Debug)]\npub struct Scheduler {\n    pub jobs: HashMap<String, Job>,\n    binary_path: PathBuf,\n    default_output: Output,\n    environment: Environment,\n}\n\n/// Specification used to filter all scheduler job with the given Spec.\n#[derive(Debug)]\npub struct Spec {\n    pub name: Option<String>,\n    pub tag: Option<String>,\n}\n\n/// Enum representing the scheduler job output.\n#[derive(Clone, Default, Debug, Serialize, Deserialize)]\npub enum Output {\n    /// Silent output, the STDOUT or STDERR of the job will not view out.\n    #[serde(rename = \"silent\")]\n    Silent,\n    /// The STDOUT or STDERR of the job will view propagated\n    #[default]\n    #[serde(rename = \"stdout\")]\n    STDOUT,\n}\n\n/// Structure representing the job command.\n#[derive(Clone, Debug)]\npub struct JobDescription {\n    /// The command to execute.\n    pub command: String,\n    /// The output setting for the job.\n    pub output: Output,\n    /// The environment in which the job will run.\n    pub environment: Environment,\n}\n\nimpl Job {\n    /// Prepares the command for execution based on the job's configuration.\n    #[must_use]\n    pub fn prepare_command(\n        &self,\n        binary_path: &Path,\n        default_output: &Output,\n        environment: &Environment,\n    ) -> JobDescription {\n        let command = if self.shell {\n            self.run.clone()\n        } else {\n            [\n                binary_path.display().to_string(),\n                \"task\".to_string(),\n                self.run.clone(),\n            ]\n            .join(\" \")\n        };\n\n        JobDescription {\n            command,\n            output: self\n                .output\n                .clone()\n                .unwrap_or_else(|| default_output.clone()),\n            environment: environment.clone(),\n        }\n    }\n}\n\nimpl JobDescription {\n    /// Executes the job command and returns the output.\n    ///\n    /// # Errors\n    ///\n    /// In addition to all the IO errors possible\n    pub fn run(&self) -> io::Result<std::process::Output> {\n        tracing::info!(command = &self.command, \"execute job command\");\n        let mut exec_job =\n            duct_sh::sh_dangerous(&self.command).env(\"LOCO_ENV\", self.environment.to_string());\n        exec_job = match self.output {\n            Output::Silent => exec_job.stdout_null().stderr_null(),\n            Output::STDOUT => exec_job,\n        };\n\n        exec_job.run()\n    }\n}\n\nimpl Scheduler {\n    /// Creates a new scheduler instance from the given configuration file.\n    ///\n    /// # Errors\n    ///\n    /// When could not parse the given file content into a [`Config`] struct.\n    pub fn from_config<H: Hooks>(config: &Path, environment: &Environment) -> Result<Self> {\n        let config_str =\n            std::fs::read_to_string(config).map_err(|error| Error::ConfigNotFound {\n                path: config.to_path_buf(),\n                error,\n            })?;\n\n        let config: Config = serde_yaml::from_str(&config_str)\n            .map_err(|error| Error::InvalidConfigSchema { error })?;\n\n        Self::new::<H>(&config, environment)\n    }\n\n    /// Creates a new scheduler instance from the provided configuration data.\n    ///\n    /// When creating a new scheduler instance all register task should be\n    /// loaded for validate the given configuration.\n    ///\n    /// # Errors\n    ///\n    /// When there is not job in the given config\n    pub fn new<H: Hooks>(data: &Config, environment: &Environment) -> Result<Self> {\n        let mut tasks = Tasks::default();\n        H::register_tasks(&mut tasks);\n\n        let mut jobs = HashMap::new();\n        for (job_name, job) in &data.jobs {\n            if job.shell {\n                jobs.insert(job_name.clone(), job.clone());\n            } else {\n                let task_name = job.run.split_whitespace().next().unwrap_or(\"\");\n                if tasks.names().iter().any(|name| name.as_str() == task_name) {\n                    jobs.insert(job_name.clone(), job.clone());\n                } else {\n                    return Err(Error::TaskNotFound(task_name.to_string()));\n                }\n            }\n        }\n\n        if jobs.is_empty() {\n            return Err(Error::Empty);\n        }\n\n        Ok(Self {\n            jobs,\n            binary_path: std::env::current_exe()?,\n            default_output: data.output.clone(),\n            environment: environment.clone(),\n        })\n    }\n\n    /// Filters the scheduler's jobs based on the provided specification.\n    #[must_use]\n    pub fn by_spec(self, include_jobs: &Spec) -> Self {\n        let jobs = self\n            .jobs\n            .into_iter()\n            .filter(|(job_name, job)| {\n                if let Some(name) = &include_jobs.name {\n                    return name == job_name;\n                }\n\n                if let Some(tag) = &include_jobs.tag {\n                    if let Some(job_tags) = &job.tags {\n                        return job_tags.contains(tag);\n                    }\n                }\n\n                true\n            })\n            .collect::<HashMap<String, Job>>();\n\n        Self { jobs, ..self }\n    }\n\n    /// Runs the scheduled jobs according to their cron expressions.\n    ///\n    /// # Errors\n    ///\n    /// When could not add job to the scheduler\n    pub async fn run(self) -> Result<()> {\n        let mut sched = JobScheduler::new().await?;\n\n        for (job_name, job) in &self.jobs {\n            let job_description =\n                job.prepare_command(&self.binary_path, &self.default_output, &self.environment);\n\n            let cron_syntax = if get_re_is_cron_syntax().is_match(&job.cron) {\n                job.cron.clone()\n            } else {\n                english_to_cron::str_cron_syntax(&job.cron).map_err(|err| {\n                    Error::InvalidCronSyntax {\n                        cron: job.cron.clone(),\n                        error: err.to_string(),\n                    }\n                })?\n            };\n\n            if job.run_on_start {\n                let job_description = job_description.clone();\n                let job_name = job_name.clone();\n                sched\n                    .add(tokio_cron_scheduler::Job::new_one_shot_async(\n                        Duration::from_secs(0),\n                        move |uuid, _l| {\n                            let job_description = job_description.clone();\n                            let job_name = job_name.clone();\n                            Box::pin(async move {\n                                execute_job(job_name.as_str(), uuid, &job_description);\n                            })\n                        },\n                    )?)\n                    .await?;\n            }\n\n            let job_name = job_name.clone();\n            sched\n                .add(tokio_cron_scheduler::Job::new_async(\n                    cron_syntax.as_str(),\n                    move |uuid, mut _l| {\n                        let job_description = job_description.clone();\n                        let job_name = job_name.clone();\n                        Box::pin(async move {\n                            execute_job(job_name.as_str(), uuid, &job_description);\n                        })\n                    },\n                )?)\n                .await?;\n        }\n\n        sched.start().await?;\n\n        tokio::signal::ctrl_c().await?;\n        sched.shutdown().await?;\n\n        Ok(())\n    }\n}\n\nfn execute_job(job_name: &str, uuid: Uuid, job_description: &JobDescription) {\n    let task_span = tracing::span!(\n        tracing::Level::DEBUG,\n        \"run_job\",\n        job_name,\n        job_id = ?uuid,\n    );\n    let start = Instant::now();\n    let _guard = task_span.enter();\n    match job_description.run() {\n        Ok(output) => {\n            tracing::debug!(\n                duration = ?start.elapsed(),\n                status_code = output.status.code(),\n                \"execute scheduler job finished\"\n            );\n        }\n        Err(err) => {\n            tracing::error!(\n                duration = ?start.elapsed(),\n                error = %err,\n                \"failed to execute scheduler job in sub process\"\n            );\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use insta::assert_debug_snapshot;\n    use rstest::rstest;\n    use tests_cfg::db::AppHook;\n    use tokio::time::{self, Duration};\n    use tree_fs::TreeBuilder;\n\n    use super::*;\n    use crate::tests_cfg;\n\n    fn setup_scheduler_config() -> (Scheduler, tree_fs::Tree) {\n        let tree = TreeBuilder::default()\n            .add_file(\n                \"scheduler.yaml\",\n                r#\"\njobs:\n  print_task:\n    run: foo\n    schedule: \"*/5 * * * * *\"\n    tags:\n      - base\n      - echo\n\n  write_to_file:\n    run: \"echo loco >> ./scheduler.txt\"\n    shell: true\n    schedule: \"*/5 * * * * *\"\n    tags:\n      - base\n      - write\n\n  run_on_start_task:\n    run: \"echo \\\"Does this run on start?\\\" >> ./run_on_start.txt \"\n    shell: true\n    schedule: \"every 24 hours\"\n    run_on_start: true\n    tags:\n      - start\n\"#,\n            )\n            .create()\n            .expect(\"Failed to create test directory structure\");\n\n        let scheduler = Scheduler::from_config::<AppHook>(\n            &tree.root.join(\"scheduler.yaml\"),\n            &Environment::Development,\n        )\n        .expect(\"Failed to create scheduler from config\");\n\n        (scheduler, tree)\n    }\n\n    #[test]\n    pub fn can_display_scheduler() {\n        let (scheduler, _tree) = setup_scheduler_config();\n        assert_debug_snapshot!(format!(\"{scheduler}\"));\n    }\n\n    #[test]\n    pub fn can_load_from_config_local_config() {\n        let (_, _tree) = setup_scheduler_config();\n        // If we got here, the setup was successful\n        assert!(true);\n    }\n\n    #[tokio::test]\n    pub async fn can_load_from_env_config() {\n        let app_context = tests_cfg::app::get_app_context().await;\n        let scheduler = Scheduler::new::<AppHook>(\n            &app_context.config.scheduler.unwrap(),\n            &Environment::Development,\n        );\n\n        assert!(scheduler.is_ok());\n    }\n\n    #[test]\n    pub fn can_load_jobs_by_spec_tag_multiple_jobs() {\n        let (scheduler, _tree) = setup_scheduler_config();\n        let scheduler = scheduler.by_spec(&Spec {\n            name: None,\n            tag: Some(\"base\".to_string()),\n        });\n\n        assert_eq!(scheduler.jobs.len(), 2);\n    }\n\n    #[test]\n    pub fn can_load_jobs_by_spec_tag_single_jobs() {\n        let (scheduler, _tree) = setup_scheduler_config();\n        let scheduler = scheduler.by_spec(&Spec {\n            name: None,\n            tag: Some(\"echo\".to_string()),\n        });\n\n        assert_eq!(scheduler.jobs.len(), 1);\n        assert!(scheduler.jobs.contains_key(\"print_task\"));\n    }\n\n    #[test]\n    pub fn can_load_jobs_by_spec_with_job_name() {\n        let (scheduler, _tree) = setup_scheduler_config();\n        let scheduler = scheduler.by_spec(&Spec {\n            name: Some(\"write_to_file\".to_string()),\n            tag: None,\n        });\n\n        assert_eq!(scheduler.jobs.len(), 1);\n        assert!(scheduler.jobs.contains_key(\"write_to_file\"));\n    }\n\n    #[rstest]\n    #[case(\"shell\", \"echo loco\", true)]\n    #[case(\"task\", \"foo LOCO_ENV:test SCHEDULER:true\", false)]\n    pub fn can_prepare_command(#[case] test_name: &str, #[case] run: &str, #[case] shell: bool) {\n        let job = Job {\n            run: run.to_string(),\n            shell,\n            run_on_start: false,\n            cron: \"*/5 * * * * *\".to_string(),\n            tags: None,\n            output: None,\n        };\n\n        let prepare_command = job.prepare_command(\n            PathBuf::from(\"[BIN_PATH]\").as_path(),\n            &Output::STDOUT,\n            &Environment::Test,\n        );\n        assert_debug_snapshot!(\n            format!(\"can_prepare_command_[{test_name}]\"),\n            prepare_command\n        );\n    }\n\n    #[tokio::test]\n    pub async fn can_run() {\n        let (mut scheduler, _config_tree) = setup_scheduler_config();\n\n        let tree_fs = tree_fs::TreeBuilder::default()\n            .drop(true)\n            .add(\"scheduler.txt\", \"\")\n            .add(\"scheduler2.txt\", \"\")\n            .add(\"scheduler3.txt\", \"\")\n            .create()\n            .unwrap();\n\n        assert_eq!(\n            std::fs::read_to_string(tree_fs.root.join(\"scheduler.txt\"))\n                .unwrap()\n                .lines()\n                .count(),\n            0\n        );\n        assert_eq!(\n            std::fs::read_to_string(tree_fs.root.join(\"scheduler2.txt\"))\n                .unwrap()\n                .lines()\n                .count(),\n            0\n        );\n        assert_eq!(\n            std::fs::read_to_string(tree_fs.root.join(\"scheduler3.txt\"))\n                .unwrap()\n                .lines()\n                .count(),\n            0\n        );\n\n        scheduler.jobs = HashMap::from([\n            (\n                \"test\".to_string(),\n                Job {\n                    run: format!(\n                        \"echo loco >> {}\",\n                        tree_fs.root.join(\"scheduler.txt\").display()\n                    ),\n                    shell: true,\n                    run_on_start: false,\n                    cron: \"run every 1 second\".to_string(),\n                    tags: None,\n                    output: None,\n                },\n            ),\n            (\n                \"test_2\".to_string(),\n                Job {\n                    run: format!(\n                        \"echo loco >> {}\",\n                        tree_fs.root.join(\"scheduler2.txt\").display()\n                    ),\n                    shell: true,\n                    run_on_start: false,\n                    cron: \"* * * * * ? *\".to_string(),\n                    tags: None,\n                    output: None,\n                },\n            ),\n            (\n                \"test_3\".to_string(),\n                Job {\n                    run: format!(\n                        \"echo loco >> {}\",\n                        tree_fs.root.join(\"scheduler3.txt\").display()\n                    ),\n                    shell: true,\n                    run_on_start: true,\n                    cron: \"0 0 * * * * *\".to_string(),\n                    tags: None,\n                    output: None,\n                },\n            ),\n        ]);\n\n        let handle = tokio::spawn(async move {\n            scheduler.run().await.unwrap();\n        });\n\n        time::sleep(Duration::from_secs(5)).await;\n        handle.abort();\n\n        assert!(\n            std::fs::read_to_string(tree_fs.root.join(\"scheduler.txt\"))\n                .unwrap()\n                .lines()\n                .count()\n                >= 4\n        );\n        assert!(\n            std::fs::read_to_string(tree_fs.root.join(\"scheduler2.txt\"))\n                .unwrap()\n                .lines()\n                .count()\n                >= 4\n        );\n        assert_eq!(\n            std::fs::read_to_string(tree_fs.root.join(\"scheduler3.txt\"))\n                .unwrap()\n                .lines()\n                .count(),\n            1\n        );\n    }\n}\n"
  },
  {
    "path": "src/schema.rs",
    "content": "use heck::ToSnakeCase;\nuse sea_orm::{\n    sea_query::{\n        Alias, ColumnDef, Expr, Index, IntoIden, PgInterval, Table, TableAlterStatement,\n        TableCreateStatement, TableForeignKey,\n    },\n    ColumnType, ConnectionTrait, DbErr, ForeignKeyAction,\n};\npub use sea_orm_migration::schema::*;\nuse sea_orm_migration::{prelude::Iden, sea_query, SchemaManager};\n\n#[derive(Iden)]\nenum GeneralIds {\n    CreatedAt,\n    UpdatedAt,\n}\n\n/// Alter table\npub fn alter<T: IntoIden + 'static>(name: T) -> TableAlterStatement {\n    Table::alter().table(name).take()\n}\n\n/// Wrapping table schema creation.\npub fn table_auto_tz<T>(name: T) -> TableCreateStatement\nwhere\n    T: IntoIden + 'static,\n{\n    timestamps_tz(Table::create().table(name).if_not_exists().take())\n}\n\n// these two are just aliases, original types exist in seaorm already.\n\n#[must_use]\npub fn timestamps_tz(t: TableCreateStatement) -> TableCreateStatement {\n    let mut t = t;\n    t.col(timestamp_with_time_zone(GeneralIds::CreatedAt).default(Expr::current_timestamp()))\n        .col(timestamp_with_time_zone(GeneralIds::UpdatedAt).default(Expr::current_timestamp()));\n    t.take()\n}\n\n/// Create a nullable timestamptz column definition.\npub fn timestamptz_null<T>(name: T) -> ColumnDef\nwhere\n    T: IntoIden,\n{\n    ColumnDef::new(name)\n        .timestamp_with_time_zone()\n        .null()\n        .take()\n}\n\n/// Create a non-nullable timestamptz column definition.\npub fn timestamptz<T>(name: T) -> ColumnDef\nwhere\n    T: IntoIden,\n{\n    ColumnDef::new(name)\n        .timestamp_with_time_zone()\n        .not_null()\n        .take()\n}\n\n/// Create a non-nullable enum column definition.\npub fn enum_type<T>(name: T, enum_name: &str) -> ColumnDef\nwhere\n    T: IntoIden,\n{\n    ColumnDef::new(name)\n        .enumeration::<Alias, Alias, Vec<Alias>>(Alias::new(enum_name), vec![])\n        .not_null()\n        .take()\n}\n\n/// Create a nullable enum column definition.\npub fn enum_type_null<T>(name: T, enum_name: &str) -> ColumnDef\nwhere\n    T: IntoIden,\n{\n    ColumnDef::new(name)\n        .enumeration::<Alias, Alias, Vec<Alias>>(Alias::new(enum_name), vec![])\n        .null()\n        .take()\n}\n\n/// Create a non-nullable enum column definition with default value.\n///\n/// # Example\n/// ```ignore\n/// create_table(m, \"users\", vec![\n///     (\"status\", ColType::EnumWithDefault(\"status_enum\".to_string(), vec![\"pending\".to_string(), \"active\".to_string()], \"pending\".to_string()))\n/// ], vec![]).await;\n/// ```\npub fn enum_type_with_default<T>(name: T, enum_name: &str, default_value: &str) -> ColumnDef\nwhere\n    T: IntoIden,\n{\n    ColumnDef::new(name)\n        .enumeration::<Alias, Alias, Vec<Alias>>(Alias::new(enum_name), vec![])\n        .not_null()\n        .default(Expr::val(default_value))\n        .take()\n}\n\n/// Create a nullable enum column definition with default value.\n///\n/// # Example\n/// ```ignore\n/// create_table(m, \"users\", vec![\n///     (\"status\", ColType::EnumNullWithDefault(\"status_enum\".to_string(), vec![\"pending\".to_string(), \"active\".to_string()], \"pending\".to_string()))\n/// ], vec![]).await;\n/// ```\npub fn enum_type_null_with_default<T>(name: T, enum_name: &str, default_value: &str) -> ColumnDef\nwhere\n    T: IntoIden,\n{\n    ColumnDef::new(name)\n        .enumeration::<Alias, Alias, Vec<Alias>>(Alias::new(enum_name), vec![])\n        .null()\n        .default(Expr::val(default_value))\n        .take()\n}\n\n/// Check if an enum type already exists in the database\nasync fn check_enum_exists(m: &SchemaManager<'_>, enum_name: &str) -> Result<bool, DbErr> {\n    match m.get_database_backend() {\n        sea_orm::DatabaseBackend::Postgres => {\n            let query = format!(\n                \"SELECT EXISTS (\n                    SELECT 1 FROM pg_type \n                    WHERE typname = '{enum_name}' \n                    AND typtype = 'e'\n                )\"\n            );\n\n            let result = m\n                .get_connection()\n                .query_one(sea_orm::Statement::from_string(\n                    sea_orm::DatabaseBackend::Postgres,\n                    query,\n                ))\n                .await?;\n\n            Ok(result.is_some_and(|row| row.try_get::<bool>(\"\", \"exists\").unwrap_or(false)))\n        }\n        sea_orm::DatabaseBackend::Sqlite => {\n            // SQLite doesn't have native enum types, so we'll always return false\n            // to allow creation of enum-like behavior through CHECK constraints\n            Ok(false)\n        }\n        sea_orm::DatabaseBackend::MySql => {\n            // MySQL doesn't support enums in the same way, so we'll always return false\n            Ok(false)\n        }\n    }\n}\n\n#[derive(Debug)]\npub enum ColType {\n    PkAuto,\n    PkUuid,\n    CharLen(u32),\n    CharLenWithDefault(u32, char),\n    CharLenNull(u32),\n    CharLenUniq(u32),\n    Char,\n    CharWithDefault(char),\n    CharNull,\n    CharUniq,\n    StringLen(u32),\n    StringLenWithDefault(u32, String),\n    StringLenNull(u32),\n    StringLenUniq(u32),\n    String,\n    StringWithDefault(String),\n    StringNull,\n    StringUniq,\n    Text,\n    TextWithDefault(String),\n    TextNull,\n    TextUniq,\n    Integer,\n    IntegerWithDefault(i32),\n    IntegerNull,\n    IntegerUniq,\n    Unsigned,\n    UnsignedWithDefault(u32),\n    UnsignedNull,\n    UnsignedUniq,\n    SmallUnsigned,\n    SmallUnsignedWithDefault(u16),\n    SmallUnsignedNull,\n    SmallUnsignedUniq,\n    BigUnsigned,\n    BigUnsignedWithDefault(u64),\n    BigUnsignedNull,\n    BigUnsignedUniq,\n    SmallInteger,\n    SmallIntegerWithDefault(i16),\n    SmallIntegerNull,\n    SmallIntegerUniq,\n    BigInteger,\n    BigIntegerWithDefault(i64),\n    BigIntegerNull,\n    BigIntegerUniq,\n    Decimal,\n    DecimalWithDefault(f64),\n    DecimalNull,\n    DecimalUniq,\n    DecimalLen(u32, u32),\n    DecimalLenWithDefault(u32, u32, f64),\n    DecimalLenNull(u32, u32),\n    DecimalLenUniq(u32, u32),\n    Float,\n    FloatWithDefault(f32),\n    FloatNull,\n    FloatUniq,\n    Double,\n    DoubleWithDefault(f64),\n    DoubleNull,\n    DoubleUniq,\n    Boolean,\n    BooleanWithDefault(bool),\n    BooleanNull,\n    Date,\n    DateWithDefault(String),\n    DateNull,\n    DateUniq,\n    DateTime,\n    DateTimeWithDefault(String),\n    DateTimeNull,\n    DateTimeUniq,\n    Time,\n    TimeWithDefault(String),\n    TimeNull,\n    TimeUniq,\n    Interval(Option<PgInterval>, Option<u32>),\n    IntervalNull(Option<PgInterval>, Option<u32>),\n    IntervalUniq(Option<PgInterval>, Option<u32>),\n    Binary,\n    BinaryNull,\n    BinaryUniq,\n    BinaryLen(u32),\n    BinaryLenNull(u32),\n    BinaryLenUniq(u32),\n    VarBinary(u32),\n    VarBinaryNull(u32),\n    VarBinaryUniq(u32),\n    TimestampWithTimeZone,\n    TimestampWithTimeZoneWithDefault(String),\n    TimestampWithTimeZoneNull,\n    Json,\n    JsonNull,\n    JsonUniq,\n    JsonBinary,\n    JsonBinaryNull,\n    JsonBinaryUniq,\n    Blob,\n    BlobNull,\n    BlobUniq,\n    Money,\n    MoneyWithDefault(f64),\n    MoneyNull,\n    MoneyUniq,\n    Uuid,\n    UuidNull,\n    UuidUniq,\n    UuidWithDefault(String),\n    UuidUniqWithDefault(String),\n    VarBitLen(u32),\n    VarBitLenNull(u32),\n    VarBitLenUniq(u32),\n    Array(ColumnType),\n    ArrayNull(ColumnType),\n    ArrayUniq(ColumnType),\n    // Enum types\n    Enum(String, Vec<String>),\n    EnumNull(String, Vec<String>),\n    EnumWithDefault(String, Vec<String>, String),\n    EnumNullWithDefault(String, Vec<String>, String),\n}\n\npub enum ArrayColType {\n    String,\n    Int,\n    BigInt,\n    Float,\n    Double,\n    Bool,\n}\n\nimpl ColType {\n    #[must_use]\n    #[allow(clippy::needless_pass_by_value)]\n    pub fn array(kind: ArrayColType) -> Self {\n        Self::Array(Self::array_col_type(&kind))\n    }\n\n    #[must_use]\n    #[allow(clippy::needless_pass_by_value)]\n    pub fn array_uniq(kind: ArrayColType) -> Self {\n        Self::ArrayUniq(Self::array_col_type(&kind))\n    }\n\n    #[must_use]\n    #[allow(clippy::needless_pass_by_value)]\n    pub fn array_null(kind: ArrayColType) -> Self {\n        Self::ArrayNull(Self::array_col_type(&kind))\n    }\n\n    fn array_col_type(kind: &ArrayColType) -> ColumnType {\n        match kind {\n            ArrayColType::String => ColumnType::string(None),\n            ArrayColType::Int => ColumnType::Integer,\n            ArrayColType::BigInt => ColumnType::BigInteger,\n            ArrayColType::Float => ColumnType::Float,\n            ArrayColType::Double => ColumnType::Double,\n            ArrayColType::Bool => ColumnType::Boolean,\n        }\n    }\n}\n\nimpl ColType {\n    #[allow(clippy::too_many_lines)]\n    fn to_def(&self, name: impl IntoIden) -> ColumnDef {\n        match self {\n            Self::PkAuto => pk_auto(name),\n            Self::PkUuid => pk_uuid(name),\n            Self::CharLen(len) => char_len(name, *len),\n            Self::CharLenNull(len) => char_len_null(name, *len),\n            Self::CharLenUniq(len) => char_len_uniq(name, *len),\n            Self::Char => char(name),\n            Self::CharNull => char_null(name),\n            Self::CharUniq => char_uniq(name),\n            Self::StringLen(len) => string_len(name, *len),\n            Self::StringLenNull(len) => string_len_null(name, *len),\n            Self::StringLenUniq(len) => string_len_uniq(name, *len),\n            Self::String => string(name),\n            Self::StringNull => string_null(name),\n            Self::StringUniq => string_uniq(name),\n            Self::Text => text(name),\n            Self::TextNull => text_null(name),\n            Self::TextUniq => text_uniq(name),\n            Self::Integer => integer(name),\n            Self::IntegerNull => integer_null(name),\n            Self::IntegerUniq => integer_uniq(name),\n            // Self::TinyInteger => tiny_integer(name),\n            // Self::TinyIntegerNull => tiny_integer_null(name),\n            // Self::TinyIntegerUniq => tiny_integer_uniq(name),\n            Self::Unsigned => unsigned(name),\n            Self::UnsignedNull => unsigned_null(name),\n            Self::UnsignedUniq => unsigned_uniq(name),\n            // Self::TinyUnsigned => tiny_unsigned(name),\n            // Self::TinyUnsignedNull => tiny_unsigned_null(name),\n            // Self::TinyUnsignedUniq => tiny_unsigned_uniq(name),\n            Self::SmallUnsigned => small_unsigned(name),\n            Self::SmallUnsignedNull => small_unsigned_null(name),\n            Self::SmallUnsignedUniq => small_unsigned_uniq(name),\n            Self::BigUnsigned => big_unsigned(name),\n            Self::BigUnsignedNull => big_unsigned_null(name),\n            Self::BigUnsignedUniq => big_unsigned_uniq(name),\n            Self::SmallInteger => small_integer(name),\n            Self::SmallIntegerNull => small_integer_null(name),\n            Self::SmallIntegerUniq => small_integer_uniq(name),\n            Self::BigInteger => big_integer(name),\n            Self::BigIntegerNull => big_integer_null(name),\n            Self::BigIntegerUniq => big_integer_uniq(name),\n            Self::Decimal => decimal(name),\n            Self::DecimalNull => decimal_null(name),\n            Self::DecimalUniq => decimal_uniq(name),\n            Self::DecimalLen(precision, scale) => decimal_len(name, *precision, *scale),\n            Self::DecimalLenNull(precision, scale) => decimal_len_null(name, *precision, *scale),\n            Self::DecimalLenUniq(precision, scale) => decimal_len_uniq(name, *precision, *scale),\n            Self::Float => float(name),\n            Self::FloatNull => float_null(name),\n            Self::FloatUniq => float_uniq(name),\n            Self::Double => double(name),\n            Self::DoubleNull => double_null(name),\n            Self::DoubleUniq => double_uniq(name),\n            Self::Boolean => boolean(name),\n            Self::BooleanNull => boolean_null(name),\n            // Self::Timestamp => timestamp(name),\n            // Self::TimestampNull => timestamp_null(name),\n            // Self::TimestampUniq => timestamp_uniq(name),\n            Self::Date => date(name),\n            Self::DateNull => date_null(name),\n            Self::DateUniq => date_uniq(name),\n            Self::DateTime => date_time(name),\n            Self::DateTimeNull => date_time_null(name),\n            Self::DateTimeUniq => date_time_uniq(name),\n            Self::Time => time(name),\n            Self::TimeNull => time_null(name),\n            Self::TimeUniq => time_uniq(name),\n            Self::Interval(ival, prec) => interval(name, ival.clone(), *prec),\n            Self::IntervalNull(ival, prec) => interval_null(name, ival.clone(), *prec),\n            Self::IntervalUniq(ival, prec) => interval_uniq(name, ival.clone(), *prec),\n            Self::Binary => binary(name),\n            Self::BinaryNull => binary_null(name),\n            Self::BinaryUniq => binary_uniq(name),\n            Self::BinaryLen(len) => binary_len(name, *len),\n            Self::BinaryLenNull(len) => binary_len_null(name, *len),\n            Self::BinaryLenUniq(len) => binary_len_uniq(name, *len),\n            Self::VarBinary(len) => var_binary(name, *len),\n            Self::VarBinaryNull(len) => var_binary_null(name, *len),\n            Self::VarBinaryUniq(len) => var_binary_uniq(name, *len),\n            Self::TimestampWithTimeZone => timestamptz(name),\n            Self::TimestampWithTimeZoneNull => timestamptz_null(name),\n            Self::Json => json(name),\n            Self::JsonNull => json_null(name),\n            Self::JsonUniq => json_uniq(name),\n            Self::JsonBinary => json_binary(name),\n            Self::JsonBinaryNull => json_binary_null(name),\n            Self::JsonBinaryUniq => json_binary_uniq(name),\n            Self::Blob => blob(name),\n            Self::BlobNull => blob_null(name),\n            Self::BlobUniq => blob_uniq(name),\n            Self::Money => money(name),\n            Self::MoneyNull => money_null(name),\n            Self::MoneyUniq => money_uniq(name),\n            Self::Uuid => uuid(name),\n            Self::UuidNull => uuid_null(name),\n            Self::UuidUniq => uuid_uniq(name),\n            Self::VarBitLen(len) => varbit(name, *len),\n            Self::VarBitLenNull(len) => varbit_null(name, *len),\n            Self::VarBitLenUniq(len) => varbit_uniq(name, *len),\n            Self::Array(kind) => array(name, kind.clone()),\n            Self::ArrayNull(kind) => array_null(name, kind.clone()),\n            Self::ArrayUniq(kind) => array_uniq(name, kind.clone()),\n            // Enum types\n            Self::Enum(enum_name, _) => enum_type(name, enum_name),\n            Self::EnumNull(enum_name, _) => enum_type_null(name, enum_name),\n            Self::EnumWithDefault(enum_name, _, default_value) => {\n                enum_type_with_default(name, enum_name, default_value)\n            }\n            Self::EnumNullWithDefault(enum_name, _, default_value) => {\n                enum_type_null_with_default(name, enum_name, default_value)\n            }\n            // defaults\n            Self::MoneyWithDefault(v) => money(name).default(*v).take(),\n            Self::IntegerWithDefault(v) => integer(name).default(*v).take(),\n            Self::UnsignedWithDefault(v) => unsigned(name).default(*v).take(),\n            Self::SmallUnsignedWithDefault(v) => small_unsigned(name).default(*v).take(),\n            Self::BigUnsignedWithDefault(v) => big_unsigned(name).default(*v).take(),\n            Self::SmallIntegerWithDefault(v) => small_integer(name).default(*v).take(),\n            Self::BigIntegerWithDefault(v) => big_integer(name).default(*v).take(),\n            Self::DecimalWithDefault(v) => decimal(name).default(*v).take(),\n            Self::DecimalLenWithDefault(p, s, v) => decimal_len(name, *p, *s).default(*v).take(),\n            Self::FloatWithDefault(v) => float(name).default(*v).take(),\n            Self::DoubleWithDefault(v) => double(name).default(*v).take(),\n            Self::BooleanWithDefault(v) => boolean(name).default(*v).take(),\n            Self::DateWithDefault(v) => date(name).default(v.clone()).take(),\n            Self::DateTimeWithDefault(v) => date_time(name).default(v.clone()).take(),\n            Self::TimeWithDefault(v) => time(name).default(v.clone()).take(),\n            Self::TimestampWithTimeZoneWithDefault(v) => {\n                timestamptz(name).default(v.clone()).take()\n            }\n            Self::CharWithDefault(v) => char(name).default(*v).take(),\n            Self::CharLenWithDefault(len, v) => char_len(name, *len).default(*v).take(),\n            Self::StringWithDefault(v) => string(name).default(v.clone()).take(),\n            Self::StringLenWithDefault(len, v) => string_len(name, *len).default(v.clone()).take(),\n            Self::TextWithDefault(v) => text(name).default(v.clone()).take(),\n            Self::UuidWithDefault(v) => uuid(name).default(Expr::cust(v.clone())).take(),\n            Self::UuidUniqWithDefault(v) => uuid_uniq(name).default(Expr::cust(v.clone())).take(),\n        }\n    }\n}\n\n///\n/// Create a table.\n/// ```ignore\n/// create_table(m, \"movies\", vec![\n///     (\"title\", ColType::String)\n/// ],\n/// vec![]\n/// )\n/// .await;\n/// ```\n///\n/// ```shell\n/// loco g migration CreateMovies title:string user:references\n/// loco g migration CreateMovies title:string user:references:admin_id\n/// ```\n/// # Errors\n/// fails when it fails\npub async fn create_table(\n    m: &SchemaManager<'_>,\n    table: &str,\n    cols: &[(&str, ColType)],\n    refs: &[(&str, &str)], // [(from_tbl, to_tbl), ...]\n) -> Result<(), DbErr> {\n    create_table_impl(m, table, cols, refs, false, true).await\n}\n\n///\n/// Create a join table. A join table has a composite primary key.\n/// ```ignore\n/// create_join_table(m, \"movies\", vec![\n///     (\"title\", ColType::String)\n/// ],\n/// vec![]\n/// )\n/// .await;\n/// ```\n///\n/// # Errors\n/// fails when it fails\npub async fn create_join_table(\n    m: &SchemaManager<'_>,\n    table: &str,\n    cols: &[(&str, ColType)],\n    refs: &[(&str, &str)], // [(from_tbl, to_tbl), ...]\n) -> Result<(), DbErr> {\n    create_table_impl(m, table, cols, refs, true, true).await\n}\n\n/// Create a table without automatic timestamps.\n/// This gives users full control over their table schema.\n/// ```ignore\n/// create_table_without_timestamps(m, \"movies\", vec![\n///     (\"title\", ColType::String)\n/// ],\n/// vec![]\n/// )\n/// .await;\n/// ```\n///\n/// ```shell\n/// loco g migration CreateMovies title:string user:references --without-timestamps\n/// ```\n/// # Errors\n/// fails when it fails\npub async fn create_table_without_timestamps(\n    m: &SchemaManager<'_>,\n    table: &str,\n    cols: &[(&str, ColType)],\n    refs: &[(&str, &str)], // [(from_tbl, to_tbl), ...]\n) -> Result<(), DbErr> {\n    create_table_impl(m, table, cols, refs, false, false).await\n}\n\n/// Create a join table without automatic timestamps.\n/// A join table has a composite primary key.\n/// ```ignore\n/// create_join_table_without_timestamps(m, \"movies\", vec![\n///     (\"title\", ColType::String)\n/// ],\n/// vec![]\n/// )\n/// .await;\n/// ```\n///\n/// # Errors\n/// fails when it fails\npub async fn create_join_table_without_timestamps(\n    m: &SchemaManager<'_>,\n    table: &str,\n    cols: &[(&str, ColType)],\n    refs: &[(&str, &str)], // [(from_tbl, to_tbl), ...]\n) -> Result<(), DbErr> {\n    create_table_impl(m, table, cols, refs, true, false).await\n}\n\nasync fn create_table_impl(\n    m: &SchemaManager<'_>,\n    table: &str,\n    cols: &[(&str, ColType)],\n    refs: &[(&str, &str)], // [(from_tbl, to_tbl), ...]\n    is_join: bool,\n    add_timestamps: bool, // New parameter to control timestamp addition\n) -> Result<(), DbErr> {\n    let nz_table = normalize_table(table);\n\n    // Create enum types automatically if they don't exist\n    let mut enum_types = std::collections::HashSet::new();\n    for (_, col_type) in cols {\n        match col_type {\n            ColType::Enum(enum_name, variants)\n            | ColType::EnumNull(enum_name, variants)\n            | ColType::EnumWithDefault(enum_name, variants, _)\n            | ColType::EnumNullWithDefault(enum_name, variants, _) => {\n                if !enum_types.contains(enum_name) {\n                    enum_types.insert(enum_name.clone());\n\n                    // Check if enum type already exists\n                    let enum_exists = check_enum_exists(m, enum_name).await?;\n\n                    if !enum_exists {\n                        // Create enum type with provided variants\n                        match m.get_database_backend() {\n                            sea_orm::DatabaseBackend::Postgres => {\n                                let variant_aliases: Vec<Alias> =\n                                    variants.iter().map(Alias::new).collect();\n                                m.create_type(\n                                    sea_query::extension::postgres::Type::create()\n                                        .as_enum(Alias::new(enum_name))\n                                        .values(variant_aliases)\n                                        .to_owned(),\n                                )\n                                .await?;\n                            }\n                            #[allow(clippy::match_same_arms)]\n                            sea_orm::DatabaseBackend::Sqlite => {\n                                // SQLite doesn't support native enum types\n                                // The enum behavior will be handled by the column definition\n                                // which will create a TEXT column with CHECK constraints\n                            }\n                            sea_orm::DatabaseBackend::MySql => {\n                                // MySql not supporting\n                            }\n                        }\n                    }\n                }\n            }\n            _ => {}\n        }\n    }\n\n    // Conditionally create table with or without timestamps\n    let mut stmt = if add_timestamps {\n        table_auto_tz(Alias::new(&nz_table))\n    } else {\n        Table::create()\n            .table(Alias::new(&nz_table))\n            .if_not_exists()\n            .take()\n    };\n\n    if is_join {\n        let mut idx = Index::create();\n        idx.name(format!(\"idx-{nz_table}-refs-pk\"))\n            .table(Alias::new(&nz_table));\n\n        for (from_tbl, ref_name) in refs {\n            let nz_from_table = normalize_table(from_tbl);\n            // in movies, user:references, creates a `user_id` field or what ever in\n            // `ref_name` if given\n            let nz_ref_name = if ref_name.is_empty() {\n                reference_id(&nz_from_table)\n            } else {\n                (*ref_name).to_string()\n            };\n            idx.col(Alias::new(nz_ref_name));\n        }\n        stmt.primary_key(&mut idx);\n    }\n\n    for (name, atype) in cols {\n        stmt.col(atype.to_def(Alias::new(*name)));\n    }\n\n    // user, None\n    // users, None\n    // user, admin_id\n    for (from_tbl, ref_name) in refs {\n        // Check for nullable reference\n        let (nz_from_table, is_nullable) = from_tbl.strip_suffix('?').map_or_else(\n            || (normalize_table(from_tbl), false),\n            |stripped| (normalize_table(stripped), true),\n        );\n        let nz_ref_name = if ref_name.is_empty() {\n            reference_id(&nz_from_table)\n        } else {\n            (*ref_name).to_string()\n        };\n        // Only add the column if it doesn't already exist in cols\n        if !cols.iter().any(|(col_name, _)| *col_name == nz_ref_name) {\n            let col_type = if is_nullable {\n                ColType::IntegerNull\n            } else {\n                ColType::Integer\n            };\n            stmt.col(col_type.to_def(Alias::new(&nz_ref_name)));\n        }\n        // Set FK actions based on nullability\n        let mut fk = sea_query::ForeignKey::create();\n        fk.name(format!(\"fk-{nz_from_table}-{nz_ref_name}-to-{nz_table}\"));\n        fk.from(Alias::new(&nz_table), Alias::new(&nz_ref_name));\n        fk.to(Alias::new(nz_from_table), Alias::new(\"id\"));\n        if is_nullable {\n            fk.on_delete(ForeignKeyAction::SetNull);\n            fk.on_update(ForeignKeyAction::NoAction);\n        } else {\n            fk.on_delete(ForeignKeyAction::Cascade);\n            fk.on_update(ForeignKeyAction::Cascade);\n        }\n        stmt.foreign_key(&mut fk);\n    }\n    m.create_table(stmt).await?;\n    Ok(())\n}\n\n/// person -> people, movies -> movie\nfn normalize_table(table: &str) -> String {\n    cruet::to_plural(table).to_snake_case()\n}\n\n/// users -> `user_id`\nfn reference_id(totbl: &str) -> String {\n    format!(\"{}_id\", cruet::to_singular(totbl).to_snake_case())\n}\n\n///\n/// Add a column to a table with a column type.\n///\n/// ```ignore\n/// add_column(m, \"movies\", \"title\", ColType::String).await;\n/// ```\n/// # Errors\n/// fails when it fails\npub async fn add_column(\n    m: &SchemaManager<'_>,\n    table: &str,\n    name: &str,\n    atype: ColType,\n) -> Result<(), DbErr> {\n    let nz_table = normalize_table(table);\n    m.alter_table(\n        alter(Alias::new(nz_table))\n            .add_column(atype.to_def(Alias::new(name)))\n            .to_owned(),\n    )\n    .await?;\n    Ok(())\n}\n\n///\n/// Drop a column from a table.\n///\n/// ```ignore\n/// drop_column(m, \"movies\", \"title\").await;\n/// ```\n/// # Errors\n/// fails when it fails\npub async fn remove_column(m: &SchemaManager<'_>, table: &str, name: &str) -> Result<(), DbErr> {\n    let nz_table = normalize_table(table);\n    m.alter_table(\n        alter(Alias::new(nz_table))\n            .drop_column(Alias::new(name))\n            .to_owned(),\n    )\n    .await?;\n    Ok(())\n}\n\n///\n/// Adds a reference. Reads \"movies belongs-to users\":\n/// ```ignore\n/// add_reference(m, \"movies\", \"users\").await;\n/// ```\n///\n/// # Errors\n/// fails when it fails\npub async fn add_reference(\n    m: &SchemaManager<'_>,\n    fromtbl: &str,\n    totbl: &str,\n    refname: &str,\n) -> Result<(), DbErr> {\n    // movies\n    let nz_fromtbl = normalize_table(fromtbl);\n    // users\n    let nz_totbl = normalize_table(totbl);\n    // user_id\n    let nz_ref_name = if refname.is_empty() {\n        reference_id(totbl)\n    } else {\n        refname.to_string()\n    };\n    let bk = m.get_database_backend();\n    let col = ColType::Integer.to_def(Alias::new(&nz_ref_name));\n    let fk = TableForeignKey::new()\n        // fk-movies-user_id-to-users\n        .name(format!(\"fk-{nz_fromtbl}-{nz_ref_name}-to-{nz_totbl}\"))\n        // from movies#user_id\n        .from_tbl(Alias::new(&nz_fromtbl))\n        .from_col(Alias::new(&nz_ref_name)) // xxx fix\n        // to users#id\n        .to_tbl(Alias::new(nz_totbl))\n        .to_col(Alias::new(\"id\"))\n        .on_delete(ForeignKeyAction::Cascade)\n        .on_update(ForeignKeyAction::Cascade)\n        .to_owned();\n    match bk {\n        sea_orm::DatabaseBackend::MySql | sea_orm::DatabaseBackend::Postgres => {\n            // from movies to users -> movies#user_id to users#id\n            m.alter_table(\n                alter(Alias::new(&nz_fromtbl))\n                    // add movies#user_id (the user_id column is new)\n                    .add_column(col.clone()) // XXX fix, totbl_id\n                    // add fk on movies#user_id\n                    .add_foreign_key(&fk)\n                    .to_owned(),\n            )\n            .await?;\n        }\n        sea_orm::DatabaseBackend::Sqlite => {\n            // from movies to users -> movies#user_id to users#id\n            m.alter_table(\n                alter(Alias::new(&nz_fromtbl))\n                    // add movies#user_id (the user_id column is new)\n                    .add_column(col.clone()) // XXX fix, totbl_id\n                    .to_owned(),\n            )\n            .await?;\n            // Per Rails 5.2, adding FK to existing table does nothing because\n            // sqlite will not allow it. FK in sqlite are applied only on table\n            // creation. more: https://www.bigbinary.com/blog/rails-6-adds-add_foreign_key-and-remove_foreign_key-for-sqlite3\n            // we comment it below leaving it for academic purposes.\n            /*\n                m.alter_table(\n                    alter(Alias::new(&nz_fromtbl))\n                        // add fk on movies#user_id\n                        .add_foreign_key(&fk)\n                        .to_owned(),\n                )\n                .await?;\n            */\n        }\n    }\n    Ok(())\n}\n\n///\n/// Removes a reference by constructing its name from the table names.\n/// ```ignore\n/// remove_reference(m, \"movies\", \"users\").await;\n/// ```\n///\n/// # Errors\n/// fails when it fails\npub async fn remove_reference(\n    m: &SchemaManager<'_>,\n    fromtbl: &str,\n    totbl: &str,\n    refname: &str,\n) -> Result<(), DbErr> {\n    // movies\n    let nz_fromtbl = normalize_table(fromtbl);\n    // users\n    let nz_totbl = normalize_table(totbl);\n    // user_id\n    let nz_ref_name = if refname.is_empty() {\n        reference_id(totbl)\n    } else {\n        refname.to_string()\n    };\n    let bk = m.get_database_backend();\n    match bk {\n        sea_orm::DatabaseBackend::MySql | sea_orm::DatabaseBackend::Postgres => {\n            // from movies to users -> movies#user_id to users#id\n            m.alter_table(\n                alter(Alias::new(&nz_fromtbl))\n                    .drop_foreign_key(\n                        // fk-movies-user_id-to-users\n                        Alias::new(format!(\"fk-{nz_fromtbl}-{nz_ref_name}-to-{nz_totbl}\")),\n                    )\n                    .to_owned(),\n            )\n            .await?;\n        }\n        sea_orm::DatabaseBackend::Sqlite => {\n            // Per Rails 5.2, removing FK on existing table does nothing because\n            // sqlite will not allow it.\n            // more: https://www.bigbinary.com/blog/rails-6-adds-add_foreign_key-and-remove_foreign_key-for-sqlite3\n        }\n    }\n    Ok(())\n}\n\n///\n/// Drop a table\n/// ```ignore\n/// drop_table(m, \"movies\").await;\n/// ```\n///\n/// # Errors\n/// fails when it fails\npub async fn drop_table(m: &SchemaManager<'_>, table: &str) -> Result<(), DbErr> {\n    let nz_table = normalize_table(table);\n    m.drop_table(Table::drop().table(Alias::new(nz_table)).to_owned())\n        .await\n}\n\n///\n/// Add enum values to an existing enum type\n/// ```ignore\n/// add_enum_values(m, \"status_enum\", vec![\"suspended\", \"cancelled\"]).await;\n/// ```\n///\n/// # Errors\n/// fails when it fails\npub async fn add_enum_values(\n    m: &SchemaManager<'_>,\n    enum_name: &str,\n    new_values: Vec<String>,\n) -> Result<(), DbErr> {\n    match m.get_database_backend() {\n        sea_orm::DatabaseBackend::Postgres => {\n            for value in new_values {\n                m.get_connection()\n                    .execute(sea_orm::Statement::from_string(\n                        sea_orm::DatabaseBackend::Postgres,\n                        format!(\"ALTER TYPE {enum_name} ADD VALUE '{value}'\"),\n                    ))\n                    .await?;\n            }\n        }\n        sea_orm::DatabaseBackend::Sqlite => {\n            // SQLite doesn't support native enums, values are handled by CHECK constraints\n            tracing::info!(\n                \"SQLite: Enum values are handled by CHECK constraints. No action needed.\"\n            );\n        }\n        sea_orm::DatabaseBackend::MySql => {\n            // MySQL handles enums differently\n            tracing::info!(\n                \"MySQL: Enum values are handled by column definition. No action needed.\"\n            );\n        }\n    }\n    Ok(())\n}\n\n///\n/// Drop an enum type completely\n/// ```ignore\n/// drop_enum_type(m, \"status_enum\").await;\n/// ```\n///\n/// # Errors\n/// fails when it fails\npub async fn drop_enum_type(m: &SchemaManager<'_>, enum_name: &str) -> Result<(), DbErr> {\n    match m.get_database_backend() {\n        sea_orm::DatabaseBackend::Postgres => {\n            // First check if the enum type exists\n            let enum_exists = check_enum_exists(m, enum_name).await?;\n            if !enum_exists {\n                tracing::info!(\"Enum type '{}' does not exist, skipping drop\", enum_name);\n                return Ok(());\n            }\n\n            // Try to drop the enum type with CASCADE to handle any remaining references\n            let query = format!(\"DROP TYPE IF EXISTS {enum_name} CASCADE\");\n            m.get_connection()\n                .execute(sea_orm::Statement::from_string(\n                    sea_orm::DatabaseBackend::Postgres,\n                    query,\n                ))\n                .await?;\n        }\n        _ => {\n            // SQLite/MySQL don't have native enum types\n            tracing::info!(\"No native enum type to drop for this database backend.\");\n        }\n    }\n    Ok(())\n}\n"
  },
  {
    "path": "src/snapshots/loco_rs__auth__tests__token expired.snap",
    "content": "---\nsource: src/auth.rs\nexpression: jwt.validate(&token)\n---\nErr(\n    Error(\n        ExpiredSignature,\n    ),\n)\n"
  },
  {
    "path": "src/snapshots/loco_rs__auth__tests__valid token.snap",
    "content": "---\nsource: src/auth.rs\nexpression: jwt.validate(&token)\n---\nOk(\n    TokenData {\n        header: Header {\n            typ: Some(\n                \"JWT\",\n            ),\n            alg: HS512,\n            cty: None,\n            jku: None,\n            jwk: None,\n            kid: None,\n            x5u: None,\n            x5c: None,\n            x5t: None,\n            x5t_s256: None,\n        },\n        claims: UserClaims {\n            pid: \"pid\",\n            exp: EXP,\n        },\n    },\n)\n"
  },
  {
    "path": "src/snapshots/loco_rs__db__tests__dump_tables_sqlite_all_types.snap",
    "content": "---\nsource: src/db.rs\nexpression: yaml_content\nsnapshot_kind: text\n---\n- active: false\n  array_col:\n  - 1\n  - 2\n  - 3\n  counter: 10\n  created_at: 2025-01-01T10:00:00Z\n  id: 1\n  json_col:\n    k: v\n  name: foo\n  rating: 3.140000104904175\n  uuid_col: 11111111-1111-1111-1111-111111111111\n- active: true\n  array_col:\n  - a\n  - b\n  counter: 20\n  created_at: 2025-01-02T11:30:00Z\n  id: 2\n  json_col:\n    n: 42\n  name: bar\n  opt_bool: true\n  opt_int: 99\n  opt_text: opt\n  rating: 6.28000020980835\n  uuid_col: 22222222-2222-2222-2222-222222222222\n"
  },
  {
    "path": "src/snapshots/loco_rs__db__tests__dump_tables_sqlite_all_types_roundtrip.snap",
    "content": "---\nsource: src/db.rs\nexpression: \"serde_json::to_string_pretty(&roundtripped).unwrap()\"\nsnapshot_kind: text\n---\n[\n  {\n    \"active\": false,\n    \"array_col\": [\n      1,\n      2,\n      3\n    ],\n    \"counter\": 10,\n    \"created_at\": \"2025-01-01T10:00:00Z\",\n    \"id\": 1,\n    \"json_col\": {\n      \"k\": \"v\"\n    },\n    \"name\": \"foo\",\n    \"opt_bool\": null,\n    \"opt_int\": null,\n    \"opt_text\": null,\n    \"rating\": 3.140000104904175,\n    \"uuid_col\": \"11111111-1111-1111-1111-111111111111\"\n  },\n  {\n    \"active\": true,\n    \"array_col\": [\n      \"a\",\n      \"b\"\n    ],\n    \"counter\": 20,\n    \"created_at\": \"2025-01-02T11:30:00Z\",\n    \"id\": 2,\n    \"json_col\": {\n      \"n\": 42\n    },\n    \"name\": \"bar\",\n    \"opt_bool\": true,\n    \"opt_int\": 99,\n    \"opt_text\": \"opt\",\n    \"rating\": 6.28000020980835,\n    \"uuid_col\": \"22222222-2222-2222-2222-222222222222\"\n  }\n]\n"
  },
  {
    "path": "src/snapshots/loco_rs__scheduler__tests__can_display_scheduler.snap",
    "content": "---\nsource: src/scheduler.rs\nassertion_line: 402\nexpression: \"format!(\\\"{scheduler}\\\")\"\nsnapshot_kind: text\n---\n\"#      job_name       run_on_start      schedule               tags               run\\n1      print_task      false        */5 * * * * *          base, echo         \\\"foo\\\"\\n2      run_on_start_task true         every 24 hours         start              \\\"echo \\\\\\\"Does this run on start?\\\\\\\" >> ./run_on_start.txt \\\"\\n3      write_to_file   false        */5 * * * * *          base, write        \\\"echo loco >> ./scheduler.txt\\\"\\n\"\n"
  },
  {
    "path": "src/snapshots/loco_rs__scheduler__tests__can_prepare_command_[shell].snap",
    "content": "---\nsource: src/scheduler.rs\nexpression: prepare_command\n---\nJobDescription {\n    command: \"echo loco\",\n    output: STDOUT,\n    environment: Test,\n}\n"
  },
  {
    "path": "src/snapshots/loco_rs__scheduler__tests__can_prepare_command_[task].snap",
    "content": "---\nsource: src/scheduler.rs\nexpression: prepare_command\n---\nJobDescription {\n    command: \"[BIN_PATH] task foo LOCO_ENV:test SCHEDULER:true\",\n    output: STDOUT,\n    environment: Test,\n}\n"
  },
  {
    "path": "src/snapshots/loco_rs__validation__tests__struct-[foo-bar].snap",
    "content": "---\nsource: src/validation.rs\nexpression: data.validate().map_err(into_db_error)\n---\nOk(\n    (),\n)\n"
  },
  {
    "path": "src/snapshots/loco_rs__validation__tests__struct-[foo].snap",
    "content": "---\nsource: src/validation.rs\nexpression: data.validate().map_err(into_db_error)\n---\nErr(\n    Custom(\n        \"{\\\"name\\\":[{\\\"code\\\":\\\"length\\\",\\\"message\\\":\\\"Invalid min characters long.\\\"}]}\",\n    ),\n)\n"
  },
  {
    "path": "src/snapshots/loco_rs__worker__tests__default_custom_queues-2.snap",
    "content": "---\nsource: src/worker.rs\nexpression: default_queues2\n---\n[\n    \"default\",\n    \"mailer\",\n]\n"
  },
  {
    "path": "src/snapshots/loco_rs__worker__tests__default_custom_queues-3.snap",
    "content": "---\nsource: src/worker.rs\nexpression: merged_queues\n---\n[\n    \"default\",\n    \"mailer\",\n    \"foo\",\n    \"bar\",\n]\n"
  },
  {
    "path": "src/snapshots/loco_rs__worker__tests__default_custom_queues.snap",
    "content": "---\nsource: src/worker.rs\nexpression: default_queues\n---\n[\n    \"default\",\n    \"mailer\",\n]\n"
  },
  {
    "path": "src/storage/contents.rs",
    "content": "use bytes::Bytes;\n\n#[derive(Debug)]\npub struct Contents {\n    data: Bytes,\n}\n\nimpl From<Bytes> for Contents {\n    /// Converts a `Vec<u8>` into a `Contents` instance.\n    ///\n    /// # Returns\n    ///\n    /// Returns a `Contents` instance with the provided byte data.\n    fn from(data: Bytes) -> Self {\n        Self { data }\n    }\n}\n\nimpl From<Contents> for Vec<u8> {\n    /// Convert `Contents` instance int a Vec<u8\n    fn from(contents: Contents) -> Self {\n        contents.data.to_vec()\n    }\n}\n\nimpl TryFrom<Contents> for String {\n    type Error = std::string::FromUtf8Error;\n\n    /// Tries to convert a `Contents` instance into a `String`.\n    ///\n    /// # Returns\n    ///\n    /// Returns a `Result` containing a `String` with the UTF-8 representation\n    /// of the byte data, or an error if the conversion fails.\n    fn try_from(contents: Contents) -> Result<Self, Self::Error> {\n        Self::from_utf8(contents.data.to_vec())\n    }\n}\n"
  },
  {
    "path": "src/storage/drivers/aws.rs",
    "content": "use opendal::{services::S3, Operator};\n\nuse super::{opendal_adapter::OpendalAdapter, StoreDriver};\nuse crate::storage::StorageResult;\n\n/// A set of AWS security credentials\n#[derive(Debug)]\npub struct Credential {\n    /// `AWS_ACCESS_KEY_ID`\n    pub key_id: String,\n    /// `AWS_SECRET_ACCESS_KEY`\n    pub secret_key: String,\n    /// `AWS_SESSION_TOKEN`\n    pub token: Option<String>,\n}\n\n/// Create new AWS s3 storage with bucket and region.\n///\n/// # Examples\n///```\n/// use loco_rs::storage::drivers::aws;\n/// let aws_driver = aws::new(\"bucket_name\", \"region\");\n/// ```\n///\n/// # Errors\n///\n/// When could not initialize the client instance\npub fn new(bucket_name: &str, region: &str) -> StorageResult<Box<dyn StoreDriver>> {\n    let s3 = S3::default().bucket(bucket_name).region(region);\n    Ok(Box::new(OpendalAdapter::new(Operator::new(s3)?.finish())))\n}\n\n/// Create new AWS s3 storage with bucket, region and credentials and URL.\n///\n/// # Examples\n///\n/// ```\n/// use loco_rs::storage::drivers::aws;\n///\n/// let credential = aws::Credential {\n///     key_id: \"\".to_string(),\n///     secret_key: \"\".to_string(),\n///     token: None,\n/// };\n///\n/// let aws_driver = aws::with_credentials_and_endpoint(\"bucket_name\", \"region\", \"https://s3.amazonaws.com\", credential);\n/// ```\n///\n/// # Errors\n///\n/// This function returns an error if the underlying `Operator` creation fails,\n/// such as due to invalid credentials, endpoint configuration issues, or other\n/// OpenDAL-related errors.\npub fn with_credentials_and_endpoint(\n    bucket_name: &str,\n    region: &str,\n    endpoint: &str,\n    credentials: Credential,\n) -> StorageResult<Box<dyn StoreDriver>> {\n    let mut s3 = S3::default()\n        .bucket(bucket_name)\n        .endpoint(endpoint)\n        .region(region)\n        .access_key_id(&credentials.key_id)\n        .secret_access_key(&credentials.secret_key);\n\n    if let Some(token) = credentials.token {\n        s3 = s3.session_token(&token);\n    }\n    Ok(Box::new(OpendalAdapter::new(Operator::new(s3)?.finish())))\n}\n\n/// Create new AWS s3 storage with bucket, region and credentials.\n///\n/// # Examples\n///```\n/// use loco_rs::storage::drivers::aws;\n/// let credential = aws::Credential {\n///    key_id: \"\".to_string(),\n///    secret_key: \"\".to_string(),\n///    token: None\n/// };\n/// let aws_driver = aws::with_credentials(\"bucket_name\", \"region\", credential);\n/// ```\n///\n/// # Errors\n///\n/// When could not initialize the client instance\npub fn with_credentials(\n    bucket_name: &str,\n    region: &str,\n    credentials: Credential,\n) -> StorageResult<Box<dyn StoreDriver>> {\n    let mut s3 = S3::default()\n        .bucket(bucket_name)\n        .region(region)\n        .access_key_id(&credentials.key_id)\n        .secret_access_key(&credentials.secret_key);\n    if let Some(token) = credentials.token {\n        s3 = s3.session_token(&token);\n    }\n    Ok(Box::new(OpendalAdapter::new(Operator::new(s3)?.finish())))\n}\n\n/// Build store with failure\n///\n/// # Panics\n///\n/// Panics if cannot build store\n#[cfg(test)]\n#[must_use]\npub fn with_failure() -> Box<dyn StoreDriver> {\n    let s3 = S3::default()\n        .bucket(\"loco-test\")\n        .region(\"ap-south-1\")\n        .allow_anonymous()\n        .disable_ec2_metadata();\n\n    Box::new(OpendalAdapter::new(Operator::new(s3).unwrap().finish()))\n}\n"
  },
  {
    "path": "src/storage/drivers/azure.rs",
    "content": "use opendal::{services::Azblob, Operator};\n\nuse super::StoreDriver;\nuse crate::storage::{drivers::opendal_adapter::OpendalAdapter, StorageResult};\n\n/// Create new Azure storage.\n///\n/// # Examples\n///```\n/// use loco_rs::storage::drivers::azure;\n/// let azure_driver = azure::new(\"name\", \"account_name\", \"access_key\", \"endpoint\");\n/// ```\n///\n/// # Errors\n///\n/// When could not initialize the client instance\npub fn new(\n    container_name: &str,\n    account_name: &str,\n    access_key: &str,\n    endpoint: &str,\n) -> StorageResult<Box<dyn StoreDriver>> {\n    let azure = Azblob::default()\n        .container(container_name)\n        .account_name(account_name)\n        .account_key(access_key)\n        .endpoint(endpoint);\n\n    Ok(Box::new(OpendalAdapter::new(\n        Operator::new(azure)?.finish(),\n    )))\n}\n"
  },
  {
    "path": "src/storage/drivers/gcp.rs",
    "content": "use opendal::{services::Gcs, Operator};\n\nuse super::StoreDriver;\nuse crate::storage::{drivers::opendal_adapter::OpendalAdapter, StorageResult};\n\n/// Create new GCP storage.\n///\n/// # Examples\n///```\n/// use loco_rs::storage::drivers::gcp;\n/// let gcp_driver = gcp::new(\"key\", \"credential_path\");\n/// ```\n///\n/// # Errors\n///\n/// When could not initialize the client instance\npub fn new(bucket_name: &str, credential_path: &str) -> StorageResult<Box<dyn StoreDriver>> {\n    let gcs = Gcs::default()\n        .bucket(bucket_name)\n        .credential_path(credential_path);\n\n    Ok(Box::new(OpendalAdapter::new(Operator::new(gcs)?.finish())))\n}\n"
  },
  {
    "path": "src/storage/drivers/local.rs",
    "content": "use opendal::{services::Fs, Operator};\n\nuse super::StoreDriver;\nuse crate::storage::{drivers::opendal_adapter::OpendalAdapter, StorageResult};\n\n/// Create new filesystem storage with no prefix\n///\n/// # Examples\n///```\n/// use loco_rs::storage::drivers::local;\n/// let file_system_driver = local::new();\n/// ```\n///\n/// # Panics\n///\n/// Panics if the filesystem service built failed.\n#[must_use]\npub fn new() -> Box<dyn StoreDriver> {\n    let fs = Fs::default().root(\"/\");\n    Box::new(OpendalAdapter::new(\n        Operator::new(fs)\n            .expect(\"fs service should build with success\")\n            .finish(),\n    ))\n}\n\n/// Create new filesystem storage with `prefix` applied to all paths\n///\n/// # Examples\n///```\n/// use loco_rs::storage::drivers::local;\n/// let file_system_driver = local::new_with_prefix(\"users\");\n/// ```\n///\n/// # Errors\n///\n/// Returns an error if the path does not exist\npub fn new_with_prefix(prefix: impl AsRef<std::path::Path>) -> StorageResult<Box<dyn StoreDriver>> {\n    let fs = Fs::default().root(&prefix.as_ref().display().to_string());\n    Ok(Box::new(OpendalAdapter::new(Operator::new(fs)?.finish())))\n}\n"
  },
  {
    "path": "src/storage/drivers/mem.rs",
    "content": "use opendal::{services::Memory, Operator};\n\nuse super::StoreDriver;\nuse crate::storage::drivers::opendal_adapter::OpendalAdapter;\n\n/// Create new in-memory storage.\n///\n/// # Examples\n///```\n/// use loco_rs::storage::drivers::mem;\n/// let mem_storage = mem::new();\n/// ```\n///\n/// # Panics\n///\n/// Panics if the memory service built failed.\n#[must_use]\npub fn new() -> Box<dyn StoreDriver> {\n    Box::new(OpendalAdapter::new(\n        Operator::new(Memory::default())\n            .expect(\"memory service must build with success\")\n            .finish(),\n    ))\n}\n"
  },
  {
    "path": "src/storage/drivers/mod.rs",
    "content": "use std::path::Path;\n\nuse async_trait::async_trait;\nuse bytes::Bytes;\nuse opendal::Reader;\n\n#[cfg(feature = \"storage_aws_s3\")]\npub mod aws;\n#[cfg(feature = \"storage_azure\")]\npub mod azure;\n#[cfg(feature = \"storage_gcp\")]\npub mod gcp;\npub mod local;\npub mod mem;\npub mod null;\npub mod opendal_adapter;\n\nuse super::{stream::BytesStream, StorageResult};\n\n#[derive(Debug)]\npub struct UploadResponse {\n    pub e_tag: Option<String>,\n    pub version: Option<String>,\n}\n\n/// TODO: Add more methods to `GetResponse` to read the content in different\n/// ways\n///\n/// For example, we can read a specific range of bytes from the stream.\npub struct GetResponse {\n    stream: Reader,\n}\n\nimpl GetResponse {\n    pub(crate) fn new(stream: Reader) -> Self {\n        Self { stream }\n    }\n\n    /// Read all content from the stream and return as `Bytes`.\n    ///\n    /// # Errors\n    ///\n    /// Returns a `StorageError` with the reason for the failure.\n    pub async fn bytes(&self) -> StorageResult<Bytes> {\n        Ok(self.stream.read(..).await?.to_bytes())\n    }\n\n    /// Convert the response into a streaming bytes reader.\n    /// This method consumes the `GetResponse` and returns a `BytesStream`\n    /// that can be used for efficient streaming without loading the entire\n    /// content into memory.\n    ///\n    /// # Errors\n    ///\n    /// Returns a `StorageError` if the stream cannot be created.\n    pub async fn into_stream(self) -> StorageResult<BytesStream> {\n        BytesStream::from_reader(self.stream).await\n    }\n}\n\n#[async_trait]\npub trait StoreDriver: Sync + Send {\n    /// Uploads the content represented by `Bytes` to the specified path in the\n    /// object store.\n    ///\n    /// # Errors\n    ///\n    /// Returns a `StorageResult` with the result of the upload operation.\n    async fn upload(&self, path: &Path, content: &Bytes) -> StorageResult<UploadResponse>;\n\n    /// Retrieves the content from the specified path in the object store.\n    ///\n    /// # Errors\n    ///\n    /// Returns a `StorageResult` with the result of the retrieval operation.\n    async fn get(&self, path: &Path) -> StorageResult<GetResponse>;\n\n    /// Deletes the content at the specified path in the object store.\n    ///\n    /// # Errors\n    ///\n    /// Returns a `StorageResult` indicating the success of the deletion\n    /// operation.\n    async fn delete(&self, path: &Path) -> StorageResult<()>;\n\n    /// Renames or moves the content from one path to another in the object\n    /// store.\n    ///\n    /// # Errors\n    ///\n    /// Returns a `StorageResult` indicating the success of the rename/move\n    /// operation.\n    async fn rename(&self, from: &Path, to: &Path) -> StorageResult<()>;\n\n    /// Copies the content from one path to another in the object store.\n    ///\n    /// # Errors\n    ///\n    /// Returns a `StorageResult` indicating the success of the copy operation.\n    async fn copy(&self, from: &Path, to: &Path) -> StorageResult<()>;\n\n    /// Checks if the content exists at the specified path in the object store.\n    ///\n    /// # Errors\n    ///\n    /// Returns a `StorageResult` with a boolean indicating the existence of the\n    /// content.\n    async fn exists(&self, path: &Path) -> StorageResult<bool>;\n\n    /// Retrieves content from the specified path and returns it as a stream.\n    /// This method is more memory-efficient than `get()` for large files as it\n    /// doesn't load the entire content into memory.\n    ///\n    /// # Default Implementation\n    ///\n    /// The default implementation uses the regular `get()` method and converts\n    /// the result to a stream. Storage drivers that support native streaming\n    /// should override this method for better performance.\n    ///\n    /// # Errors\n    ///\n    /// Returns a `StorageResult` with the streaming response.\n    async fn get_stream(&self, path: &Path) -> StorageResult<BytesStream> {\n        let response = self.get(path).await?;\n        response.into_stream().await\n    }\n\n    /// Uploads content from a stream to the specified path.\n    /// This method is more memory-efficient than `upload()` for large files\n    /// as it doesn't require loading the entire content into memory.\n    ///\n    /// # Default Implementation\n    ///\n    /// The default implementation collects the stream into bytes and calls\n    /// the regular `upload()` method. Storage drivers that support native\n    /// streaming should override this method for better performance.\n    ///\n    /// # Errors\n    ///\n    /// Returns a `StorageResult` with the upload response.\n    async fn upload_stream(\n        &self,\n        path: &Path,\n        stream: BytesStream,\n    ) -> StorageResult<UploadResponse> {\n        let bytes = stream\n            .collect()\n            .await\n            .map_err(|e| super::StorageError::Any(Box::new(e)))?;\n        self.upload(path, &bytes).await\n    }\n}\n"
  },
  {
    "path": "src/storage/drivers/null.rs",
    "content": "//! # Null Storage Driver\n//!\n//! The Null storage Driver is the default storage driver implemented when the\n//! Loco framework is initialized. The primary purpose of this driver is to\n//! simplify the user workflow by avoiding the need for feature flags or\n//! optional storage driver configurations.\nuse std::path::Path;\n\nuse async_trait::async_trait;\nuse bytes::Bytes;\n\nuse super::{GetResponse, StorageResult, StoreDriver, UploadResponse};\nuse crate::storage::StorageError;\n\npub struct NullStorage {}\n\n/// Constructor for creating a new `Store` instance.\n#[must_use]\npub fn new() -> Box<dyn StoreDriver> {\n    Box::new(NullStorage {})\n}\n\n#[async_trait]\nimpl StoreDriver for NullStorage {\n    /// Uploads the content represented by `Bytes` to the specified path in the\n    /// object store.\n    ///\n    /// # Errors\n    ///\n    /// Returns a `StorageResult` with the result of the upload operation.\n    async fn upload(&self, _path: &Path, _content: &Bytes) -> StorageResult<UploadResponse> {\n        Err(StorageError::Any(\n            \"Operation not supported by null storage\".into(),\n        ))\n    }\n\n    /// Retrieves the content from the specified path in the object store.\n    ///\n    /// # Errors\n    ///\n    /// Returns a `StorageResult` with the result of the retrieval operation.\n    async fn get(&self, _path: &Path) -> StorageResult<GetResponse> {\n        Err(StorageError::Any(\n            \"Operation not supported by null storage\".into(),\n        ))\n    }\n\n    /// Deletes the content at the specified path in the object store.\n    ///\n    /// # Errors\n    ///\n    /// Returns a `StorageResult` indicating the success of the deletion\n    /// operation.\n    async fn delete(&self, _path: &Path) -> StorageResult<()> {\n        Err(StorageError::Any(\n            \"Operation not supported by null storage\".into(),\n        ))\n    }\n\n    /// Renames or moves the content from one path to another in the object\n    /// store.\n    ///\n    /// # Errors\n    ///\n    /// Returns a `StorageResult` indicating the success of the rename/move\n    /// operation.\n    async fn rename(&self, _from: &Path, _to: &Path) -> StorageResult<()> {\n        Err(StorageError::Any(\n            \"Operation not supported by null storage\".into(),\n        ))\n    }\n\n    /// Copies the content from one path to another in the object store.\n    ///\n    /// # Errors\n    ///\n    /// Returns a `StorageResult` indicating the success of the copy operation.\n    async fn copy(&self, _from: &Path, _to: &Path) -> StorageResult<()> {\n        Err(StorageError::Any(\n            \"Operation not supported by null storage\".into(),\n        ))\n    }\n\n    /// Checks if the content exists at the specified path in the object store.\n    ///\n    /// # Errors\n    ///\n    /// Returns a `StorageResult` with a boolean indicating the existence of the\n    /// content.\n    async fn exists(&self, _path: &Path) -> StorageResult<bool> {\n        Err(StorageError::Any(\n            \"Operation not supported by null storage\".into(),\n        ))\n    }\n}\n"
  },
  {
    "path": "src/storage/drivers/opendal_adapter.rs",
    "content": "use std::path::Path;\n\nuse async_trait::async_trait;\nuse bytes::Bytes;\nuse futures_util::{SinkExt, StreamExt};\nuse opendal::{layers::RetryLayer, Operator};\n\nuse super::{GetResponse, StoreDriver, UploadResponse};\nuse crate::storage::{stream::BytesStream, StorageError, StorageResult};\n\npub struct OpendalAdapter {\n    opendal_impl: Operator,\n}\n\nimpl OpendalAdapter {\n    /// Constructor for creating a new `Store` instance.\n    #[must_use]\n    pub fn new(opendal_impl: Operator) -> Self {\n        let opendal_impl = opendal_impl\n            // Add retry layer with default settings\n            .layer(RetryLayer::default().with_jitter());\n        Self { opendal_impl }\n    }\n}\n\n#[async_trait]\nimpl StoreDriver for OpendalAdapter {\n    /// Uploads the content represented by `Bytes` to the specified path in the\n    /// object store.\n    ///\n    /// # Errors\n    ///\n    /// Returns a `StorageResult` with the result of the upload operation.\n    async fn upload(&self, path: &Path, content: &Bytes) -> StorageResult<UploadResponse> {\n        self.opendal_impl\n            .write(&path.display().to_string(), content.clone())\n            .await?;\n        // TODO: opendal will return the e_tag and version in the future\n        Ok(UploadResponse {\n            e_tag: None,\n            version: None,\n        })\n    }\n\n    /// Retrieves the content from the specified path in the object store.\n    ///\n    /// # Errors\n    ///\n    /// Returns a `StorageResult` with the result of the retrieval operation.\n    async fn get(&self, path: &Path) -> StorageResult<GetResponse> {\n        let r = self\n            .opendal_impl\n            .reader(&path.display().to_string())\n            .await?;\n        Ok(GetResponse::new(r))\n    }\n\n    /// Deletes the content at the specified path in the object store.\n    ///\n    /// # Errors\n    ///\n    /// Returns a `StorageResult` indicating the success of the deletion\n    /// operation.\n    async fn delete(&self, path: &Path) -> StorageResult<()> {\n        Ok(self\n            .opendal_impl\n            .delete(&path.display().to_string())\n            .await?)\n    }\n\n    /// Renames or moves the content from one path to another in the object\n    /// store.\n    ///\n    /// # Behavior\n    ///\n    /// Fallback to copy and delete source if the storage does not support rename.\n    ///\n    /// # Errors\n    ///\n    /// Returns a `StorageResult` indicating the success of the rename/move\n    /// operation.\n    async fn rename(&self, from: &Path, to: &Path) -> StorageResult<()> {\n        if self.opendal_impl.info().full_capability().rename {\n            let from = from.display().to_string();\n            let to = to.display().to_string();\n            Ok(self.opendal_impl.rename(&from, &to).await?)\n        } else {\n            self.copy(from, to).await?;\n            self.delete(from).await?;\n            Ok(())\n        }\n    }\n\n    /// Copies the content from one path to another in the object store.\n    ///\n    /// # Behavior\n    ///\n    /// Fallback to read from source and write into dest if the storage does not support copy.\n    ///\n    /// # Errors\n    ///\n    /// Returns a `StorageResult` indicating the success of the copy operation.\n    async fn copy(&self, from: &Path, to: &Path) -> StorageResult<()> {\n        let from = from.display().to_string();\n        let to = to.display().to_string();\n        if self.opendal_impl.info().full_capability().copy {\n            Ok(self.opendal_impl.copy(&from, &to).await?)\n        } else {\n            let mut reader = self\n                .opendal_impl\n                .reader(&from)\n                .await?\n                .into_bytes_stream(..)\n                .await?;\n            let mut writer = self.opendal_impl.writer(&to).await?.into_bytes_sink();\n            writer\n                .send_all(&mut reader)\n                .await\n                .map_err(|err| StorageError::Any(Box::new(err)))?;\n            writer\n                .close()\n                .await\n                .map_err(|err| StorageError::Any(Box::new(err)))?;\n            Ok(())\n        }\n    }\n\n    /// Checks if the content exists at the specified path in the object store.\n    ///\n    /// # Errors\n    ///\n    /// Returns a `StorageResult` with a boolean indicating the existence of the\n    /// content.\n    ///\n    /// # TODO\n    ///\n    /// The `exists` function should return an error for issues such as permission denied.\n    /// However, these errors are not handled during the migration process and should be addressed\n    /// after the test suites are refactored.\n    async fn exists(&self, path: &Path) -> StorageResult<bool> {\n        let path = path.display().to_string();\n        Ok(self.opendal_impl.exists(&path).await.unwrap_or(false))\n    }\n\n    /// Native streaming implementation for `OpenDAL`.\n    /// This directly uses `OpenDAL`'s reader for efficient streaming.\n    async fn get_stream(&self, path: &Path) -> StorageResult<BytesStream> {\n        let reader = self\n            .opendal_impl\n            .reader(&path.display().to_string())\n            .await?;\n        BytesStream::from_reader(reader).await\n    }\n\n    /// Native streaming upload for `OpenDAL`.\n    /// This uses `OpenDAL`'s writer to stream data directly without buffering.\n    async fn upload_stream(\n        &self,\n        path: &Path,\n        stream: BytesStream,\n    ) -> StorageResult<UploadResponse> {\n        let path_str = path.display().to_string();\n\n        // Create writer with OpenDAL's native API\n        let mut writer = self.opendal_impl.writer(&path_str).await?;\n\n        // Stream data directly to the writer using native write method\n        let mut stream = Box::pin(stream);\n        while let Some(chunk) = stream.next().await {\n            let chunk = chunk.map_err(|e| StorageError::Any(Box::new(e)))?;\n            // Use the native write method which handles the data more efficiently\n            writer.write(chunk).await?;\n        }\n\n        let meta = writer.close().await?;\n\n        Ok(UploadResponse {\n            e_tag: meta.etag().map(std::string::ToString::to_string),\n            version: meta.version().map(std::string::ToString::to_string),\n        })\n    }\n}\n"
  },
  {
    "path": "src/storage/mod.rs",
    "content": "//! # Storage Module\n//!\n//! This module defines a generic storage abstraction represented by the\n//! [`Storage`] struct. It provides methods for performing common storage\n//! operations such as upload, download, delete, rename, and copy.\n//!\n//! ## Storage Strategy\n//!\n//! The [`Storage`] struct is designed to work with different storage\n//! strategies. A storage strategy defines the behavior of the storage\n//! operations. Strategies implement the [`strategies::StorageStrategy`].\n//! The selected strategy can be dynamically changed at runtime.\nmod contents;\npub mod drivers;\npub mod strategies;\npub mod stream;\nuse std::{\n    collections::BTreeMap,\n    path::{Path, PathBuf},\n};\n\nuse bytes::Bytes;\n\nuse self::{drivers::StoreDriver, stream::BytesStream};\n\n#[derive(thiserror::Error, Debug)]\n#[allow(clippy::module_name_repetitions)]\npub enum StorageError {\n    #[error(\"store not found by the given key: {0}\")]\n    StoreNotFound(String),\n\n    #[error(transparent)]\n    Store(#[from] Box<opendal::Error>),\n\n    #[error(\"Unable to read data from file {}\", path.display().to_string())]\n    UnableToReadBytes { path: PathBuf },\n\n    #[error(\"secondaries errors\")]\n    Multi(BTreeMap<String, String>),\n\n    #[error(transparent)]\n    Any(#[from] Box<dyn std::error::Error + Send + Sync>),\n}\n\npub type StorageResult<T> = std::result::Result<T, StorageError>;\n\nimpl From<opendal::Error> for StorageError {\n    fn from(val: opendal::Error) -> Self {\n        Self::Store(Box::new(val))\n    }\n}\n\npub struct Storage {\n    pub stores: BTreeMap<String, Box<dyn StoreDriver>>,\n    pub strategy: Box<dyn strategies::StorageStrategy>,\n}\n\nimpl Storage {\n    /// Creates a new storage instance with a single store and the default\n    /// strategy.\n    ///\n    /// # Examples\n    ///```\n    /// use loco_rs::storage;\n    ///\n    /// let storage = storage::Storage::single(storage::drivers::mem::new());\n    /// ```\n    #[must_use]\n    pub fn single(store: Box<dyn StoreDriver>) -> Self {\n        let default_key = \"store\";\n        Self {\n            strategy: Box::new(strategies::single::SingleStrategy::new(default_key)),\n            stores: BTreeMap::from([(default_key.to_string(), store)]),\n        }\n    }\n\n    /// Creates a new storage instance with the provided stores and strategy.\n    #[must_use]\n    pub fn new(\n        stores: BTreeMap<String, Box<dyn StoreDriver>>,\n        strategy: Box<dyn strategies::StorageStrategy>,\n    ) -> Self {\n        Self { stores, strategy }\n    }\n\n    /// Uploads content to the storage at the specified path.\n    ///\n    /// This method uses the selected strategy for the upload operation.\n    ///\n    /// # Examples\n    ///```\n    /// use loco_rs::storage;\n    /// use std::path::Path;\n    /// use bytes::Bytes;\n    /// pub async fn upload() {\n    ///     let storage = storage::Storage::single(storage::drivers::mem::new());\n    ///     let path = Path::new(\"example.txt\");\n    ///     let content = \"Loco!\";\n    ///     let result = storage.upload(path, &Bytes::from(content)).await;\n    ///     assert!(result.is_ok());\n    /// }\n    /// ```\n    ///\n    /// # Errors\n    ///\n    /// This method returns an error if the upload operation fails or if there\n    /// is an issue with the strategy configuration.\n    pub async fn upload(&self, path: &Path, content: &Bytes) -> StorageResult<()> {\n        self.upload_with_strategy(path, content, &*self.strategy)\n            .await\n    }\n\n    /// Uploads content to the storage at the specified path using a specific\n    /// strategy.\n    ///\n    /// This method allows specifying a custom strategy for the upload\n    /// operation.\n    ///\n    /// # Errors\n    ///\n    /// This method returns an error if the upload operation fails or if there\n    /// is an issue with the strategy configuration.\n    pub async fn upload_with_strategy(\n        &self,\n        path: &Path,\n        content: &Bytes,\n        strategy: &dyn strategies::StorageStrategy,\n    ) -> StorageResult<()> {\n        strategy.upload(self, path, content).await\n    }\n\n    /// Downloads content from the storage at the specified path.\n    ///\n    /// This method uses the selected strategy for the download operation.\n    ///\n    /// # Examples\n    ///```\n    /// use loco_rs::storage;\n    /// use std::path::Path;\n    /// use bytes::Bytes;\n    /// pub async fn download() {\n    ///     let storage = storage::Storage::single(storage::drivers::mem::new());\n    ///     let path = Path::new(\"example.txt\");\n    ///     let content = \"Loco!\";\n    ///     storage.upload(path, &Bytes::from(content)).await;\n    ///\n    ///     let result: String = storage.download(path).await.unwrap();\n    ///     assert_eq!(result, \"Loco!\");\n    /// }\n    /// ```\n    ///\n    /// # Errors\n    ///\n    /// This method returns an error if the download operation fails or if there\n    /// is an issue with the strategy configuration.\n    pub async fn download<T: TryFrom<contents::Contents>>(&self, path: &Path) -> StorageResult<T> {\n        self.download_with_policy(path, &*self.strategy).await\n    }\n\n    /// Downloads content from the storage at the specified path using a\n    /// specific strategy.\n    ///\n    /// This method allows specifying a custom strategy for the download\n    /// operation.\n    ///\n    /// # Errors\n    ///\n    /// This method returns an error if the download operation fails or if there\n    /// is an issue with the strategy configuration.\n    pub async fn download_with_policy<T: TryFrom<contents::Contents>>(\n        &self,\n        path: &Path,\n        strategy: &dyn strategies::StorageStrategy,\n    ) -> StorageResult<T> {\n        let res = strategy.download(self, path).await?;\n        contents::Contents::from(res).try_into().map_or_else(\n            |_| {\n                Err(StorageError::UnableToReadBytes {\n                    path: path.to_path_buf(),\n                })\n            },\n            |content| Ok(content),\n        )\n    }\n\n    /// Deletes content from the storage at the specified path.\n    ///\n    /// This method uses the selected strategy for the delete operation.\n    ///\n    /// # Examples\n    ///```\n    /// use loco_rs::storage;\n    /// use std::path::Path;\n    /// use bytes::Bytes;\n    /// pub async fn download() {\n    ///     let storage = storage::Storage::single(storage::drivers::mem::new());\n    ///     let path = Path::new(\"example.txt\");\n    ///     let content = \"Loco!\";\n    ///     storage.upload(path, &Bytes::from(content)).await;\n    ///\n    ///     let result = storage.delete(path).await;\n    ///     assert!(result.is_ok());\n    /// }\n    /// ```\n    ///\n    /// # Errors\n    ///\n    /// This method returns an error if the delete operation fails or if there\n    /// is an issue with the strategy configuration.\n    pub async fn delete(&self, path: &Path) -> StorageResult<()> {\n        self.delete_with_policy(path, &*self.strategy).await\n    }\n\n    /// Deletes content from the storage at the specified path using a specific\n    /// strategy.\n    ///\n    /// This method allows specifying a custom strategy for the delete\n    /// operation.\n    ///\n    /// # Errors\n    ///\n    /// This method returns an error if the delete operation fails or if there\n    /// is an issue with the strategy configuration.    \n    pub async fn delete_with_policy(\n        &self,\n        path: &Path,\n        strategy: &dyn strategies::StorageStrategy,\n    ) -> StorageResult<()> {\n        strategy.delete(self, path).await\n    }\n\n    /// Renames content from one path to another in the storage.\n    ///\n    /// This method uses the selected strategy for the rename operation.\n    ///\n    /// # Examples\n    ///```\n    /// use loco_rs::storage;\n    /// use std::path::Path;\n    /// use bytes::Bytes;\n    /// pub async fn download() {\n    ///     let storage = storage::Storage::single(storage::drivers::mem::new());\n    ///     let path = Path::new(\"example.txt\");\n    ///     let content = \"Loco!\";\n    ///     storage.upload(path, &Bytes::from(content)).await;\n    ///     \n    ///     let new_path = Path::new(\"new_path.txt\");\n    ///     let store = storage.as_store(\"default\").unwrap();\n    ///     assert!(storage.rename(&path, &new_path).await.is_ok());\n    ///     assert!(!store.exists(&path).await.unwrap());\n    ///     assert!(store.exists(&new_path).await.unwrap());\n    /// }\n    /// ```\n    ///\n    /// # Errors\n    ///\n    /// This method returns an error if the rename operation fails or if there\n    /// is an issue with the strategy configuration.\n    pub async fn rename(&self, from: &Path, to: &Path) -> StorageResult<()> {\n        self.rename_with_policy(from, to, &*self.strategy).await\n    }\n\n    /// Renames content from one path to another in the storage using a specific\n    /// strategy.\n    ///\n    /// This method allows specifying a custom strategy for the rename\n    /// operation.\n    ///\n    /// # Errors\n    ///\n    /// This method returns an error if the rename operation fails or if there\n    /// is an issue with the strategy configuration.\n    pub async fn rename_with_policy(\n        &self,\n        from: &Path,\n        to: &Path,\n        strategy: &dyn strategies::StorageStrategy,\n    ) -> StorageResult<()> {\n        strategy.rename(self, from, to).await\n    }\n\n    /// Copies content from one path to another in the storage.\n    ///\n    /// This method uses the selected strategy for the copy operation.\n    ///\n    /// # Examples\n    ///```\n    /// use loco_rs::storage;\n    /// use std::path::Path;\n    /// use bytes::Bytes;\n    /// pub async fn download() {\n    ///     let storage = storage::Storage::single(storage::drivers::mem::new());\n    ///     let path = Path::new(\"example.txt\");\n    ///     let content = \"Loco!\";\n    ///     storage.upload(path, &Bytes::from(content)).await;\n    ///     \n    ///     let new_path = Path::new(\"new_path.txt\");\n    ///     let store = storage.as_store(\"default\").unwrap();\n    ///     assert!(storage.copy(&path, &new_path).await.is_ok());\n    ///     assert!(store.exists(&path).await.unwrap());\n    ///     assert!(store.exists(&new_path).await.unwrap());\n    /// }\n    /// ```\n    ///\n    /// # Errors\n    ///\n    /// This method returns an error if the copy operation fails or if there is\n    /// an issue with the strategy configuration.\n    pub async fn copy(&self, from: &Path, to: &Path) -> StorageResult<()> {\n        self.copy_with_policy(from, to, &*self.strategy).await\n    }\n\n    /// Copies content from one path to another in the storage using a specific\n    /// strategy.\n    ///\n    /// This method allows specifying a custom strategy for the copy operation.\n    ///\n    /// # Errors\n    ///\n    /// This method returns an error if the copy operation fails or if there is\n    /// an issue with the strategy configuration.\n    pub async fn copy_with_policy(\n        &self,\n        from: &Path,\n        to: &Path,\n        strategy: &dyn strategies::StorageStrategy,\n    ) -> StorageResult<()> {\n        strategy.copy(self, from, to).await\n    }\n\n    /// Returns a reference to the store with the specified name if exists.\n    ///\n    /// # Examples\n    ///```\n    /// use loco_rs::storage;\n    /// use std::path::Path;\n    /// use bytes::Bytes;\n    /// pub async fn download() {\n    ///     let storage = storage::Storage::single(storage::drivers::mem::new());\n    ///     assert!(storage.as_store(\"default\").is_some());\n    ///     assert!(storage.as_store(\"store_2\").is_none());\n    /// }\n    /// ```\n    ///\n    /// # Returns\n    /// Return None if the given name not found.\n    #[must_use]\n    pub fn as_store(&self, name: &str) -> Option<&dyn StoreDriver> {\n        self.stores.get(name).map(|s| &**s)\n    }\n\n    /// Returns a reference to the store with the specified name.\n    ///\n    /// # Examples\n    ///```\n    /// use loco_rs::storage;\n    /// use std::path::Path;\n    /// use bytes::Bytes;\n    /// pub async fn download() {\n    ///     let storage = storage::Storage::single(storage::drivers::mem::new());\n    ///     assert!(storage.as_store_err(\"default\").is_ok());\n    ///     assert!(storage.as_store_err(\"store_2\").is_err());\n    /// }\n    /// ```\n    ///\n    /// # Errors\n    ///\n    /// Return an error if the given store name not exists\n    // REVIEW(nd): not sure bout the name 'as_store_err' -- it returns result\n    pub fn as_store_err(&self, name: &str) -> StorageResult<&dyn StoreDriver> {\n        self.as_store(name)\n            .ok_or(StorageError::StoreNotFound(name.to_string()))\n    }\n\n    /// Downloads content from storage as a stream, enabling efficient\n    /// handling of large files without loading them entirely into memory.\n    ///\n    /// This method uses the selected strategy for the download operation.\n    ///\n    /// # Examples\n    ///```\n    /// use loco_rs::storage;\n    /// use std::path::Path;\n    /// pub async fn stream_download() {\n    ///     let storage = storage::Storage::single(storage::drivers::mem::new());\n    ///     let path = Path::new(\"large_file.mp4\");\n    ///     \n    ///     let stream = storage.download_stream(path).await.unwrap();\n    ///     // Stream can be converted to axum Body for HTTP response\n    ///     // let body = stream.into_body();\n    /// }\n    /// ```\n    ///\n    /// # Errors\n    ///\n    /// This method returns an error if the download operation fails or if there\n    /// is an issue with the strategy configuration.\n    pub async fn download_stream(&self, path: &Path) -> StorageResult<BytesStream> {\n        self.download_stream_with_policy(path, &*self.strategy)\n            .await\n    }\n\n    /// Downloads content from storage as a stream using a specific strategy.\n    ///\n    /// # Errors\n    ///\n    /// This method returns an error if the download operation fails or if there\n    /// is an issue with the strategy configuration.\n    pub async fn download_stream_with_policy(\n        &self,\n        path: &Path,\n        strategy: &dyn strategies::StorageStrategy,\n    ) -> StorageResult<BytesStream> {\n        strategy.download_stream(self, path).await\n    }\n\n    /// Uploads content from a stream to storage, enabling efficient\n    /// handling of large files without loading them entirely into memory.\n    ///\n    /// This method uses the selected strategy for the upload operation.\n    ///\n    /// # Examples\n    ///```\n    /// use loco_rs::storage;\n    /// use std::path::Path;\n    /// pub async fn stream_upload(stream: storage::stream::BytesStream) {\n    ///     let storage = storage::Storage::single(storage::drivers::mem::new());\n    ///     let path = Path::new(\"large_file.mp4\");\n    ///     \n    ///     storage.upload_stream(path, stream).await.unwrap();\n    /// }\n    /// ```\n    ///\n    /// # Errors\n    ///\n    /// This method returns an error if the upload operation fails or if there\n    /// is an issue with the strategy configuration.\n    pub async fn upload_stream(&self, path: &Path, stream: BytesStream) -> StorageResult<()> {\n        self.upload_stream_with_policy(path, stream, &*self.strategy)\n            .await\n    }\n\n    /// Uploads content from a stream using a specific strategy.\n    ///\n    /// # Errors\n    ///\n    /// This method returns an error if the upload operation fails or if there\n    /// is an issue with the strategy configuration.\n    pub async fn upload_stream_with_policy(\n        &self,\n        path: &Path,\n        stream: BytesStream,\n        strategy: &dyn strategies::StorageStrategy,\n    ) -> StorageResult<()> {\n        strategy.upload_stream(self, path, stream).await\n    }\n}\n"
  },
  {
    "path": "src/storage/strategies/backup.rs",
    "content": "//! # `BackupStrategy` Implementation for Storage Strategies\n//!\n//! This module provides an implementation of the [`StorageStrategy`] for\n//! the [`BackupStrategy`]. The [`BackupStrategy`] is designed to mirror storage\n//! operations.\n//!\n//! ## Strategy Description per operation\n//!\n//! * `upload`/`delete`/`rename`/`copy`: The primary storage must succeed in the\n//!   given operation. If there is any failure with the primary storage, this\n//!   function returns an error. When\n//!   * [`FailureMode::BackupAll`] is given - all the secondary storages must\n//!     succeed. If there is one failure in the backup, the operation continues\n//!     to the rest but returns an error.\n//!   * [`FailureMode::AllowBackupFailure`] is given - the operation does not\n//!     return an error when one or more mirror operations fail.\n//!   * [`FailureMode::AtLeastOneFailure`] is given - at least one operation\n//!     should pass.\n//!   * [`FailureMode::CountFailure`] is given - the number of the given backup\n//!     should pass.\n//!\n//! * `download`: Initiates the download of the given path only from primary\n//!   storage.\nuse std::{collections::BTreeMap, path::Path};\n\nuse bytes::Bytes;\n\nuse crate::storage::{strategies::StorageStrategy, Storage, StorageError, StorageResult};\n\n/// Enum representing the failure mode for the [`BackupStrategy`].\n#[derive(Clone, Debug)]\npub enum FailureMode {\n    /// Fail if any secondary storage backend encounters an error.\n    BackupAll,\n    /// Allow errors from secondary storage backup without failing.\n    AllowBackupFailure,\n    /// Allow only one backup failure from secondary storage backup without\n    /// failing.\n    AtLeastOneFailure,\n    /// Allow the given backup number to failure from secondary storage backup\n    /// without failing.\n    CountFailure(usize),\n}\n\n/// Represents the Backup Strategy for storage operations.\n#[derive(Clone)]\npub struct BackupStrategy {\n    pub primary: String,\n    pub secondaries: Option<Vec<String>>,\n    pub failure_mode: FailureMode,\n}\n\n#[async_trait::async_trait]\nimpl StorageStrategy for BackupStrategy {\n    /// Uploads content to the primary and, if configured, secondary storage\n    /// backends.\n    // # Errors\n    ///\n    /// Returns a [`StorageResult`] indicating success or an error depend of the\n    /// [`FailureMode`].\n    async fn upload(&self, storage: &Storage, path: &Path, content: &Bytes) -> StorageResult<()> {\n        storage\n            .as_store_err(&self.primary)?\n            .upload(path, content)\n            .await?;\n\n        let mut collect_errors: BTreeMap<String, String> = BTreeMap::new();\n        if let Some(secondaries) = self.secondaries.as_ref() {\n            for secondary_store in secondaries {\n                match storage.as_store_err(secondary_store) {\n                    Ok(store) => {\n                        if let Err(err) = store.upload(path, content).await {\n                            collect_errors.insert(secondary_store.clone(), err.to_string());\n                        }\n                    }\n                    Err(err) => {\n                        collect_errors.insert(secondary_store.clone(), err.to_string());\n                    }\n                };\n            }\n        }\n\n        if self.failure_mode.should_fail(&collect_errors) {\n            return Err(StorageError::Multi(collect_errors));\n        }\n\n        Ok(())\n    }\n\n    /// Downloads content only from primary storage backend.\n    async fn download(&self, storage: &Storage, path: &Path) -> StorageResult<Bytes> {\n        let store = storage.as_store_err(&self.primary)?;\n        Ok(store.get(path).await?.bytes().await?)\n    }\n\n    /// Deletes content from the primary and, if configured, secondary storage\n    /// backends.\n    ///\n    /// # Errors\n    ///\n    /// Returns a [`StorageResult`] indicating success or an error depend of the\n    /// [`FailureMode`].\n    async fn delete(&self, storage: &Storage, path: &Path) -> StorageResult<()> {\n        storage.as_store_err(&self.primary)?.delete(path).await?;\n\n        let mut collect_errors: BTreeMap<String, String> = BTreeMap::new();\n        if let Some(secondaries) = self.secondaries.as_ref() {\n            for secondary_store in secondaries {\n                match storage.as_store_err(secondary_store) {\n                    Ok(store) => {\n                        if let Err(err) = store.delete(path).await {\n                            collect_errors.insert(secondary_store.clone(), err.to_string());\n                        }\n                    }\n                    Err(err) => {\n                        collect_errors.insert(secondary_store.clone(), err.to_string());\n                    }\n                };\n            }\n        }\n\n        if self.failure_mode.should_fail(&collect_errors) {\n            return Err(StorageError::Multi(collect_errors));\n        }\n\n        Ok(())\n    }\n\n    /// Renames content on the primary and, if configured, secondary storage\n    /// backends.\n    ///\n    /// # Errors\n    ///\n    /// Returns a [`StorageResult`] indicating success or an error depend of the\n    /// [`FailureMode`].\n    async fn rename(&self, storage: &Storage, from: &Path, to: &Path) -> StorageResult<()> {\n        storage\n            .as_store_err(&self.primary)?\n            .rename(from, to)\n            .await?;\n\n        let mut collect_errors: BTreeMap<String, String> = BTreeMap::new();\n        if let Some(secondaries) = self.secondaries.as_ref() {\n            for secondary_store in secondaries {\n                match storage.as_store_err(secondary_store) {\n                    Ok(store) => {\n                        if let Err(err) = store.rename(from, to).await {\n                            collect_errors.insert(secondary_store.clone(), err.to_string());\n                        }\n                    }\n                    Err(err) => {\n                        collect_errors.insert(secondary_store.clone(), err.to_string());\n                    }\n                };\n            }\n        }\n\n        if self.failure_mode.should_fail(&collect_errors) {\n            return Err(StorageError::Multi(collect_errors));\n        }\n\n        Ok(())\n    }\n\n    /// Copies content from the primary and, if configured, secondary storage\n    /// backends.\n    ///\n    /// # Errors\n    ///\n    /// Returns a [`StorageResult`] indicating success or an error depend of the\n    /// [`FailureMode`].\n    async fn copy(&self, storage: &Storage, from: &Path, to: &Path) -> StorageResult<()> {\n        storage.as_store_err(&self.primary)?.copy(from, to).await?;\n\n        let mut collect_errors: BTreeMap<String, String> = BTreeMap::new();\n        if let Some(secondaries) = self.secondaries.as_ref() {\n            for secondary_store in secondaries {\n                match storage.as_store_err(secondary_store) {\n                    Ok(store) => {\n                        if let Err(err) = store.copy(from, to).await {\n                            collect_errors.insert(secondary_store.clone(), err.to_string());\n                        }\n                    }\n                    Err(err) => {\n                        collect_errors.insert(secondary_store.clone(), err.to_string());\n                    }\n                };\n            }\n        }\n\n        if self.failure_mode.should_fail(&collect_errors) {\n            return Err(StorageError::Multi(collect_errors));\n        }\n\n        Ok(())\n    }\n\n    /// Downloads content as a stream from the primary storage\n    ///\n    /// # Errors\n    ///\n    /// Returns a [`StorageResult`] with the stream\n    async fn download_stream(\n        &self,\n        storage: &Storage,\n        path: &Path,\n    ) -> StorageResult<super::super::stream::BytesStream> {\n        // For backup strategy, we only download from primary\n        storage.as_store_err(&self.primary)?.get_stream(path).await\n    }\n\n    /// Uploads content from a stream to the primary and backup storage\n    ///\n    /// # Errors\n    ///\n    /// Returns a [`StorageResult`] indicating of the operation status.\n    async fn upload_stream(\n        &self,\n        storage: &Storage,\n        path: &Path,\n        stream: super::super::stream::BytesStream,\n    ) -> StorageResult<()> {\n        // For backup strategy, we need to buffer the stream content once\n        // to be able to upload to multiple stores\n        let content = stream\n            .collect()\n            .await\n            .map_err(|e| StorageError::Any(Box::new(e)))?;\n\n        // Upload to primary\n        storage\n            .as_store_err(&self.primary)?\n            .upload(path, &content)\n            .await?;\n\n        // Upload to backups if configured\n        if let Some(secondaries) = self.secondaries.as_ref() {\n            let mut collect_errors: BTreeMap<String, String> = BTreeMap::new();\n            for secondary_store in secondaries {\n                match storage.as_store_err(secondary_store) {\n                    Ok(store) => {\n                        if let Err(err) = store.upload(path, &content).await {\n                            collect_errors.insert(secondary_store.clone(), err.to_string());\n                        }\n                    }\n                    Err(err) => {\n                        collect_errors.insert(secondary_store.clone(), err.to_string());\n                    }\n                }\n            }\n\n            if self.failure_mode.should_fail(&collect_errors) {\n                return Err(StorageError::Multi(collect_errors));\n            }\n        }\n\n        Ok(())\n    }\n}\n\nimpl BackupStrategy {\n    /// Creates a new instance of [`BackupStrategy`].\n    #[must_use]\n    pub fn new(primary: &str, secondaries: Option<Vec<String>>, failure_mode: FailureMode) -> Self {\n        Self {\n            primary: primary.to_string(),\n            secondaries,\n            failure_mode,\n        }\n    }\n}\n\nimpl FailureMode {\n    #[must_use]\n    pub fn should_fail(&self, errors: &BTreeMap<String, String>) -> bool {\n        match self {\n            Self::BackupAll => !errors.is_empty(),\n            Self::AllowBackupFailure => false,\n            Self::AtLeastOneFailure => errors.len() > 1,\n            Self::CountFailure(count) => count <= &errors.len(),\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n\n    use std::{collections::BTreeMap, path::PathBuf};\n\n    use super::*;\n    use crate::storage::{drivers, Storage};\n\n    // Upload\n\n    #[tokio::test]\n    async fn upload_should_pass_when_backup_all_policy() {\n        let store_1 = drivers::mem::new();\n        let store_2 = drivers::mem::new();\n        let store_3 = drivers::mem::new();\n\n        let strategy: Box<dyn StorageStrategy> = Box::new(BackupStrategy::new(\n            \"store_1\",\n            Some(vec![\"store_2\".to_string(), \"store_3\".to_string()]),\n            FailureMode::BackupAll,\n        )) as Box<dyn StorageStrategy>;\n\n        let storage = Storage::new(\n            BTreeMap::from([\n                (\"store_1\".to_string(), store_1),\n                (\"store_2\".to_string(), store_2),\n                (\"store_3\".to_string(), store_3),\n            ]),\n            strategy,\n        );\n        let store_1 = storage.as_store(\"store_1\").unwrap();\n        let store_2 = storage.as_store(\"store_2\").unwrap();\n        let store_3 = storage.as_store(\"store_3\").unwrap();\n\n        let path = PathBuf::from(\"users\").join(\"data\").join(\"1.txt\");\n        let file_content = Bytes::from(\"file content\");\n\n        assert!(storage.upload(path.as_path(), &file_content).await.is_ok());\n\n        assert!(store_1.exists(path.as_path()).await.unwrap());\n        assert!(store_2.exists(path.as_path()).await.unwrap());\n        assert!(store_3.exists(path.as_path()).await.unwrap());\n    }\n\n    #[cfg(feature = \"storage_aws_s3\")]\n    #[tokio::test]\n    async fn upload_should_fail_when_primary_fail() {\n        let store_1 = drivers::aws::with_failure();\n        let store_2 = drivers::mem::new();\n        let store_3 = drivers::mem::new();\n\n        let strategy: Box<dyn StorageStrategy> = Box::new(BackupStrategy::new(\n            \"store_1\",\n            Some(vec![\"store_2\".to_string(), \"store_3\".to_string()]),\n            FailureMode::BackupAll,\n        )) as Box<dyn StorageStrategy>;\n\n        let storage = Storage::new(\n            BTreeMap::from([\n                (\"store_1\".to_string(), store_1),\n                (\"store_2\".to_string(), store_2),\n                (\"store_3\".to_string(), store_3),\n            ]),\n            strategy,\n        );\n        let store_1 = storage.as_store(\"store_1\").unwrap();\n        let store_2 = storage.as_store(\"store_2\").unwrap();\n        let store_3 = storage.as_store(\"store_3\").unwrap();\n\n        let path = PathBuf::from(\"users\").join(\"data\").join(\"1.txt\");\n        let file_content = Bytes::from(\"file content\");\n\n        assert!(storage.upload(path.as_path(), &file_content).await.is_err());\n\n        assert!(!store_1.exists(path.as_path()).await.unwrap());\n        assert!(!store_2.exists(path.as_path()).await.unwrap());\n        assert!(!store_3.exists(path.as_path()).await.unwrap());\n    }\n\n    #[cfg(feature = \"storage_aws_s3\")]\n    #[tokio::test]\n    async fn upload_should_pass_when_allow_backup_failure_policy() {\n        let store_1 = drivers::mem::new();\n        let store_2 = drivers::aws::with_failure();\n        let store_3 = drivers::mem::new();\n\n        let strategy: Box<dyn StorageStrategy> = Box::new(BackupStrategy::new(\n            \"store_1\",\n            Some(vec![\"store_2\".to_string(), \"store_3\".to_string()]),\n            FailureMode::AllowBackupFailure,\n        )) as Box<dyn StorageStrategy>;\n\n        let storage = Storage::new(\n            BTreeMap::from([\n                (\"store_1\".to_string(), store_1),\n                (\"store_2\".to_string(), store_2),\n                (\"store_3\".to_string(), store_3),\n            ]),\n            strategy,\n        );\n        let store_1 = storage.as_store(\"store_1\").unwrap();\n        let store_2 = storage.as_store(\"store_2\").unwrap();\n        let store_3 = storage.as_store(\"store_3\").unwrap();\n\n        let path = PathBuf::from(\"users\").join(\"data\").join(\"1.txt\");\n        let file_content = Bytes::from(\"file content\");\n\n        assert!(storage.upload(path.as_path(), &file_content).await.is_ok());\n\n        assert!(store_1.exists(path.as_path()).await.unwrap());\n        assert!(!store_2.exists(path.as_path()).await.unwrap());\n        assert!(store_3.exists(path.as_path()).await.unwrap());\n    }\n\n    #[cfg(feature = \"storage_aws_s3\")]\n    #[tokio::test]\n    async fn upload_should_pass_when_at_least_one_failure_policy() {\n        let store_1 = drivers::mem::new();\n        let store_2 = drivers::aws::with_failure();\n        let store_3 = drivers::mem::new();\n\n        let strategy: Box<dyn StorageStrategy> = Box::new(BackupStrategy::new(\n            \"store_1\",\n            Some(vec![\"store_2\".to_string(), \"store_3\".to_string()]),\n            FailureMode::AtLeastOneFailure,\n        )) as Box<dyn StorageStrategy>;\n\n        let storage = Storage::new(\n            BTreeMap::from([\n                (\"store_1\".to_string(), store_1),\n                (\"store_2\".to_string(), store_2),\n                (\"store_3\".to_string(), store_3),\n            ]),\n            strategy,\n        );\n        let store_1 = storage.as_store(\"store_1\").unwrap();\n        let store_2 = storage.as_store(\"store_2\").unwrap();\n        let store_3 = storage.as_store(\"store_3\").unwrap();\n\n        let path = PathBuf::from(\"users\").join(\"data\").join(\"1.txt\");\n        let file_content = Bytes::from(\"file content\");\n\n        assert!(storage.upload(path.as_path(), &file_content).await.is_ok());\n\n        assert!(store_1.exists(path.as_path()).await.unwrap());\n        assert!(!store_2.exists(path.as_path()).await.unwrap());\n        assert!(store_3.exists(path.as_path()).await.unwrap());\n    }\n\n    #[cfg(feature = \"storage_aws_s3\")]\n    #[tokio::test]\n    async fn upload_should_fail_when_at_least_one_failure_policy() {\n        let store_1 = drivers::mem::new();\n        let store_2 = drivers::aws::with_failure();\n        let store_3 = drivers::aws::with_failure();\n\n        let strategy: Box<dyn StorageStrategy> = Box::new(BackupStrategy::new(\n            \"store_1\",\n            Some(vec![\"store_2\".to_string(), \"store_3\".to_string()]),\n            FailureMode::CountFailure(2),\n        )) as Box<dyn StorageStrategy>;\n\n        let storage = Storage::new(\n            BTreeMap::from([\n                (\"store_1\".to_string(), store_1),\n                (\"store_2\".to_string(), store_2),\n                (\"store_3\".to_string(), store_3),\n            ]),\n            strategy,\n        );\n        let store_1 = storage.as_store(\"store_1\").unwrap();\n        let store_2 = storage.as_store(\"store_2\").unwrap();\n        let store_3 = storage.as_store(\"store_3\").unwrap();\n\n        let path = PathBuf::from(\"users\").join(\"data\").join(\"1.txt\");\n        let file_content = Bytes::from(\"file content\");\n\n        assert!(storage.upload(path.as_path(), &file_content).await.is_err());\n\n        assert!(store_1.exists(path.as_path()).await.unwrap());\n        assert!(!store_2.exists(path.as_path()).await.unwrap());\n        assert!(!store_3.exists(path.as_path()).await.unwrap());\n    }\n\n    #[cfg(feature = \"storage_aws_s3\")]\n    #[tokio::test]\n    async fn upload_should_pass_count_fail_policy_should_pass() {\n        let store_1 = drivers::mem::new();\n        let store_2 = drivers::aws::with_failure();\n        let store_3 = drivers::mem::new();\n\n        let strategy: Box<dyn StorageStrategy> = Box::new(BackupStrategy::new(\n            \"store_1\",\n            Some(vec![\"store_2\".to_string(), \"store_3\".to_string()]),\n            FailureMode::CountFailure(2),\n        )) as Box<dyn StorageStrategy>;\n\n        let storage = Storage::new(\n            BTreeMap::from([\n                (\"store_1\".to_string(), store_1),\n                (\"store_2\".to_string(), store_2),\n                (\"store_3\".to_string(), store_3),\n            ]),\n            strategy,\n        );\n        let store_1 = storage.as_store(\"store_1\").unwrap();\n        let store_2 = storage.as_store(\"store_2\").unwrap();\n        let store_3 = storage.as_store(\"store_3\").unwrap();\n\n        let path = PathBuf::from(\"users\").join(\"data\").join(\"1.txt\");\n        let file_content = Bytes::from(\"file content\");\n\n        assert!(storage.upload(path.as_path(), &file_content).await.is_ok());\n\n        assert!(store_1.exists(path.as_path()).await.unwrap());\n        assert!(!store_2.exists(path.as_path()).await.unwrap());\n        assert!(store_3.exists(path.as_path()).await.unwrap());\n    }\n\n    #[cfg(feature = \"storage_aws_s3\")]\n    #[tokio::test]\n    async fn upload_should_fail_when_count_fail_should_fail() {\n        let store_1 = drivers::mem::new();\n        let store_2 = drivers::aws::with_failure();\n        let store_3 = drivers::aws::with_failure();\n\n        let strategy: Box<dyn StorageStrategy> = Box::new(BackupStrategy::new(\n            \"store_1\",\n            Some(vec![\"store_2\".to_string(), \"store_3\".to_string()]),\n            FailureMode::CountFailure(2),\n        )) as Box<dyn StorageStrategy>;\n\n        let storage = Storage::new(\n            BTreeMap::from([\n                (\"store_1\".to_string(), store_1),\n                (\"store_2\".to_string(), store_2),\n                (\"store_3\".to_string(), store_3),\n            ]),\n            strategy,\n        );\n        let store_1 = storage.as_store(\"store_1\").unwrap();\n        let store_2 = storage.as_store(\"store_2\").unwrap();\n        let store_3 = storage.as_store(\"store_3\").unwrap();\n\n        let path = PathBuf::from(\"users\").join(\"data\").join(\"1.txt\");\n        let file_content = Bytes::from(\"file content\");\n\n        assert!(storage.upload(path.as_path(), &file_content).await.is_err());\n\n        assert!(store_1.exists(path.as_path()).await.unwrap());\n        assert!(!store_2.exists(path.as_path()).await.unwrap());\n        assert!(!store_3.exists(path.as_path()).await.unwrap());\n    }\n\n    // Download\n\n    #[tokio::test]\n    async fn can_download() {\n        let store_1 = drivers::mem::new();\n\n        let strategy: Box<dyn StorageStrategy> = Box::new(BackupStrategy::new(\n            \"store_1\",\n            Some(vec![\"store_2\".to_string(), \"store_3\".to_string()]),\n            FailureMode::BackupAll,\n        )) as Box<dyn StorageStrategy>;\n\n        let storage = Storage::new(BTreeMap::from([(\"store_1\".to_string(), store_1)]), strategy);\n        let store_1 = storage.as_store(\"store_1\").unwrap();\n\n        let path = PathBuf::from(\"users\").join(\"data\").join(\"1.txt\");\n        let file_content = Bytes::from(\"file content\");\n\n        assert!(storage.upload(path.as_path(), &file_content).await.is_err());\n\n        let download_file: String = storage.download(path.as_path()).await.unwrap();\n        assert_eq!(download_file, file_content);\n\n        assert!(store_1.delete(path.as_path()).await.is_ok());\n\n        let download_file: StorageResult<String> = storage.download(path.as_path()).await;\n        assert!(download_file.is_err());\n    }\n\n    // Delete\n\n    #[tokio::test]\n    async fn delete_should_pass_when_backup_all_policy() {\n        let store_1 = drivers::mem::new();\n        let store_2 = drivers::mem::new();\n        let store_3 = drivers::mem::new();\n\n        let strategy: Box<dyn StorageStrategy> = Box::new(BackupStrategy::new(\n            \"store_1\",\n            Some(vec![\"store_2\".to_string(), \"store_3\".to_string()]),\n            FailureMode::AllowBackupFailure,\n        )) as Box<dyn StorageStrategy>;\n\n        let storage = Storage::new(\n            BTreeMap::from([\n                (\"store_1\".to_string(), store_1),\n                (\"store_2\".to_string(), store_2),\n                (\"store_3\".to_string(), store_3),\n            ]),\n            strategy,\n        );\n        let store_1 = storage.as_store(\"store_1\").unwrap();\n        let store_2 = storage.as_store(\"store_2\").unwrap();\n        let store_3 = storage.as_store(\"store_3\").unwrap();\n\n        let path = PathBuf::from(\"users\").join(\"data\").join(\"1.txt\");\n        let file_content = Bytes::from(\"file content\");\n\n        assert!(storage.upload(path.as_path(), &file_content).await.is_ok());\n\n        assert!(storage.delete(path.as_path()).await.is_ok());\n\n        assert!(!store_1.exists(path.as_path()).await.unwrap());\n        assert!(!store_2.exists(path.as_path()).await.unwrap());\n        assert!(!store_3.exists(path.as_path()).await.unwrap());\n    }\n\n    // rename\n    #[tokio::test]\n    async fn rename_should_pass_when_backup_all_policy() {\n        let store_1 = drivers::mem::new();\n        let store_2 = drivers::mem::new();\n        let store_3 = drivers::mem::new();\n\n        let strategy: Box<dyn StorageStrategy> = Box::new(BackupStrategy::new(\n            \"store_1\",\n            Some(vec![\"store_2\".to_string(), \"store_3\".to_string()]),\n            FailureMode::BackupAll,\n        )) as Box<dyn StorageStrategy>;\n\n        let storage = Storage::new(\n            BTreeMap::from([\n                (\"store_1\".to_string(), store_1),\n                (\"store_2\".to_string(), store_2),\n                (\"store_3\".to_string(), store_3),\n            ]),\n            strategy,\n        );\n        let store_1 = storage.as_store(\"store_1\").unwrap();\n        let store_2 = storage.as_store(\"store_2\").unwrap();\n        let store_3 = storage.as_store(\"store_3\").unwrap();\n\n        let orig_path = PathBuf::from(\"users\").join(\"data\").join(\"1.txt\");\n        let new_path = PathBuf::from(\"data-2\").join(\"data\").join(\"2.txt\");\n        let file_content = Bytes::from(\"file content\");\n\n        assert!(storage\n            .upload(orig_path.as_path(), &file_content)\n            .await\n            .is_ok());\n\n        assert!(store_1.exists(orig_path.as_path()).await.unwrap());\n        assert!(store_2.exists(orig_path.as_path()).await.unwrap());\n        assert!(store_3.exists(orig_path.as_path()).await.unwrap());\n\n        assert!(storage\n            .rename(orig_path.as_path(), new_path.as_path())\n            .await\n            .is_ok());\n\n        assert!(!store_1.exists(orig_path.as_path()).await.unwrap());\n        assert!(!store_2.exists(orig_path.as_path()).await.unwrap());\n        assert!(!store_3.exists(orig_path.as_path()).await.unwrap());\n\n        assert!(store_1.exists(new_path.as_path()).await.unwrap());\n        assert!(store_2.exists(new_path.as_path()).await.unwrap());\n        assert!(store_3.exists(new_path.as_path()).await.unwrap());\n    }\n\n    #[tokio::test]\n    async fn rename_should_pass_when_allow_backup_failure_policy() {\n        let store_1 = drivers::mem::new();\n        let store_2 = drivers::mem::new();\n        let store_3 = drivers::mem::new();\n\n        let strategy: Box<dyn StorageStrategy> = Box::new(BackupStrategy::new(\n            \"store_1\",\n            Some(vec![\"store_2\".to_string(), \"store_3\".to_string()]),\n            FailureMode::AllowBackupFailure,\n        )) as Box<dyn StorageStrategy>;\n\n        let storage = Storage::new(\n            BTreeMap::from([\n                (\"store_1\".to_string(), store_1),\n                (\"store_2\".to_string(), store_2),\n                (\"store_3\".to_string(), store_3),\n            ]),\n            strategy,\n        );\n        let store_1 = storage.as_store(\"store_1\").unwrap();\n        let store_2 = storage.as_store(\"store_2\").unwrap();\n        let store_3 = storage.as_store(\"store_3\").unwrap();\n\n        let orig_path = PathBuf::from(\"users\").join(\"data\").join(\"1.txt\");\n        let new_path = PathBuf::from(\"data-2\").join(\"data\").join(\"2.txt\");\n        let file_content = Bytes::from(\"file content\");\n\n        assert!(storage\n            .upload(orig_path.as_path(), &file_content)\n            .await\n            .is_ok());\n\n        assert!(store_1.exists(orig_path.as_path()).await.unwrap());\n        assert!(store_2.exists(orig_path.as_path()).await.unwrap());\n        assert!(store_3.exists(orig_path.as_path()).await.unwrap());\n\n        assert!(store_2.delete(orig_path.as_path()).await.is_ok());\n\n        assert!(storage\n            .rename(orig_path.as_path(), new_path.as_path())\n            .await\n            .is_ok());\n\n        assert!(!store_1.exists(orig_path.as_path()).await.unwrap());\n        assert!(!store_2.exists(orig_path.as_path()).await.unwrap());\n        assert!(!store_3.exists(orig_path.as_path()).await.unwrap());\n\n        assert!(store_1.exists(new_path.as_path()).await.unwrap());\n        assert!(!store_2.exists(new_path.as_path()).await.unwrap());\n        assert!(store_3.exists(new_path.as_path()).await.unwrap());\n    }\n\n    #[tokio::test]\n    async fn rename_should_pass_when_at_least_one_failure_policy() {\n        let store_1 = drivers::mem::new();\n        let store_2 = drivers::mem::new();\n        let store_3 = drivers::mem::new();\n\n        let strategy: Box<dyn StorageStrategy> = Box::new(BackupStrategy::new(\n            \"store_1\",\n            Some(vec![\"store_2\".to_string(), \"store_3\".to_string()]),\n            FailureMode::AtLeastOneFailure,\n        )) as Box<dyn StorageStrategy>;\n\n        let storage = Storage::new(\n            BTreeMap::from([\n                (\"store_1\".to_string(), store_1),\n                (\"store_2\".to_string(), store_2),\n                (\"store_3\".to_string(), store_3),\n            ]),\n            strategy,\n        );\n        let store_1 = storage.as_store(\"store_1\").unwrap();\n        let store_2 = storage.as_store(\"store_2\").unwrap();\n        let store_3 = storage.as_store(\"store_3\").unwrap();\n\n        let orig_path = PathBuf::from(\"users\").join(\"data\").join(\"1.txt\");\n        let new_path = PathBuf::from(\"data-2\").join(\"data\").join(\"2.txt\");\n        let file_content = Bytes::from(\"file content\");\n\n        assert!(storage\n            .upload(orig_path.as_path(), &file_content)\n            .await\n            .is_ok());\n\n        assert!(store_1.exists(orig_path.as_path()).await.unwrap());\n        assert!(store_2.exists(orig_path.as_path()).await.unwrap());\n        assert!(store_3.exists(orig_path.as_path()).await.unwrap());\n\n        assert!(store_2.delete(orig_path.as_path()).await.is_ok());\n\n        assert!(storage\n            .rename(orig_path.as_path(), new_path.as_path())\n            .await\n            .is_ok());\n\n        assert!(!store_1.exists(orig_path.as_path()).await.unwrap());\n        assert!(!store_2.exists(orig_path.as_path()).await.unwrap());\n        assert!(!store_3.exists(orig_path.as_path()).await.unwrap());\n\n        assert!(store_1.exists(new_path.as_path()).await.unwrap());\n        assert!(!store_2.exists(new_path.as_path()).await.unwrap());\n        assert!(store_3.exists(new_path.as_path()).await.unwrap());\n    }\n\n    #[tokio::test]\n    async fn rename_should_fail_when_at_least_one_failure_policy() {\n        let store_1 = drivers::mem::new();\n        let store_2 = drivers::mem::new();\n        let store_3 = drivers::mem::new();\n\n        let strategy: Box<dyn StorageStrategy> = Box::new(BackupStrategy::new(\n            \"store_1\",\n            Some(vec![\"store_2\".to_string(), \"store_3\".to_string()]),\n            FailureMode::AtLeastOneFailure,\n        )) as Box<dyn StorageStrategy>;\n\n        let storage = Storage::new(\n            BTreeMap::from([\n                (\"store_1\".to_string(), store_1),\n                (\"store_2\".to_string(), store_2),\n                (\"store_3\".to_string(), store_3),\n            ]),\n            strategy,\n        );\n        let store_1 = storage.as_store(\"store_1\").unwrap();\n        let store_2 = storage.as_store(\"store_2\").unwrap();\n        let store_3 = storage.as_store(\"store_3\").unwrap();\n\n        let orig_path = PathBuf::from(\"users\").join(\"data\").join(\"1.txt\");\n        let new_path = PathBuf::from(\"data-2\").join(\"data\").join(\"2.txt\");\n        let file_content = Bytes::from(\"file content\");\n\n        assert!(storage\n            .upload(orig_path.as_path(), &file_content)\n            .await\n            .is_ok());\n\n        assert!(store_1.exists(orig_path.as_path()).await.unwrap());\n        assert!(store_2.exists(orig_path.as_path()).await.unwrap());\n        assert!(store_3.exists(orig_path.as_path()).await.unwrap());\n\n        assert!(store_2.delete(orig_path.as_path()).await.is_ok());\n        assert!(store_3.delete(orig_path.as_path()).await.is_ok());\n\n        assert!(storage\n            .rename(orig_path.as_path(), new_path.as_path())\n            .await\n            .is_err());\n\n        assert!(!store_1.exists(orig_path.as_path()).await.unwrap());\n        assert!(!store_2.exists(orig_path.as_path()).await.unwrap());\n        assert!(!store_3.exists(orig_path.as_path()).await.unwrap());\n\n        assert!(store_1.exists(new_path.as_path()).await.unwrap());\n        assert!(!store_2.exists(new_path.as_path()).await.unwrap());\n        assert!(!store_3.exists(new_path.as_path()).await.unwrap());\n    }\n\n    #[tokio::test]\n    async fn rename_should_pass_when_count_fail_policy() {\n        let store_1 = drivers::mem::new();\n        let store_2 = drivers::mem::new();\n        let store_3 = drivers::mem::new();\n\n        let strategy: Box<dyn StorageStrategy> = Box::new(BackupStrategy::new(\n            \"store_1\",\n            Some(vec![\"store_2\".to_string(), \"store_3\".to_string()]),\n            FailureMode::CountFailure(2),\n        )) as Box<dyn StorageStrategy>;\n\n        let storage = Storage::new(\n            BTreeMap::from([\n                (\"store_1\".to_string(), store_1),\n                (\"store_2\".to_string(), store_2),\n                (\"store_3\".to_string(), store_3),\n            ]),\n            strategy,\n        );\n        let store_1 = storage.as_store(\"store_1\").unwrap();\n        let store_2 = storage.as_store(\"store_2\").unwrap();\n        let store_3 = storage.as_store(\"store_3\").unwrap();\n\n        let orig_path = PathBuf::from(\"users\").join(\"data\").join(\"1.txt\");\n        let new_path = PathBuf::from(\"data-2\").join(\"data\").join(\"2.txt\");\n        let file_content = Bytes::from(\"file content\");\n\n        assert!(storage\n            .upload(orig_path.as_path(), &file_content)\n            .await\n            .is_ok());\n\n        assert!(store_1.exists(orig_path.as_path()).await.unwrap());\n        assert!(store_2.exists(orig_path.as_path()).await.unwrap());\n        assert!(store_3.exists(orig_path.as_path()).await.unwrap());\n\n        assert!(store_2.delete(orig_path.as_path()).await.is_ok());\n\n        assert!(storage\n            .rename(orig_path.as_path(), new_path.as_path())\n            .await\n            .is_ok());\n\n        assert!(!store_1.exists(orig_path.as_path()).await.unwrap());\n        assert!(!store_2.exists(orig_path.as_path()).await.unwrap());\n        assert!(!store_3.exists(orig_path.as_path()).await.unwrap());\n\n        assert!(store_1.exists(new_path.as_path()).await.unwrap());\n        assert!(!store_2.exists(new_path.as_path()).await.unwrap());\n        assert!(store_3.exists(new_path.as_path()).await.unwrap());\n    }\n\n    #[tokio::test]\n    async fn rename_should_fail_when_count_fail_policy() {\n        let store_1 = drivers::mem::new();\n        let store_2 = drivers::mem::new();\n        let store_3 = drivers::mem::new();\n\n        let strategy: Box<dyn StorageStrategy> = Box::new(BackupStrategy::new(\n            \"store_1\",\n            Some(vec![\"store_2\".to_string(), \"store_3\".to_string()]),\n            FailureMode::CountFailure(2),\n        )) as Box<dyn StorageStrategy>;\n\n        let storage = Storage::new(\n            BTreeMap::from([\n                (\"store_1\".to_string(), store_1),\n                (\"store_2\".to_string(), store_2),\n                (\"store_3\".to_string(), store_3),\n            ]),\n            strategy,\n        );\n        let store_1 = storage.as_store(\"store_1\").unwrap();\n        let store_2 = storage.as_store(\"store_2\").unwrap();\n        let store_3 = storage.as_store(\"store_3\").unwrap();\n\n        let orig_path = PathBuf::from(\"users\").join(\"data\").join(\"1.txt\");\n        let new_path = PathBuf::from(\"data-2\").join(\"data\").join(\"2.txt\");\n        let file_content = Bytes::from(\"file content\");\n\n        assert!(storage\n            .upload(orig_path.as_path(), &file_content)\n            .await\n            .is_ok());\n\n        assert!(store_1.exists(orig_path.as_path()).await.unwrap());\n        assert!(store_2.exists(orig_path.as_path()).await.unwrap());\n        assert!(store_3.exists(orig_path.as_path()).await.unwrap());\n\n        assert!(store_2.delete(orig_path.as_path()).await.is_ok());\n        assert!(store_3.delete(orig_path.as_path()).await.is_ok());\n\n        assert!(storage\n            .rename(orig_path.as_path(), new_path.as_path())\n            .await\n            .is_err());\n\n        assert!(!store_1.exists(orig_path.as_path()).await.unwrap());\n        assert!(!store_2.exists(orig_path.as_path()).await.unwrap());\n        assert!(!store_3.exists(orig_path.as_path()).await.unwrap());\n\n        assert!(store_1.exists(new_path.as_path()).await.unwrap());\n        assert!(!store_2.exists(new_path.as_path()).await.unwrap());\n        assert!(!store_3.exists(new_path.as_path()).await.unwrap());\n    }\n\n    // Copy\n\n    #[tokio::test]\n    async fn copy_should_pass_when_backup_all_policy() {\n        let store_1 = drivers::mem::new();\n        let store_2 = drivers::mem::new();\n        let store_3 = drivers::mem::new();\n\n        let strategy: Box<dyn StorageStrategy> = Box::new(BackupStrategy::new(\n            \"store_1\",\n            Some(vec![\"store_2\".to_string(), \"store_3\".to_string()]),\n            FailureMode::BackupAll,\n        )) as Box<dyn StorageStrategy>;\n\n        let storage = Storage::new(\n            BTreeMap::from([\n                (\"store_1\".to_string(), store_1),\n                (\"store_2\".to_string(), store_2),\n                (\"store_3\".to_string(), store_3),\n            ]),\n            strategy,\n        );\n        let store_1 = storage.as_store(\"store_1\").unwrap();\n        let store_2 = storage.as_store(\"store_2\").unwrap();\n        let store_3 = storage.as_store(\"store_3\").unwrap();\n\n        let orig_path = PathBuf::from(\"users\").join(\"data\").join(\"1.txt\");\n        let new_path = PathBuf::from(\"data-2\").join(\"data\").join(\"2.txt\");\n        let file_content = Bytes::from(\"file content\");\n\n        assert!(storage\n            .upload(orig_path.as_path(), &file_content)\n            .await\n            .is_ok());\n\n        assert!(store_1.exists(orig_path.as_path()).await.unwrap());\n        assert!(store_2.exists(orig_path.as_path()).await.unwrap());\n        assert!(store_3.exists(orig_path.as_path()).await.unwrap());\n\n        assert!(storage\n            .copy(orig_path.as_path(), new_path.as_path())\n            .await\n            .is_ok());\n\n        assert!(store_1.exists(orig_path.as_path()).await.unwrap());\n        assert!(store_2.exists(orig_path.as_path()).await.unwrap());\n        assert!(store_3.exists(orig_path.as_path()).await.unwrap());\n\n        assert!(store_1.exists(new_path.as_path()).await.unwrap());\n        assert!(store_2.exists(new_path.as_path()).await.unwrap());\n        assert!(store_3.exists(new_path.as_path()).await.unwrap());\n    }\n\n    #[tokio::test]\n    async fn copy_should_pass_when_allow_backup_failure_policy() {\n        let store_1 = drivers::mem::new();\n        let store_2 = drivers::mem::new();\n        let store_3 = drivers::mem::new();\n\n        let strategy: Box<dyn StorageStrategy> = Box::new(BackupStrategy::new(\n            \"store_1\",\n            Some(vec![\"store_2\".to_string(), \"store_3\".to_string()]),\n            FailureMode::AllowBackupFailure,\n        )) as Box<dyn StorageStrategy>;\n\n        let storage = Storage::new(\n            BTreeMap::from([\n                (\"store_1\".to_string(), store_1),\n                (\"store_2\".to_string(), store_2),\n                (\"store_3\".to_string(), store_3),\n            ]),\n            strategy,\n        );\n        let store_1 = storage.as_store(\"store_1\").unwrap();\n        let store_2 = storage.as_store(\"store_2\").unwrap();\n        let store_3 = storage.as_store(\"store_3\").unwrap();\n\n        let orig_path = PathBuf::from(\"users\").join(\"data\").join(\"1.txt\");\n        let new_path = PathBuf::from(\"data-2\").join(\"data\").join(\"2.txt\");\n        let file_content = Bytes::from(\"file content\");\n\n        assert!(storage\n            .upload(orig_path.as_path(), &file_content)\n            .await\n            .is_ok());\n\n        assert!(store_1.exists(orig_path.as_path()).await.unwrap());\n        assert!(store_2.exists(orig_path.as_path()).await.unwrap());\n        assert!(store_3.exists(orig_path.as_path()).await.unwrap());\n\n        assert!(store_2.delete(orig_path.as_path()).await.is_ok());\n\n        assert!(storage\n            .copy(orig_path.as_path(), new_path.as_path())\n            .await\n            .is_ok());\n\n        assert!(store_1.exists(orig_path.as_path()).await.unwrap());\n        assert!(!store_2.exists(orig_path.as_path()).await.unwrap());\n        assert!(store_3.exists(orig_path.as_path()).await.unwrap());\n\n        assert!(store_1.exists(new_path.as_path()).await.unwrap());\n        assert!(!store_2.exists(new_path.as_path()).await.unwrap());\n        assert!(store_3.exists(new_path.as_path()).await.unwrap());\n    }\n\n    #[tokio::test]\n    async fn copy_should_pass_when_at_least_one_failure_policy() {\n        let store_1 = drivers::mem::new();\n        let store_2 = drivers::mem::new();\n        let store_3 = drivers::mem::new();\n\n        let strategy: Box<dyn StorageStrategy> = Box::new(BackupStrategy::new(\n            \"store_1\",\n            Some(vec![\"store_2\".to_string(), \"store_3\".to_string()]),\n            FailureMode::AtLeastOneFailure,\n        )) as Box<dyn StorageStrategy>;\n\n        let storage = Storage::new(\n            BTreeMap::from([\n                (\"store_1\".to_string(), store_1),\n                (\"store_2\".to_string(), store_2),\n                (\"store_3\".to_string(), store_3),\n            ]),\n            strategy,\n        );\n        let store_1 = storage.as_store(\"store_1\").unwrap();\n        let store_2 = storage.as_store(\"store_2\").unwrap();\n        let store_3 = storage.as_store(\"store_3\").unwrap();\n\n        let orig_path = PathBuf::from(\"users\").join(\"data\").join(\"1.txt\");\n        let new_path = PathBuf::from(\"data-2\").join(\"data\").join(\"2.txt\");\n        let file_content = Bytes::from(\"file content\");\n\n        assert!(storage\n            .upload(orig_path.as_path(), &file_content)\n            .await\n            .is_ok());\n\n        assert!(store_1.exists(orig_path.as_path()).await.unwrap());\n        assert!(store_2.exists(orig_path.as_path()).await.unwrap());\n        assert!(store_3.exists(orig_path.as_path()).await.unwrap());\n\n        assert!(store_2.delete(orig_path.as_path()).await.is_ok());\n\n        assert!(storage\n            .copy(orig_path.as_path(), new_path.as_path())\n            .await\n            .is_ok());\n\n        assert!(store_1.exists(orig_path.as_path()).await.unwrap());\n        assert!(!store_2.exists(orig_path.as_path()).await.unwrap());\n        assert!(store_3.exists(orig_path.as_path()).await.unwrap());\n\n        assert!(store_1.exists(new_path.as_path()).await.unwrap());\n        assert!(!store_2.exists(new_path.as_path()).await.unwrap());\n        assert!(store_3.exists(new_path.as_path()).await.unwrap());\n    }\n\n    #[tokio::test]\n    async fn copy_should_fail_when_at_least_one_failure_policy() {\n        let store_1 = drivers::mem::new();\n        let store_2 = drivers::mem::new();\n        let store_3 = drivers::mem::new();\n\n        let strategy: Box<dyn StorageStrategy> = Box::new(BackupStrategy::new(\n            \"store_1\",\n            Some(vec![\"store_2\".to_string(), \"store_3\".to_string()]),\n            FailureMode::AtLeastOneFailure,\n        )) as Box<dyn StorageStrategy>;\n\n        let storage = Storage::new(\n            BTreeMap::from([\n                (\"store_1\".to_string(), store_1),\n                (\"store_2\".to_string(), store_2),\n                (\"store_3\".to_string(), store_3),\n            ]),\n            strategy,\n        );\n        let store_1 = storage.as_store(\"store_1\").unwrap();\n        let store_2 = storage.as_store(\"store_2\").unwrap();\n        let store_3 = storage.as_store(\"store_3\").unwrap();\n\n        let orig_path = PathBuf::from(\"users\").join(\"data\").join(\"1.txt\");\n        let new_path = PathBuf::from(\"data-2\").join(\"data\").join(\"2.txt\");\n        let file_content = Bytes::from(\"file content\");\n\n        assert!(storage\n            .upload(orig_path.as_path(), &file_content)\n            .await\n            .is_ok());\n\n        assert!(store_1.exists(orig_path.as_path()).await.unwrap());\n        assert!(store_2.exists(orig_path.as_path()).await.unwrap());\n        assert!(store_3.exists(orig_path.as_path()).await.unwrap());\n\n        assert!(store_2.delete(orig_path.as_path()).await.is_ok());\n        assert!(store_3.delete(orig_path.as_path()).await.is_ok());\n\n        assert!(storage\n            .copy(orig_path.as_path(), new_path.as_path())\n            .await\n            .is_err());\n\n        assert!(store_1.exists(orig_path.as_path()).await.unwrap());\n        assert!(!store_2.exists(orig_path.as_path()).await.unwrap());\n        assert!(!store_3.exists(orig_path.as_path()).await.unwrap());\n\n        assert!(store_1.exists(new_path.as_path()).await.unwrap());\n        assert!(!store_2.exists(new_path.as_path()).await.unwrap());\n        assert!(!store_3.exists(new_path.as_path()).await.unwrap());\n    }\n\n    #[tokio::test]\n    async fn copy_should_pass_when_count_fail_policy() {\n        let store_1 = drivers::mem::new();\n        let store_2 = drivers::mem::new();\n        let store_3 = drivers::mem::new();\n\n        let strategy: Box<dyn StorageStrategy> = Box::new(BackupStrategy::new(\n            \"store_1\",\n            Some(vec![\"store_2\".to_string(), \"store_3\".to_string()]),\n            FailureMode::CountFailure(2),\n        )) as Box<dyn StorageStrategy>;\n\n        let storage = Storage::new(\n            BTreeMap::from([\n                (\"store_1\".to_string(), store_1),\n                (\"store_2\".to_string(), store_2),\n                (\"store_3\".to_string(), store_3),\n            ]),\n            strategy,\n        );\n        let store_1 = storage.as_store(\"store_1\").unwrap();\n        let store_2 = storage.as_store(\"store_2\").unwrap();\n        let store_3 = storage.as_store(\"store_3\").unwrap();\n\n        let orig_path = PathBuf::from(\"users\").join(\"data\").join(\"1.txt\");\n        let new_path = PathBuf::from(\"data-2\").join(\"data\").join(\"2.txt\");\n        let file_content = Bytes::from(\"file content\");\n\n        assert!(storage\n            .upload(orig_path.as_path(), &file_content)\n            .await\n            .is_ok());\n\n        assert!(store_1.exists(orig_path.as_path()).await.unwrap());\n        assert!(store_2.exists(orig_path.as_path()).await.unwrap());\n        assert!(store_3.exists(orig_path.as_path()).await.unwrap());\n\n        assert!(store_2.delete(orig_path.as_path()).await.is_ok());\n\n        assert!(storage\n            .copy(orig_path.as_path(), new_path.as_path())\n            .await\n            .is_ok());\n\n        assert!(store_1.exists(orig_path.as_path()).await.unwrap());\n        assert!(!store_2.exists(orig_path.as_path()).await.unwrap());\n        assert!(store_3.exists(orig_path.as_path()).await.unwrap());\n\n        assert!(store_1.exists(new_path.as_path()).await.unwrap());\n        assert!(!store_2.exists(new_path.as_path()).await.unwrap());\n        assert!(store_3.exists(new_path.as_path()).await.unwrap());\n    }\n\n    #[tokio::test]\n    async fn copy_should_fail_when_count_fail_policy() {\n        let store_1 = drivers::mem::new();\n        let store_2 = drivers::mem::new();\n        let store_3 = drivers::mem::new();\n\n        let strategy: Box<dyn StorageStrategy> = Box::new(BackupStrategy::new(\n            \"store_1\",\n            Some(vec![\"store_2\".to_string(), \"store_3\".to_string()]),\n            FailureMode::CountFailure(2),\n        )) as Box<dyn StorageStrategy>;\n\n        let storage = Storage::new(\n            BTreeMap::from([\n                (\"store_1\".to_string(), store_1),\n                (\"store_2\".to_string(), store_2),\n                (\"store_3\".to_string(), store_3),\n            ]),\n            strategy,\n        );\n        let store_1 = storage.as_store(\"store_1\").unwrap();\n        let store_2 = storage.as_store(\"store_2\").unwrap();\n        let store_3 = storage.as_store(\"store_3\").unwrap();\n\n        let orig_path = PathBuf::from(\"users\").join(\"data\").join(\"1.txt\");\n        let new_path = PathBuf::from(\"data-2\").join(\"data\").join(\"2.txt\");\n        let file_content = Bytes::from(\"file content\");\n\n        assert!(storage\n            .upload(orig_path.as_path(), &file_content)\n            .await\n            .is_ok());\n\n        assert!(store_1.exists(orig_path.as_path()).await.unwrap());\n        assert!(store_2.exists(orig_path.as_path()).await.unwrap());\n        assert!(store_3.exists(orig_path.as_path()).await.unwrap());\n\n        assert!(store_2.delete(orig_path.as_path()).await.is_ok());\n        assert!(store_3.delete(orig_path.as_path()).await.is_ok());\n\n        assert!(storage\n            .copy(orig_path.as_path(), new_path.as_path())\n            .await\n            .is_err());\n\n        assert!(store_1.exists(orig_path.as_path()).await.unwrap());\n        assert!(!store_2.exists(orig_path.as_path()).await.unwrap());\n        assert!(!store_3.exists(orig_path.as_path()).await.unwrap());\n\n        assert!(store_1.exists(new_path.as_path()).await.unwrap());\n        assert!(!store_2.exists(new_path.as_path()).await.unwrap());\n        assert!(!store_3.exists(new_path.as_path()).await.unwrap());\n    }\n}\n"
  },
  {
    "path": "src/storage/strategies/mirror.rs",
    "content": "//! # `MirrorStrategy` Implementation for Storage Strategies\n//!\n//! This module provides an implementation of the [`StorageStrategy`] for\n//! the [`MirrorStrategy`]. The [`MirrorStrategy`] is designed to mirror storage\n//! operations.\n//!\n//! ## Strategy Description per operation\n//!\n//! * `upload`/`delete`/`rename`/`copy`: The primary storage must succeed in the\n//!   given operation. If there is any failure with the primary storage, this\n//!   function returns an error. When\n//!   * [`FailureMode::MirrorAll`] is given - all the secondary storages must\n//!     succeed. If there is one failure in the mirror, the operation continues\n//!     to the rest but returns an error.\n//!   * [`FailureMode::AllowMirrorFailure`] is given - the operation does not\n//!     return an error when one or more mirror operations fail.\n//!\n//! * `download`: Initiates the download of the given path from the primary\n//!   storage. If successful, it returns the content. If not found in the\n//!   primary, it looks for the content in the secondary storages. If the\n//!   content is not found in any storage backend (both primary and secondary),\n//!   it returns an error.\nuse std::{collections::BTreeMap, path::Path};\n\nuse bytes::Bytes;\n\nuse crate::storage::{strategies::StorageStrategy, Storage, StorageError, StorageResult};\n\n/// Enum representing the failure mode for the [`MirrorStrategy`].\n#[derive(Clone, Debug)]\npub enum FailureMode {\n    /// Fail if any secondary storage mirror encounters an error.\n    MirrorAll,\n    /// Allow errors from secondary storage mirror without failing.\n    AllowMirrorFailure,\n}\n\n/// Represents the Mirror Strategy for storage operations.\n#[derive(Clone, Debug)]\npub struct MirrorStrategy {\n    /// The primary storage backend.\n    pub primary: String,\n    /// Optional secondary storage backends.\n    pub secondaries: Option<Vec<String>>,\n    /// The failure mode for handling errors from secondary storage backends.\n    pub failure_mode: FailureMode,\n}\n\n/// Implementation of the [`StorageStrategy`] for the [`MirrorStrategy`].\n///\n/// The [`MirrorStrategy`] is designed to mirror operations (upload, download,\n/// delete, rename, copy) across multiple storage backends, with optional\n/// secondary storage support and customizable failure modes.\n#[async_trait::async_trait]\nimpl StorageStrategy for MirrorStrategy {\n    /// Uploads content to the primary and, if configured, secondary storage\n    /// mirror.\n    ///\n    /// # Errors\n    ///\n    /// Returns a [`StorageResult`] indicating success or an error depend of the\n    /// [`FailureMode`].\n    async fn upload(&self, storage: &Storage, path: &Path, content: &Bytes) -> StorageResult<()> {\n        storage\n            .as_store_err(&self.primary)?\n            .upload(path, content)\n            .await?;\n\n        let mut collect_errors: BTreeMap<String, String> = BTreeMap::new();\n        if let Some(secondaries) = self.secondaries.as_ref() {\n            for secondary_store in secondaries {\n                match storage.as_store_err(secondary_store) {\n                    Ok(store) => {\n                        if let Err(err) = store.upload(path, content).await {\n                            collect_errors.insert(secondary_store.clone(), err.to_string());\n                        }\n                    }\n                    Err(err) => {\n                        collect_errors.insert(secondary_store.clone(), err.to_string());\n                    }\n                };\n            }\n        }\n\n        if self.failure_mode.should_fail(&collect_errors) {\n            return Err(StorageError::Multi(collect_errors));\n        }\n\n        Ok(())\n    }\n\n    /// Downloads content from the primary storage backend. If the primary\n    /// fails, attempts to download from secondary backends.\n    async fn download(&self, storage: &Storage, path: &Path) -> StorageResult<Bytes> {\n        let res = Self::try_download(storage, &self.primary, path).await;\n\n        match res {\n            Ok(content) => Ok(content),\n            Err(error) => {\n                if let Some(secondaries) = self.secondaries.as_ref() {\n                    for secondary_store in secondaries {\n                        if let Ok(content) =\n                            Self::try_download(storage, secondary_store, path).await\n                        {\n                            return Ok(content);\n                        }\n                    }\n                }\n\n                return Err(error);\n            }\n        }\n    }\n\n    /// Deletes content from the primary and, if configured, secondary storage\n    /// mirrors.\n    ///\n    /// # Errors\n    ///\n    /// Returns a [`StorageResult`] indicating success or an error depend of the\n    /// [`FailureMode`].\n    async fn delete(&self, storage: &Storage, path: &Path) -> StorageResult<()> {\n        storage.as_store_err(&self.primary)?.delete(path).await?;\n\n        let mut collect_errors: BTreeMap<String, String> = BTreeMap::new();\n        if let Some(secondaries) = self.secondaries.as_ref() {\n            for secondary_store in secondaries {\n                match storage.as_store_err(secondary_store) {\n                    Ok(store) => {\n                        if let Err(err) = store.delete(path).await {\n                            collect_errors.insert(secondary_store.clone(), err.to_string());\n                        }\n                    }\n                    Err(err) => {\n                        collect_errors.insert(secondary_store.clone(), err.to_string());\n                    }\n                };\n            }\n        }\n        if self.failure_mode.should_fail(&collect_errors) {\n            return Err(StorageError::Multi(collect_errors));\n        }\n\n        Ok(())\n    }\n\n    /// Renames content on the primary and, if configured, secondary storage\n    /// mirrors.\n    ///\n    /// # Errors\n    ///\n    /// Returns a [`StorageResult`] indicating success or an error depend of the\n    /// [`FailureMode`].\n    async fn rename(&self, storage: &Storage, from: &Path, to: &Path) -> StorageResult<()> {\n        storage\n            .as_store_err(&self.primary)?\n            .rename(from, to)\n            .await?;\n\n        if let Some(secondaries) = self.secondaries.as_ref() {\n            let mut collect_errors: BTreeMap<String, String> = BTreeMap::new();\n            for secondary_store in secondaries {\n                match storage.as_store_err(secondary_store) {\n                    Ok(store) => {\n                        if let Err(err) = store.rename(from, to).await {\n                            collect_errors.insert(secondary_store.clone(), err.to_string());\n                        }\n                    }\n                    Err(err) => {\n                        collect_errors.insert(secondary_store.clone(), err.to_string());\n                    }\n                }\n\n                if self.failure_mode.should_fail(&collect_errors) {\n                    return Err(StorageError::Multi(collect_errors));\n                }\n            }\n        }\n\n        Ok(())\n    }\n\n    /// Copies content from the primary and, if configured, secondary storage\n    /// mirrors.\n    ///\n    /// Returns a [`StorageResult`] indicating success or an error depend of the\n    /// [`FailureMode`].\n    async fn copy(&self, storage: &Storage, from: &Path, to: &Path) -> StorageResult<()> {\n        storage.as_store_err(&self.primary)?.copy(from, to).await?;\n\n        if let Some(secondaries) = self.secondaries.as_ref() {\n            let mut collect_errors: BTreeMap<String, String> = BTreeMap::new();\n            for secondary_store in secondaries {\n                match storage.as_store_err(secondary_store) {\n                    Ok(store) => {\n                        if let Err(err) = store.copy(from, to).await {\n                            collect_errors.insert(secondary_store.clone(), err.to_string());\n                        }\n                    }\n                    Err(err) => {\n                        collect_errors.insert(secondary_store.clone(), err.to_string());\n                    }\n                }\n\n                if self.failure_mode.should_fail(&collect_errors) {\n                    return Err(StorageError::Multi(collect_errors));\n                }\n            }\n        }\n\n        Ok(())\n    }\n\n    /// Downloads content as a stream from the primary storage, or from\n    /// secondary storage if primary fails.\n    ///\n    /// # Errors\n    ///\n    /// Returns a [`StorageResult`] with the stream\n    async fn download_stream(\n        &self,\n        storage: &Storage,\n        path: &Path,\n    ) -> StorageResult<super::super::stream::BytesStream> {\n        // Try primary first\n        if let Ok(stream) = storage.as_store_err(&self.primary)?.get_stream(path).await {\n            Ok(stream)\n        } else {\n            // If primary fails, try secondaries\n            if let Some(secondaries) = self.secondaries.as_ref() {\n                for secondary_store in secondaries {\n                    if let Some(store) = storage.as_store(secondary_store) {\n                        if let Ok(stream) = store.get_stream(path).await {\n                            return Ok(stream);\n                        }\n                    }\n                }\n            }\n            // If all failed, return error from primary\n            storage.as_store_err(&self.primary)?.get_stream(path).await\n        }\n    }\n\n    /// Uploads content from a stream to the primary and secondary storage\n    ///\n    /// # Errors\n    ///\n    /// Returns a [`StorageResult`] indicating of the operation status.\n    async fn upload_stream(\n        &self,\n        storage: &Storage,\n        path: &Path,\n        stream: super::super::stream::BytesStream,\n    ) -> StorageResult<()> {\n        // For mirroring, we need to buffer the stream content once\n        // to be able to upload to multiple stores\n        let content = stream\n            .collect()\n            .await\n            .map_err(|e| StorageError::Any(Box::new(e)))?;\n\n        // Upload to primary\n        storage\n            .as_store_err(&self.primary)?\n            .upload(path, &content)\n            .await?;\n\n        // Upload to secondaries if configured\n        if let Some(secondaries) = self.secondaries.as_ref() {\n            let mut collect_errors: BTreeMap<String, String> = BTreeMap::new();\n            for secondary_store in secondaries {\n                match storage.as_store_err(secondary_store) {\n                    Ok(store) => {\n                        if let Err(err) = store.upload(path, &content).await {\n                            collect_errors.insert(secondary_store.clone(), err.to_string());\n                        }\n                    }\n                    Err(err) => {\n                        collect_errors.insert(secondary_store.clone(), err.to_string());\n                    }\n                }\n\n                if self.failure_mode.should_fail(&collect_errors) {\n                    return Err(StorageError::Multi(collect_errors));\n                }\n            }\n        }\n\n        Ok(())\n    }\n}\n\nimpl MirrorStrategy {\n    /// Creates a new instance of [`MirrorStrategy`].\n    #[must_use]\n    pub fn new(primary: &str, secondaries: Option<Vec<String>>, failure_mode: FailureMode) -> Self {\n        Self {\n            primary: primary.to_string(),\n            secondaries,\n            failure_mode,\n        }\n    }\n\n    // Private helper function for downloading from a specific store.\n    async fn try_download(\n        storage: &Storage,\n        store_name: &str,\n        path: &Path,\n    ) -> StorageResult<Bytes> {\n        let store = storage.as_store_err(store_name)?;\n        store.get(path).await?.bytes().await\n    }\n}\n\nimpl FailureMode {\n    #[must_use]\n    pub fn should_fail(&self, errors: &BTreeMap<String, String>) -> bool {\n        match self {\n            Self::MirrorAll => !errors.is_empty(),\n            Self::AllowMirrorFailure => false,\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n\n    use std::{collections::BTreeMap, path::PathBuf};\n\n    use super::*;\n    use crate::storage::{drivers, Storage};\n\n    #[tokio::test]\n    async fn upload_should_pass_with_mirror_all_policy() {\n        let store_1 = drivers::mem::new();\n        let store_2 = drivers::mem::new();\n        let store_3 = drivers::mem::new();\n\n        let strategy = Box::new(MirrorStrategy::new(\n            \"store_1\",\n            Some(vec![\"store_2\".to_string(), \"store_3\".to_string()]),\n            FailureMode::MirrorAll,\n        )) as Box<dyn StorageStrategy>;\n\n        let storage = Storage::new(\n            BTreeMap::from([\n                (\"store_1\".to_string(), store_1),\n                (\"store_2\".to_string(), store_2),\n                (\"store_3\".to_string(), store_3),\n            ]),\n            strategy,\n        );\n        let store_1 = storage.as_store(\"store_1\").unwrap();\n        let store_2 = storage.as_store(\"store_2\").unwrap();\n        let store_3 = storage.as_store(\"store_3\").unwrap();\n\n        let path = PathBuf::from(\"users\").join(\"data\").join(\"1.txt\");\n        let file_content = Bytes::from(\"file content\");\n\n        assert!(storage.upload(path.as_path(), &file_content).await.is_ok());\n\n        assert!(store_1.exists(path.as_path()).await.unwrap());\n        assert!(store_2.exists(path.as_path()).await.unwrap());\n        assert!(store_3.exists(path.as_path()).await.unwrap());\n    }\n\n    #[cfg(feature = \"storage_aws_s3\")]\n    #[tokio::test]\n    async fn upload_should_fail_with_mirror_all_policy() {\n        let store_1 = drivers::aws::with_failure();\n        let store_2 = drivers::mem::new();\n        let store_3 = drivers::mem::new();\n\n        let strategy = Box::new(MirrorStrategy::new(\n            \"store_1\",\n            Some(vec![\"store_2\".to_string(), \"store_3\".to_string()]),\n            FailureMode::MirrorAll,\n        )) as Box<dyn StorageStrategy>;\n\n        let storage = Storage::new(\n            BTreeMap::from([\n                (\"store_1\".to_string(), store_1),\n                (\"store_2\".to_string(), store_2),\n                (\"store_3\".to_string(), store_3),\n            ]),\n            strategy,\n        );\n        let store_1 = storage.as_store(\"store_1\").unwrap();\n        let store_2 = storage.as_store(\"store_2\").unwrap();\n        let store_3 = storage.as_store(\"store_3\").unwrap();\n\n        let path = PathBuf::from(\"users\").join(\"data\").join(\"1.txt\");\n        let file_content = Bytes::from(\"file content\");\n\n        assert!(storage.upload(path.as_path(), &file_content).await.is_err());\n\n        assert!(!store_1.exists(path.as_path()).await.unwrap());\n        assert!(!store_2.exists(path.as_path()).await.unwrap());\n        assert!(!store_3.exists(path.as_path()).await.unwrap());\n    }\n\n    #[cfg(feature = \"storage_aws_s3\")]\n    #[tokio::test]\n    async fn upload_should_fail_when_allow_mirror_failure_policy() {\n        let store_1 = drivers::mem::new();\n        let store_2 = drivers::aws::with_failure();\n        let store_3 = drivers::mem::new();\n\n        let strategy = Box::new(MirrorStrategy::new(\n            \"store_1\",\n            Some(vec![\"store_2\".to_string(), \"store_3\".to_string()]),\n            FailureMode::AllowMirrorFailure,\n        )) as Box<dyn StorageStrategy>;\n\n        let storage = Storage::new(\n            BTreeMap::from([\n                (\"store_1\".to_string(), store_1),\n                (\"store_2\".to_string(), store_2),\n                (\"store_3\".to_string(), store_3),\n            ]),\n            strategy,\n        );\n        let store_1 = storage.as_store(\"store_1\").unwrap();\n        let store_2 = storage.as_store(\"store_2\").unwrap();\n        let store_3 = storage.as_store(\"store_3\").unwrap();\n\n        let path = PathBuf::from(\"users\").join(\"data\").join(\"1.txt\");\n        let file_content = Bytes::from(\"file content\");\n\n        assert!(storage.upload(path.as_path(), &file_content).await.is_ok());\n\n        assert!(store_1.exists(path.as_path()).await.unwrap());\n        assert!(!store_2.exists(path.as_path()).await.unwrap());\n        assert!(store_3.exists(path.as_path()).await.unwrap());\n    }\n\n    #[tokio::test]\n    async fn can_download_when_primary_is_ok() {\n        let store_1 = drivers::mem::new();\n        let store_2 = drivers::mem::new();\n        let store_3 = drivers::mem::new();\n\n        let strategy = Box::new(MirrorStrategy::new(\n            \"store_1\",\n            Some(vec![\"store_2\".to_string(), \"store_3\".to_string()]),\n            FailureMode::MirrorAll,\n        )) as Box<dyn StorageStrategy>;\n\n        let storage = Storage::new(\n            BTreeMap::from([\n                (\"store_1\".to_string(), store_1),\n                (\"store_2\".to_string(), store_2),\n                (\"store_3\".to_string(), store_3),\n            ]),\n            strategy,\n        );\n        let store_1 = storage.as_store(\"store_1\").unwrap();\n        let store_2 = storage.as_store(\"store_2\").unwrap();\n        let store_3 = storage.as_store(\"store_3\").unwrap();\n\n        let path = PathBuf::from(\"users\").join(\"data\").join(\"1.txt\");\n        let file_content = Bytes::from(\"file content\");\n\n        assert!(storage.upload(path.as_path(), &file_content).await.is_ok());\n\n        let content: String = storage.download(path.as_path()).await.unwrap();\n        assert_eq!(content, \"file content\".to_string());\n\n        assert!(store_1.exists(path.as_path()).await.unwrap());\n        assert!(store_2.exists(path.as_path()).await.unwrap());\n        assert!(store_3.exists(path.as_path()).await.unwrap());\n    }\n\n    #[tokio::test]\n    async fn can_download_when_primary_failed() {\n        let store_1 = drivers::mem::new();\n        let store_2 = drivers::mem::new();\n        let store_3 = drivers::mem::new();\n\n        let strategy = Box::new(MirrorStrategy::new(\n            \"store_1\",\n            Some(vec![\n                \"store_1\".to_string(),\n                \"store_2\".to_string(),\n                \"store_3\".to_string(),\n            ]),\n            FailureMode::MirrorAll,\n        )) as Box<dyn StorageStrategy>;\n\n        let storage = Storage::new(\n            BTreeMap::from([\n                (\"store_1\".to_string(), store_1),\n                (\"store_2\".to_string(), store_2),\n                (\"store_3\".to_string(), store_3),\n            ]),\n            strategy,\n        );\n        let store_1 = storage.as_store(\"store_1\").unwrap();\n        let store_2 = storage.as_store(\"store_2\").unwrap();\n        let store_3 = storage.as_store(\"store_3\").unwrap();\n\n        let path = PathBuf::from(\"users\").join(\"data\").join(\"1.txt\");\n        let file_content = Bytes::from(\"file content\");\n\n        assert!(storage.upload(path.as_path(), &file_content).await.is_ok());\n\n        assert!(store_1.delete(path.as_path()).await.is_ok());\n        assert!(store_2.delete(path.as_path()).await.is_ok());\n\n        assert!(!store_1.exists(path.as_path()).await.unwrap());\n        assert!(!store_2.exists(path.as_path()).await.unwrap());\n        assert!(store_3.exists(path.as_path()).await.unwrap());\n\n        let content: String = storage.download(path.as_path()).await.unwrap();\n        assert_eq!(content, \"file content\".to_string());\n    }\n\n    #[tokio::test]\n    async fn rename_should_pass_when_primary_is_ok() {\n        let store_1 = drivers::mem::new();\n        let store_2 = drivers::mem::new();\n        let store_3 = drivers::mem::new();\n\n        let strategy = Box::new(MirrorStrategy::new(\n            \"store_1\",\n            Some(vec![\"store_2\".to_string(), \"store_3\".to_string()]),\n            FailureMode::MirrorAll,\n        )) as Box<dyn StorageStrategy>;\n\n        let storage = Storage::new(\n            BTreeMap::from([\n                (\"store_1\".to_string(), store_1),\n                (\"store_2\".to_string(), store_2),\n                (\"store_3\".to_string(), store_3),\n            ]),\n            strategy,\n        );\n        let store_1 = storage.as_store(\"store_1\").unwrap();\n        let store_2 = storage.as_store(\"store_2\").unwrap();\n        let store_3 = storage.as_store(\"store_3\").unwrap();\n\n        let orig_path = PathBuf::from(\"users\").join(\"data\").join(\"1.txt\");\n        let new_path = PathBuf::from(\"data-2\").join(\"data\").join(\"2.txt\");\n        let file_content = Bytes::from(\"file content\");\n\n        assert!(storage\n            .upload(orig_path.as_path(), &file_content)\n            .await\n            .is_ok());\n\n        assert!(store_1.exists(orig_path.as_path()).await.unwrap());\n        assert!(store_2.exists(orig_path.as_path()).await.unwrap());\n        assert!(store_3.exists(orig_path.as_path()).await.unwrap());\n\n        assert!(storage\n            .rename(orig_path.as_path(), new_path.as_path())\n            .await\n            .is_ok());\n\n        assert!(!store_1.exists(orig_path.as_path()).await.unwrap());\n        assert!(!store_2.exists(orig_path.as_path()).await.unwrap());\n        assert!(!store_3.exists(orig_path.as_path()).await.unwrap());\n\n        assert!(store_1.exists(new_path.as_path()).await.unwrap());\n        assert!(store_2.exists(new_path.as_path()).await.unwrap());\n        assert!(store_3.exists(new_path.as_path()).await.unwrap());\n    }\n\n    #[tokio::test]\n    async fn rename_should_fail_when_primary_failed() {\n        let store_1 = drivers::mem::new();\n        let store_2 = drivers::mem::new();\n        let store_3 = drivers::mem::new();\n\n        let strategy: Box<dyn StorageStrategy> = Box::new(MirrorStrategy::new(\n            \"store_1\",\n            Some(vec![\"store_2\".to_string(), \"store_3\".to_string()]),\n            FailureMode::MirrorAll,\n        )) as Box<dyn StorageStrategy>;\n\n        let storage = Storage::new(\n            BTreeMap::from([\n                (\"store_1\".to_string(), store_1),\n                (\"store_2\".to_string(), store_2),\n                (\"store_3\".to_string(), store_3),\n            ]),\n            strategy,\n        );\n        let store_1 = storage.as_store(\"store_1\").unwrap();\n        let store_2 = storage.as_store(\"store_2\").unwrap();\n        let store_3 = storage.as_store(\"store_3\").unwrap();\n\n        let orig_path = PathBuf::from(\"users\").join(\"data\").join(\"1.txt\");\n        let new_path = PathBuf::from(\"data-2\").join(\"data\").join(\"2.txt\");\n        let file_content = Bytes::from(\"file content\");\n\n        assert!(storage\n            .upload(orig_path.as_path(), &file_content)\n            .await\n            .is_ok());\n\n        assert!(store_1.exists(orig_path.as_path()).await.unwrap());\n        assert!(store_2.exists(orig_path.as_path()).await.unwrap());\n        assert!(store_3.exists(orig_path.as_path()).await.unwrap());\n\n        assert!(store_2.delete(orig_path.as_path()).await.is_ok());\n\n        assert!(storage\n            .rename(orig_path.as_path(), new_path.as_path())\n            .await\n            .is_err());\n    }\n\n    #[tokio::test]\n    async fn rename_should_pass_when_allow_mirror_failure() {\n        let store_1 = drivers::mem::new();\n        let store_2 = drivers::mem::new();\n        let store_3 = drivers::mem::new();\n\n        let strategy: Box<dyn StorageStrategy> = Box::new(MirrorStrategy::new(\n            \"store_1\",\n            Some(vec![\"store_2\".to_string(), \"store_3\".to_string()]),\n            FailureMode::AllowMirrorFailure,\n        )) as Box<dyn StorageStrategy>;\n\n        let storage = Storage::new(\n            BTreeMap::from([\n                (\"store_1\".to_string(), store_1),\n                (\"store_2\".to_string(), store_2),\n                (\"store_3\".to_string(), store_3),\n            ]),\n            strategy,\n        );\n        let store_1 = storage.as_store(\"store_1\").unwrap();\n        let store_2 = storage.as_store(\"store_2\").unwrap();\n        let store_3 = storage.as_store(\"store_3\").unwrap();\n\n        let orig_path = PathBuf::from(\"users\").join(\"data\").join(\"1.txt\");\n        let new_path = PathBuf::from(\"data-2\").join(\"data\").join(\"2.txt\");\n        let file_content = Bytes::from(\"file content\");\n\n        assert!(storage\n            .upload(orig_path.as_path(), &file_content)\n            .await\n            .is_ok());\n\n        assert!(store_1.exists(orig_path.as_path()).await.unwrap());\n        assert!(store_2.exists(orig_path.as_path()).await.unwrap());\n        assert!(store_3.exists(orig_path.as_path()).await.unwrap());\n\n        assert!(store_2.delete(orig_path.as_path()).await.is_ok());\n\n        assert!(storage\n            .rename(orig_path.as_path(), new_path.as_path())\n            .await\n            .is_ok());\n\n        assert!(!store_1.exists(orig_path.as_path()).await.unwrap());\n        assert!(!store_3.exists(orig_path.as_path()).await.unwrap());\n\n        assert!(store_1.exists(new_path.as_path()).await.unwrap());\n        assert!(store_3.exists(new_path.as_path()).await.unwrap());\n    }\n\n    #[tokio::test]\n    async fn copy_should_pass_when_primary_is_ok() {\n        let store_1 = drivers::mem::new();\n        let store_2 = drivers::mem::new();\n        let store_3 = drivers::mem::new();\n\n        let strategy = Box::new(MirrorStrategy::new(\n            \"store_1\",\n            Some(vec![\"store_2\".to_string(), \"store_3\".to_string()]),\n            FailureMode::MirrorAll,\n        )) as Box<dyn StorageStrategy>;\n\n        let storage = Storage::new(\n            BTreeMap::from([\n                (\"store_1\".to_string(), store_1),\n                (\"store_2\".to_string(), store_2),\n                (\"store_3\".to_string(), store_3),\n            ]),\n            strategy,\n        );\n        let store_1 = storage.as_store(\"store_1\").unwrap();\n        let store_2 = storage.as_store(\"store_2\").unwrap();\n        let store_3 = storage.as_store(\"store_3\").unwrap();\n\n        let orig_path = PathBuf::from(\"users\").join(\"data\").join(\"1.txt\");\n        let new_path = PathBuf::from(\"data-2\").join(\"data\").join(\"2.txt\");\n        let file_content = Bytes::from(\"file content\");\n\n        assert!(storage\n            .upload(orig_path.as_path(), &file_content)\n            .await\n            .is_ok());\n\n        assert!(store_1.exists(orig_path.as_path()).await.unwrap());\n        assert!(store_2.exists(orig_path.as_path()).await.unwrap());\n        assert!(store_3.exists(orig_path.as_path()).await.unwrap());\n\n        assert!(storage\n            .copy(orig_path.as_path(), new_path.as_path())\n            .await\n            .is_ok());\n\n        assert!(store_1.exists(orig_path.as_path()).await.unwrap());\n        assert!(store_2.exists(orig_path.as_path()).await.unwrap());\n        assert!(store_3.exists(orig_path.as_path()).await.unwrap());\n\n        assert!(store_1.exists(new_path.as_path()).await.unwrap());\n        assert!(store_2.exists(new_path.as_path()).await.unwrap());\n        assert!(store_3.exists(new_path.as_path()).await.unwrap());\n    }\n\n    #[tokio::test]\n    async fn copy_should_pass_fail_when_primary() {\n        let store_1 = drivers::mem::new();\n        let store_2 = drivers::mem::new();\n        let store_3 = drivers::mem::new();\n\n        let strategy: Box<dyn StorageStrategy> = Box::new(MirrorStrategy::new(\n            \"store_1\",\n            Some(vec![\"store_2\".to_string(), \"store_3\".to_string()]),\n            FailureMode::MirrorAll,\n        )) as Box<dyn StorageStrategy>;\n\n        let storage = Storage::new(\n            BTreeMap::from([\n                (\"store_1\".to_string(), store_1),\n                (\"store_2\".to_string(), store_2),\n                (\"store_3\".to_string(), store_3),\n            ]),\n            strategy,\n        );\n        let store_1 = storage.as_store(\"store_1\").unwrap();\n        let store_2 = storage.as_store(\"store_2\").unwrap();\n        let store_3 = storage.as_store(\"store_3\").unwrap();\n\n        let orig_path = PathBuf::from(\"users\").join(\"data\").join(\"1.txt\");\n        let new_path = PathBuf::from(\"data-2\").join(\"data\").join(\"2.txt\");\n        let file_content = Bytes::from(\"file content\");\n\n        assert!(storage\n            .upload(orig_path.as_path(), &file_content)\n            .await\n            .is_ok());\n\n        assert!(store_1.exists(orig_path.as_path()).await.unwrap());\n        assert!(store_2.exists(orig_path.as_path()).await.unwrap());\n        assert!(store_3.exists(orig_path.as_path()).await.unwrap());\n\n        assert!(store_2.delete(orig_path.as_path()).await.is_ok());\n\n        assert!(storage\n            .copy(orig_path.as_path(), new_path.as_path())\n            .await\n            .is_err());\n    }\n\n    #[tokio::test]\n    async fn should_pass_when_allow_mirror_failure() {\n        let store_1 = drivers::mem::new();\n        let store_2 = drivers::mem::new();\n        let store_3 = drivers::mem::new();\n\n        let strategy: Box<dyn StorageStrategy> = Box::new(MirrorStrategy::new(\n            \"store_1\",\n            Some(vec![\"store_2\".to_string(), \"store_3\".to_string()]),\n            FailureMode::AllowMirrorFailure,\n        )) as Box<dyn StorageStrategy>;\n\n        let storage = Storage::new(\n            BTreeMap::from([\n                (\"store_1\".to_string(), store_1),\n                (\"store_2\".to_string(), store_2),\n                (\"store_3\".to_string(), store_3),\n            ]),\n            strategy,\n        );\n        let store_1 = storage.as_store(\"store_1\").unwrap();\n        let store_2 = storage.as_store(\"store_2\").unwrap();\n        let store_3 = storage.as_store(\"store_3\").unwrap();\n\n        let orig_path = PathBuf::from(\"users\").join(\"data\").join(\"1.txt\");\n        let new_path = PathBuf::from(\"data-2\").join(\"data\").join(\"2.txt\");\n        let file_content = Bytes::from(\"file content\");\n\n        assert!(storage\n            .upload(orig_path.as_path(), &file_content)\n            .await\n            .is_ok());\n\n        assert!(store_1.exists(orig_path.as_path()).await.unwrap());\n        assert!(store_2.exists(orig_path.as_path()).await.unwrap());\n        assert!(store_3.exists(orig_path.as_path()).await.unwrap());\n\n        assert!(store_2.delete(orig_path.as_path()).await.is_ok());\n\n        assert!(storage\n            .copy(orig_path.as_path(), new_path.as_path())\n            .await\n            .is_ok());\n\n        assert!(store_1.exists(orig_path.as_path()).await.unwrap());\n        assert!(store_3.exists(orig_path.as_path()).await.unwrap());\n\n        assert!(store_1.exists(new_path.as_path()).await.unwrap());\n        assert!(store_3.exists(new_path.as_path()).await.unwrap());\n    }\n}\n"
  },
  {
    "path": "src/storage/strategies/mod.rs",
    "content": "pub mod backup;\npub mod mirror;\npub mod single;\n\nuse std::path::Path;\n\nuse bytes::Bytes;\n\nuse crate::storage::{stream::BytesStream, Storage, StorageResult};\n\n#[async_trait::async_trait]\npub trait StorageStrategy: Sync + Send {\n    async fn upload(&self, storage: &Storage, path: &Path, content: &Bytes) -> StorageResult<()>;\n    async fn download(&self, storage: &Storage, path: &Path) -> StorageResult<Bytes>;\n    async fn delete(&self, storage: &Storage, path: &Path) -> StorageResult<()>;\n    async fn rename(&self, storage: &Storage, from: &Path, to: &Path) -> StorageResult<()>;\n    async fn copy(&self, storage: &Storage, from: &Path, to: &Path) -> StorageResult<()>;\n\n    /// Download content as a stream for memory-efficient large file handling.\n    ///\n    /// Strategies must implement this method to support streaming downloads.\n    async fn download_stream(&self, storage: &Storage, path: &Path) -> StorageResult<BytesStream>;\n\n    /// Upload content from a stream for memory-efficient large file handling.\n    ///\n    /// Strategies must implement this method to support streaming uploads.\n    async fn upload_stream(\n        &self,\n        storage: &Storage,\n        path: &Path,\n        stream: BytesStream,\n    ) -> StorageResult<()>;\n}\n"
  },
  {
    "path": "src/storage/strategies/single.rs",
    "content": "//! # Single Storage Strategy Implementation\n//!\n//! This module provides an implementation of the [`StorageStrategy`] for a\n//! single storage strategy.\nuse std::path::Path;\n\nuse bytes::Bytes;\n\nuse crate::storage::{strategies::StorageStrategy, Storage, StorageResult};\n\n/// Represents a single storage strategy.\n#[derive(Clone)]\npub struct SingleStrategy {\n    pub primary: String,\n}\n\nimpl SingleStrategy {\n    /// Creates a new instance of `SingleStrategy` with the specified primary\n    /// storage identifier.\n    #[must_use]\n    pub fn new(primary: &str) -> Self {\n        Self {\n            primary: primary.to_string(),\n        }\n    }\n}\n\n/// Implementation of `StorageStrategy` for a single storage strategy.\n#[async_trait::async_trait]\nimpl StorageStrategy for SingleStrategy {\n    /// Uploads content to the primary storage.\n    ///\n    /// # Errors\n    ///\n    /// Returns a [`StorageResult`] indicating of the operation status.\n    async fn upload(&self, storage: &Storage, path: &Path, content: &Bytes) -> StorageResult<()> {\n        storage\n            .as_store_err(&self.primary)?\n            .upload(path, content)\n            .await?;\n        Ok(())\n    }\n\n    /// Downloads content\n    ///\n    /// # Errors\n    ///\n    /// Returns a [`StorageResult`] indicating of the operation status.\n    async fn download(&self, storage: &Storage, path: &Path) -> StorageResult<Bytes> {\n        let store = storage.as_store_err(&self.primary)?;\n        Ok(store.get(path).await?.bytes().await?)\n    }\n\n    /// Deletes the given path\n    ///\n    /// # Errors\n    ///\n    /// Returns a [`StorageResult`] indicating of the operation status.\n    async fn delete(&self, storage: &Storage, path: &Path) -> StorageResult<()> {\n        Ok(storage.as_store_err(&self.primary)?.delete(path).await?)\n    }\n\n    /// Renames the file name\n    ///\n    /// # Errors\n    ///\n    /// Returns a [`StorageResult`] indicating of the operation status.\n    async fn rename(&self, storage: &Storage, from: &Path, to: &Path) -> StorageResult<()> {\n        Ok(storage\n            .as_store_err(&self.primary)?\n            .rename(from, to)\n            .await?)\n    }\n\n    /// Copy file from the given path to the new path\n    ///\n    /// # Errors\n    ///\n    /// Returns a [`StorageResult`] indicating of the operation status.\n    async fn copy(&self, storage: &Storage, from: &Path, to: &Path) -> StorageResult<()> {\n        Ok(storage.as_store_err(&self.primary)?.copy(from, to).await?)\n    }\n\n    /// Downloads content as a stream from the primary storage\n    ///\n    /// # Errors\n    ///\n    /// Returns a [`StorageResult`] with the stream\n    async fn download_stream(\n        &self,\n        storage: &Storage,\n        path: &Path,\n    ) -> StorageResult<super::super::stream::BytesStream> {\n        storage.as_store_err(&self.primary)?.get_stream(path).await\n    }\n\n    /// Uploads content from a stream to the primary storage\n    ///\n    /// # Errors\n    ///\n    /// Returns a [`StorageResult`] indicating of the operation status.\n    async fn upload_stream(\n        &self,\n        storage: &Storage,\n        path: &Path,\n        stream: super::super::stream::BytesStream,\n    ) -> StorageResult<()> {\n        storage\n            .as_store_err(&self.primary)?\n            .upload_stream(path, stream)\n            .await?;\n        Ok(())\n    }\n}\n\n#[cfg(test)]\nmod tests {\n\n    use std::{collections::BTreeMap, path::PathBuf};\n\n    use super::*;\n    use crate::storage::{drivers, Storage};\n\n    #[tokio::test]\n    async fn can_upload() {\n        let store = drivers::mem::new();\n\n        let strategy = Box::new(SingleStrategy::new(\"default\")) as Box<dyn StorageStrategy>;\n\n        let storage = Storage::new(BTreeMap::from([(\"default\".to_string(), store)]), strategy);\n\n        let store = storage.as_store(\"default\").unwrap();\n        let path = PathBuf::from(\"users\").join(\"data\").join(\"1.txt\");\n        let file_content = Bytes::from(\"file content\");\n\n        assert!(storage.upload(path.as_path(), &file_content).await.is_ok());\n\n        assert!(store.exists(path.as_path()).await.unwrap());\n    }\n\n    #[tokio::test]\n    async fn can_download() {\n        let store = drivers::mem::new();\n\n        let strategy = Box::new(SingleStrategy::new(\"default\")) as Box<dyn StorageStrategy>;\n\n        let storage = Storage::new(BTreeMap::from([(\"default\".to_string(), store)]), strategy);\n\n        let path = PathBuf::from(\"users\").join(\"data\").join(\"1.txt\");\n        let file_content = Bytes::from(\"file content\");\n\n        let store = storage.as_store(\"default\").unwrap();\n        assert!(store.upload(path.as_path(), &file_content).await.is_ok());\n\n        let download_file: String = storage.download(path.as_path()).await.unwrap();\n        assert_eq!(download_file, file_content);\n    }\n\n    #[tokio::test]\n    async fn can_delete() {\n        let store = drivers::mem::new();\n\n        let strategy = Box::new(SingleStrategy::new(\"default\")) as Box<dyn StorageStrategy>;\n\n        let storage = Storage::new(BTreeMap::from([(\"default\".to_string(), store)]), strategy);\n\n        let store = storage.as_store(\"default\").unwrap();\n        let path = PathBuf::from(\"users\").join(\"data\").join(\"1.txt\");\n        let file_content = Bytes::from(\"file content\");\n\n        assert!(store.upload(path.as_path(), &file_content).await.is_ok());\n\n        assert!(store.exists(path.as_path()).await.unwrap());\n\n        assert!(storage.delete(path.as_path()).await.is_ok());\n\n        assert!(!store.exists(path.as_path()).await.unwrap());\n    }\n\n    #[tokio::test]\n    async fn can_rename_file_path() {\n        let store = drivers::mem::new();\n\n        let strategy = Box::new(SingleStrategy::new(\"default\")) as Box<dyn StorageStrategy>;\n\n        let storage = Storage::new(BTreeMap::from([(\"default\".to_string(), store)]), strategy);\n\n        let store = storage.as_store(\"default\").unwrap();\n        let orig_path = PathBuf::from(\"users\").join(\"data\").join(\"1.txt\");\n        let file_content = Bytes::from(\"file content\");\n\n        assert!(storage\n            .upload(orig_path.as_path(), &file_content)\n            .await\n            .is_ok());\n\n        assert!(store.exists(orig_path.as_path()).await.unwrap());\n\n        let new_path = PathBuf::from(\"users\").join(\"data-2\").join(\"2.txt\");\n        assert!(storage\n            .rename(orig_path.as_path(), new_path.as_path())\n            .await\n            .is_ok());\n\n        assert!(!store.exists(orig_path.as_path()).await.unwrap());\n        assert!(store.exists(new_path.as_path()).await.unwrap());\n    }\n\n    #[tokio::test]\n    async fn can_copy_file_path() {\n        let store = drivers::mem::new();\n\n        let strategy = Box::new(SingleStrategy::new(\"default\")) as Box<dyn StorageStrategy>;\n\n        let storage = Storage::new(BTreeMap::from([(\"default\".to_string(), store)]), strategy);\n\n        let store = storage.as_store(\"default\").unwrap();\n        let orig_path = PathBuf::from(\"users\").join(\"data\").join(\"1.txt\");\n        let file_content = Bytes::from(\"file content\");\n\n        assert!(storage\n            .upload(orig_path.as_path(), &file_content)\n            .await\n            .is_ok());\n\n        assert!(store.exists(orig_path.as_path()).await.unwrap());\n\n        let new_path = PathBuf::from(\"users\").join(\"data-2\").join(\"2.txt\");\n        assert!(storage\n            .copy(orig_path.as_path(), new_path.as_path())\n            .await\n            .is_ok());\n\n        assert!(store.exists(orig_path.as_path()).await.unwrap());\n        assert!(store.exists(new_path.as_path()).await.unwrap());\n    }\n}\n"
  },
  {
    "path": "src/storage/stream.rs",
    "content": "use std::pin::Pin;\nuse std::task::{Context, Poll};\n\nuse bytes::Bytes;\nuse futures_util::{Stream, StreamExt};\nuse opendal::Reader;\n\n/// A stream of bytes that abstracts over the underlying storage implementation.\n/// This type ensures that `OpenDAL` types are not exposed in the public API.\npub struct BytesStream {\n    inner: Pin<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>>,\n}\n\nimpl BytesStream {\n    /// Create a `BytesStream` from an `OpenDAL` `Reader`.\n    /// This is an internal method used by storage drivers.\n    pub(crate) async fn from_reader(reader: Reader) -> Result<Self, crate::storage::StorageError> {\n        // Convert the Reader into a stream of bytes\n        // The range parameter (..) means we want to read the entire content\n        let stream = reader\n            .into_bytes_stream(..)\n            .await\n            .map_err(crate::storage::StorageError::from)?;\n\n        // Convert opendal::Error to std::io::Error for uniform error handling\n        let mapped_stream = stream\n            .map(|result| result.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)));\n\n        Ok(Self {\n            inner: Box::pin(mapped_stream),\n        })\n    }\n\n    /// Collect the entire stream into a single `Bytes` buffer.\n    /// This method should be used carefully as it loads the entire content into memory.\n    ///\n    /// # Errors\n    ///\n    /// Returns an error if reading from the stream fails.\n    pub async fn collect(mut self) -> Result<Bytes, std::io::Error> {\n        let mut buffer = Vec::new();\n\n        while let Some(chunk) = self.next().await {\n            let chunk = chunk?;\n            buffer.extend_from_slice(&chunk);\n        }\n\n        Ok(Bytes::from(buffer))\n    }\n}\n\nimpl Stream for BytesStream {\n    type Item = Result<Bytes, std::io::Error>;\n\n    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {\n        self.inner.as_mut().poll_next(cx)\n    }\n}\n\n/// Extension trait for axum integration\nimpl BytesStream {\n    /// Convert the `BytesStream` into an axum `Body` for HTTP responses.\n    /// This enables zero-copy streaming directly to the HTTP response.\n    pub fn into_body(self) -> axum::body::Body {\n        axum::body::Body::from_stream(self)\n    }\n\n    /// Create a `BytesStream` from an axum request body.\n    /// This is useful for streaming uploads.\n    ///\n    /// Note: This requires the body to be converted to a stream first.\n    /// In practice, you might want to use axum's extractors directly.\n    pub fn from_body_stream<S>(stream: S) -> Self\n    where\n        S: Stream<Item = Result<Bytes, std::io::Error>> + Send + 'static,\n    {\n        Self {\n            inner: Box::pin(stream),\n        }\n    }\n}\n"
  },
  {
    "path": "src/task.rs",
    "content": "//! # Task Management Module\n//!\n//! This module defines the task management framework used to manage and execute\n//! tasks in a web server application.\nuse std::collections::BTreeMap;\n\nuse async_trait::async_trait;\n\nuse crate::{app::AppContext, errors::Error, Result};\n\n/// Struct representing a collection of task arguments.\n#[derive(Default, Debug)]\npub struct Vars {\n    /// A list of cli arguments.\n    pub cli: BTreeMap<String, String>,\n}\n\nimpl Vars {\n    /// Create [`Vars`] instance from cli arguments.\n    ///\n    /// # Arguments\n    ///\n    /// * `key` - A string representing the key.\n    /// * `value` - A string representing the value.\n    ///\n    /// # Example\n    ///\n    /// ```\n    /// use loco_rs::task::Vars;\n    ///\n    /// let args = vec![(\"key1\".to_string(), \"value\".to_string())];\n    /// let vars = Vars::from_cli_args(args);\n    /// ```\n    #[must_use]\n    pub fn from_cli_args(args: Vec<(String, String)>) -> Self {\n        Self {\n            cli: args.into_iter().collect(),\n        }\n    }\n\n    /// Retrieves the value associated with the given key from the `cli` list.\n    ///\n    /// # Errors\n    ///\n    /// Returns an error if the key does not exist.\n    ///\n    /// # Example\n    ///\n    /// ```\n    /// use loco_rs::task::Vars;\n    ///\n    /// let args = vec![(\"key1\".to_string(), \"value\".to_string())];\n    /// let vars = Vars::from_cli_args(args);\n    ///\n    /// assert!(vars.cli_arg(\"key1\").is_ok());\n    /// assert!(vars.cli_arg(\"not-exists\").is_err());\n    /// ```\n    pub fn cli_arg(&self, key: &str) -> Result<&String> {\n        self.cli\n            .get(key)\n            .ok_or(Error::Message(format!(\"the argument {key} does not exist\")))\n    }\n}\n\n/// Information about a task, including its name and details.\n#[allow(clippy::module_name_repetitions)]\n#[derive(Debug)]\npub struct TaskInfo {\n    pub name: String,\n    pub detail: String,\n}\n\n/// A trait defining the behavior of a task.\n#[async_trait]\npub trait Task: Send + Sync {\n    /// Get information about the task.\n    fn task(&self) -> TaskInfo;\n    /// Execute the task with the provided application context and variables.\n    async fn run(&self, app_context: &AppContext, vars: &Vars) -> Result<()>;\n}\n\n/// Managing and running tasks.\n#[derive(Default)]\npub struct Tasks {\n    registry: BTreeMap<String, Box<dyn Task>>,\n}\n\nimpl Tasks {\n    /// List all registered tasks with their information.\n    #[must_use]\n    pub fn list(&self) -> Vec<TaskInfo> {\n        self.registry.values().map(|t| t.task()).collect::<Vec<_>>()\n    }\n\n    /// List of all tasks names\n    #[must_use]\n    pub fn names(&self) -> Vec<String> {\n        self.registry\n            .values()\n            .map(|t| t.task().name)\n            .collect::<Vec<_>>()\n    }\n\n    /// Run a registered task by name with provided variables.\n    ///\n    /// # Errors\n    ///\n    /// Returns a [`Result`] if an task finished with error. mostly if the given\n    /// task is not found or an error to run the task.s\n    pub async fn run(&self, app_context: &AppContext, task: &str, vars: &Vars) -> Result<()> {\n        let task = self\n            .registry\n            .get(task)\n            .ok_or_else(|| Error::TaskNotFound(task.to_string()))?;\n        task.run(app_context, vars).await?;\n        Ok(())\n    }\n\n    /// Register a new task to the registry.\n    pub fn register(&mut self, task: impl Task + 'static) {\n        let name = task.task().name;\n        self.registry.insert(name, Box::new(task));\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use crate::tests_cfg;\n\n    #[tokio::test]\n    async fn test_vars_from_cli_args() {\n        let args = vec![\n            (\"key1\".to_string(), \"value1\".to_string()),\n            (\"key2\".to_string(), \"value2\".to_string()),\n        ];\n        let vars = Vars::from_cli_args(args);\n\n        assert_eq!(vars.cli.len(), 2);\n        assert_eq!(vars.cli.get(\"key1\"), Some(&\"value1\".to_string()));\n        assert_eq!(vars.cli.get(\"key2\"), Some(&\"value2\".to_string()));\n    }\n\n    #[tokio::test]\n    async fn test_vars_cli_arg() {\n        let args = vec![(\"key1\".to_string(), \"value1\".to_string())];\n        let vars = Vars::from_cli_args(args);\n\n        assert_eq!(vars.cli_arg(\"key1\").unwrap(), \"value1\");\n        assert!(vars.cli_arg(\"not-exists\").is_err());\n    }\n\n    #[tokio::test]\n    async fn test_tasks_registry() {\n        let mut tasks = Tasks::default();\n        tasks.register(tests_cfg::task::Foo);\n        tasks.register(tests_cfg::task::ParseArgs);\n\n        assert_eq!(tasks.names().len(), 2);\n        assert!(tasks.names().contains(&\"foo\".to_string()));\n        assert!(tasks.names().contains(&\"parse_args\".to_string()));\n    }\n\n    #[tokio::test]\n    async fn test_tasks_list() {\n        let mut tasks = Tasks::default();\n        tasks.register(tests_cfg::task::Foo);\n        tasks.register(tests_cfg::task::ParseArgs);\n\n        let task_infos = tasks.list();\n        assert_eq!(task_infos.len(), 2);\n\n        let names: Vec<String> = task_infos.iter().map(|info| info.name.clone()).collect();\n        let details: Vec<String> = task_infos.iter().map(|info| info.detail.clone()).collect();\n\n        assert!(names.contains(&\"foo\".to_string()));\n        assert!(names.contains(&\"parse_args\".to_string()));\n        assert!(details.contains(&\"run foo task\".to_string()));\n        assert!(details.contains(&\"Validate the paring args\".to_string()));\n    }\n\n    #[tokio::test]\n    async fn test_tasks_run_success() {\n        let mut tasks = Tasks::default();\n        tasks.register(tests_cfg::task::Foo);\n\n        let app_context = tests_cfg::app::get_app_context().await;\n        let vars = Vars::default();\n\n        let result = tasks.run(&app_context, \"foo\", &vars).await;\n        assert!(result.is_ok());\n    }\n\n    #[tokio::test]\n    async fn test_tasks_run_failure() {\n        let mut tasks = Tasks::default();\n        tasks.register(tests_cfg::task::ParseArgs);\n\n        let app_context = tests_cfg::app::get_app_context().await;\n        let vars = Vars::default();\n\n        // ParseArgs will fail with \"invalid args\" if app != \"loco\" or refresh != true\n        let result = tasks.run(&app_context, \"parse_args\", &vars).await;\n        assert!(result.is_err());\n\n        if let Err(Error::Message(msg)) = result {\n            assert_eq!(msg, \"invalid args\");\n        } else {\n            panic!(\"Expected Error::Message variant\");\n        }\n    }\n\n    #[tokio::test]\n    async fn test_tasks_run_with_args() {\n        let mut tasks = Tasks::default();\n        tasks.register(tests_cfg::task::ParseArgs);\n\n        let app_context = tests_cfg::app::get_app_context().await;\n        let args = vec![\n            (\"test\".to_string(), \"true\".to_string()),\n            (\"app\".to_string(), \"loco\".to_string()),\n        ];\n        let vars = Vars::from_cli_args(args);\n\n        // ParseArgs will succeed when app == \"loco\" and test == \"true\"\n        let result = tasks.run(&app_context, \"parse_args\", &vars).await;\n        assert!(result.is_ok());\n    }\n\n    #[tokio::test]\n    async fn test_tasks_run_not_found() {\n        let tasks = Tasks::default();\n        let app_context = tests_cfg::app::get_app_context().await;\n        let vars = Vars::default();\n\n        let result = tasks.run(&app_context, \"non_existent_task\", &vars).await;\n        assert!(result.is_err());\n\n        match result {\n            Err(Error::TaskNotFound(task_name)) => {\n                assert_eq!(task_name, \"non_existent_task\");\n            }\n            _ => panic!(\"Expected Error::TaskNotFound variant\"),\n        }\n    }\n\n    #[tokio::test]\n    async fn test_task_registration_and_override() {\n        // Create a custom task that will override Foo\n        struct CustomFoo;\n\n        #[async_trait]\n        impl Task for CustomFoo {\n            fn task(&self) -> TaskInfo {\n                TaskInfo {\n                    name: \"foo\".to_string(),\n                    detail: \"Updated foo task\".to_string(),\n                }\n            }\n\n            async fn run(&self, _app_context: &AppContext, _vars: &Vars) -> Result<()> {\n                Ok(())\n            }\n        }\n\n        let mut tasks = Tasks::default();\n        tasks.register(tests_cfg::task::Foo);\n        assert_eq!(tasks.names().len(), 1);\n\n        // Register a new task with the same name\n        tasks.register(CustomFoo);\n\n        // Should still have only one task (overwritten)\n        assert_eq!(tasks.names().len(), 1);\n\n        let task_infos = tasks.list();\n        assert_eq!(task_infos[0].detail, \"Updated foo task\");\n    }\n}\n"
  },
  {
    "path": "src/tera.rs",
    "content": "use tera::{Context, Tera};\n\nuse crate::Result;\n\npub fn render_string(tera_template: &str, locals: &serde_json::Value) -> Result<String> {\n    let text = Tera::one_off(tera_template, &Context::from_serialize(locals)?, false)?;\n    Ok(text)\n}\n"
  },
  {
    "path": "src/testing/db.rs",
    "content": "use crate::{\n    app::{AppContext, Hooks},\n    db, hash, Error, Result,\n};\nuse sqlx::{Pool, Postgres};\nuse std::future::Future;\nuse std::path::PathBuf;\nuse std::pin::Pin;\nuse tree_fs::TreeBuilder;\n\n/// Seeds data into the database.\n///\n///\n/// # Errors\n/// When seed fails\n///\n/// # Example\n///\n/// The provided example demonstrates how to boot the test case and run seed\n/// data.\n///\n/// ```rust,ignore\n/// use myapp::app::App;\n/// use loco_rs::testing::prelude::*;\n/// use migration::Migrator;\n///\n/// #[tokio::test]\n/// async fn test_create_user() {\n///     let boot = boot_test::<App, Migrator>().await;\n///     seed::<App>(&boot.app_context).await.unwrap();\n///\n///     /// .....\n///     assert!(false)\n/// }\n/// ```\npub async fn seed<H: Hooks>(ctx: &AppContext) -> Result<()> {\n    let path = std::path::Path::new(\"src/fixtures\");\n    H::seed(ctx, path).await\n}\n\n/// Initializes a test database connection.\n///\n/// # Errors\n/// Returns an error if could not create a new test db.\npub fn init_test_db_creation(conn_str: &str) -> Result<Box<dyn TestSupport>> {\n    if conn_str.starts_with(\"postgres://\") {\n        PostgresTest::new(conn_str).map(|test| Box::new(test) as Box<dyn TestSupport>)\n    } else if conn_str.starts_with(\"sqlite://\") {\n        SqliteTest::new(conn_str).map(|test| Box::new(test) as Box<dyn TestSupport>)\n    } else {\n        Ok(Box::new(Any::new(conn_str)))\n    }\n}\n\npub trait TestSupport: Send + Sync {\n    /// Initializes the database.\n    fn init_db<'a>(&'a self) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>>;\n    /// Returns the connection string.\n    fn get_connection_str(&self) -> &str;\n    /// Cleans up the database.\n    fn cleanup_db(&self);\n}\n\npub struct PostgresTest {\n    root_connection_string: String,\n    connection_string: String,\n    schema_name: String,\n}\n\nimpl PostgresTest {\n    /// Creates a new `PostgreSQL` test database.\n    ///\n    /// # Errors\n    /// Returns an error if could not create DB schema.\n    pub fn new(conn_str: &str) -> Result<Self> {\n        let db_name = db::extract_db_name(conn_str)?;\n\n        let current_timestamp = chrono::Utc::now().timestamp();\n        let test_schema_name: String = hash::random_string(10).to_lowercase();\n        let test_schema_name = format!(\"_loco_test_{test_schema_name}_{current_timestamp}\");\n\n        Ok(Self {\n            root_connection_string: conn_str.replace(db_name, \"postgres\"),\n            connection_string: conn_str.replace(db_name, &test_schema_name),\n            schema_name: test_schema_name,\n        })\n    }\n}\n\n#[async_trait::async_trait]\nimpl TestSupport for PostgresTest {\n    fn get_connection_str(&self) -> &str {\n        &self.connection_string\n    }\n\n    fn init_db<'a>(&'a self) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {\n        Box::pin(async move {\n            let pool = Pool::<Postgres>::connect(&self.root_connection_string)\n                .await\n                .expect(\"db connection should success\");\n            let query = format!(\"CREATE DATABASE {};\", self.schema_name);\n\n            sqlx::query(&query)\n                .execute(&pool)\n                .await\n                .expect(\"create DB schema\");\n        })\n    }\n\n    fn cleanup_db(&self) {\n        let connection_string = self.root_connection_string.clone();\n        let table_name = self.schema_name.clone();\n\n        tokio::task::spawn_blocking(move || {\n            let rt = tokio::runtime::Runtime::new().unwrap();\n\n            rt.block_on(async {\n                let pool = Pool::<Postgres>::connect(&connection_string)\n                    .await\n                    .expect(\"db connection should success\");\n                let query = format!(\"drop database if exists {table_name};\");\n                sqlx::query(&query)\n                    .execute(&pool)\n                    .await\n                    .expect(\"Drop database\");\n            });\n        });\n    }\n}\n\npub struct SqliteTest {\n    connection_string: String,\n    db_folder: PathBuf,\n    _tree: tree_fs::Tree, // Keep the tree alive while the test runs\n}\n\nimpl SqliteTest {\n    /// Prepare new `SQLite` connection string.\n    ///\n    /// # Errors\n    /// Returns an error if could not prepare the connection string\n    pub fn new(conn_str: &str) -> Result<Self> {\n        let db_name = db::extract_db_name(conn_str)?;\n\n        let tree = TreeBuilder::default()\n            .add_empty_file(\"test.sqlite\")\n            .create()\n            .map_err(|err| {\n                Error::string(&format!(\n                    \"could not create test database directory. err: {err}\"\n                ))\n            })?;\n\n        Ok(Self {\n            connection_string: conn_str.replace(\n                db_name,\n                &tree.root.join(\"test.sqlite\").display().to_string(),\n            ),\n            db_folder: tree.root.clone(),\n            _tree: tree,\n        })\n    }\n}\n\n#[async_trait::async_trait]\nimpl TestSupport for SqliteTest {\n    fn get_connection_str(&self) -> &str {\n        &self.connection_string\n    }\n    fn init_db<'a>(&'a self) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {\n        Box::pin(async move {})\n    }\n\n    fn cleanup_db(&self) {\n        std::fs::remove_dir_all(&self.db_folder).expect(\"Could not delete sqlite test db\");\n    }\n}\n\npub struct Any {\n    connection_string: String,\n}\nimpl Any {\n    #[must_use]\n    pub fn new(conn_str: &str) -> Self {\n        Self {\n            connection_string: conn_str.to_string(),\n        }\n    }\n}\n\nimpl TestSupport for Any {\n    fn get_connection_str(&self) -> &str {\n        &self.connection_string\n    }\n    fn init_db<'a>(&'a self) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {\n        Box::pin(async move {})\n    }\n\n    fn cleanup_db(&self) {}\n}\n\n#[cfg(test)]\nmod tests {\n\n    use super::*;\n    use sqlx::Row;\n    use std::{thread, time};\n\n    async fn schema_exists(pool: &sqlx::PgPool, schema_name: &str) -> bool {\n        let row =\n            sqlx::query(\"SELECT EXISTS (SELECT 1 FROM pg_catalog.pg_database  WHERE datname = $1)\")\n                .bind(schema_name)\n                .fetch_one(pool)\n                .await\n                .expect(\"check if table exists\");\n\n        println!(\"schema_name: {row:#?}\");\n        row.get(0)\n    }\n\n    #[tokio::test]\n    async fn sqlite_test_support() {\n        let conn = \"sqlite://test.sqlite?mode=rwc\";\n        let sqlite = SqliteTest::new(conn).expect(\"create Sqlite test support\");\n\n        sqlite.init_db().await;\n\n        assert!(sqlite.db_folder.exists());\n        sqlite.cleanup_db();\n        assert!(!sqlite.db_folder.exists());\n    }\n\n    #[tokio::test]\n    async fn postgres_test_support() {\n        let (conn, _container) = crate::tests_cfg::postgres::setup_postgres_container().await;\n        let pg: PostgresTest = PostgresTest::new(&conn).expect(\"create Postgres test support\");\n\n        pg.init_db().await;\n\n        let pool = Pool::<Postgres>::connect(&conn)\n            .await\n            .expect(\"db connection should success\");\n\n        assert!(schema_exists(&pool, &pg.schema_name).await);\n\n        pg.cleanup_db();\n\n        thread::sleep(time::Duration::from_secs(1));\n        assert!(!schema_exists(&pool, &pg.schema_name).await);\n    }\n}\n"
  },
  {
    "path": "src/testing/mod.rs",
    "content": "#[cfg(feature = \"with-db\")]\npub mod db;\npub mod prelude;\npub mod redaction;\npub mod request;\npub mod selector;\n"
  },
  {
    "path": "src/testing/prelude.rs",
    "content": "#[cfg(feature = \"with-db\")]\npub use crate::testing::db::*;\npub use crate::testing::{redaction::*, request::*, selector::*};\n"
  },
  {
    "path": "src/testing/redaction.rs",
    "content": "use std::sync::OnceLock;\n\nstatic CLEANUP_USER_MODEL: OnceLock<Vec<(&'static str, &'static str)>> = OnceLock::new();\nstatic CLEANUP_DATE: OnceLock<Vec<(&'static str, &'static str)>> = OnceLock::new();\nstatic CLEANUP_MODEL: OnceLock<Vec<(&'static str, &'static str)>> = OnceLock::new();\nstatic CLEANUP_MAIL: OnceLock<Vec<(&'static str, &'static str)>> = OnceLock::new();\n\npub fn get_cleanup_user_model() -> &'static Vec<(&'static str, &'static str)> {\n    CLEANUP_USER_MODEL.get_or_init(|| {\n        vec![\n            (\n                r\"([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})\",\n                \"PID\",\n            ),\n            (r\"password: (.*{60}),\", \"password: \\\"PASSWORD\\\",\"),\n            (r\"([A-Za-z0-9-_]*\\.[A-Za-z0-9-_]*\\.[A-Za-z0-9-_]*)\", \"TOKEN\"),\n        ]\n    })\n}\n\npub fn get_cleanup_date() -> &'static Vec<(&'static str, &'static str)> {\n    CLEANUP_DATE.get_or_init(|| {\n        vec![\n            (\n                r\"\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?\\+\\d{2}:\\d{2}\",\n                \"DATE\",\n            ), // with tz\n            (r\"\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d+\", \"DATE\"),\n            (r\"(\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2})\", \"DATE\"),\n        ]\n    })\n}\n\npub fn get_cleanup_model() -> &'static Vec<(&'static str, &'static str)> {\n    CLEANUP_MODEL.get_or_init(|| vec![(r\"id: \\d+,\", \"id: ID\")])\n}\n\npub fn get_cleanup_mail() -> &'static Vec<(&'static str, &'static str)> {\n    CLEANUP_MAIL.get_or_init(|| {\n        vec![\n            (r\"[0-9A-Za-z]+{40}\", \"IDENTIFIER\"),\n            (\n                r\"\\w+, \\d{1,2} \\w+ \\d{4} \\d{2}:\\d{2}:\\d{2} [+-]\\d{4}\",\n                \"DATE\",\n            ),\n            (\n                r\"([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})\",\n                \"RANDOM_ID\",\n            ),\n            (\n                r\"([0-9a-fA-F]{8}-[0-9a-fA-F]{4})-[0-9a-fA-F]{4}-.*[0-9a-fA-F]{2}\",\n                \"RANDOM_ID\",\n            ),\n        ]\n    })\n}\n\n/// Combines cleanup filters from various categories (user model, date, and\n/// model) into one list. This is used for data cleaning and pattern\n/// replacement.\n///\n/// # Example\n///\n/// The provided example demonstrates how to efficiently clean up a user model.\n/// This process is particularly valuable when you need to capture a snapshot of\n/// user model data that includes dynamic elements such as incrementing IDs,\n/// automatically generated PIDs, creation/update timestamps, and similar\n/// attributes.\n///\n/// ```rust,ignore\n/// use myapp::app::App;\n/// use loco_rs::testing::prelude::*;\n/// use migration::Migrator;\n///\n/// #[tokio::test]\n/// async fn test_create_user() {\n///     let boot = boot_test::<App, Migrator>().await;\n///\n///     // Create a user and save into the database.\n///\n///     // capture the snapshot and cleanup the data.\n///     with_settings!({\n///         filters => cleanup_user_model()\n///     }, {\n///         assert_debug_snapshot!(saved_user);\n///     });\n/// }\n/// ```\n#[must_use]\npub fn cleanup_user_model() -> Vec<(&'static str, &'static str)> {\n    let mut combined_filters = get_cleanup_user_model().clone();\n    combined_filters.extend(get_cleanup_date().iter().copied());\n    combined_filters.extend(get_cleanup_model().iter().copied());\n    combined_filters\n}\n\n/// Combines cleanup filters from emails  that can be dynamic\n#[must_use]\npub fn cleanup_email() -> Vec<(&'static str, &'static str)> {\n    let mut combined_filters = get_cleanup_mail().clone();\n    combined_filters.extend(get_cleanup_date().iter().copied());\n    combined_filters\n}\n"
  },
  {
    "path": "src/testing/request.rs",
    "content": "use std::net::SocketAddr;\n\nuse axum_test::{TestServer, TestServerConfig};\nuse tokio::net::TcpListener;\n\n#[cfg(feature = \"with-db\")]\nuse crate::Error;\n\nuse crate::{\n    app::{AppContext, Hooks},\n    boot::{self, BootResult},\n    config::Server,\n    environment::Environment,\n    Result,\n};\n#[cfg(feature = \"with-db\")]\nuse std::ops::Deref;\n\n#[cfg(feature = \"with-db\")]\npub struct BootResultWrapper {\n    inner: BootResult,\n    test_db: Box<dyn super::db::TestSupport>,\n}\n\n#[cfg(feature = \"with-db\")]\nimpl BootResultWrapper {\n    #[must_use]\n    pub fn new(boot: BootResult, test_db: Box<dyn super::db::TestSupport>) -> Self {\n        Self {\n            inner: boot,\n            test_db,\n        }\n    }\n}\n\n#[cfg(feature = \"with-db\")]\nimpl Deref for BootResultWrapper {\n    type Target = BootResult;\n\n    fn deref(&self) -> &Self::Target {\n        &self.inner\n    }\n}\n\n#[cfg(feature = \"with-db\")]\nimpl Drop for BootResultWrapper {\n    fn drop(&mut self) {\n        self.test_db.cleanup_db();\n    }\n}\n\n/// Configuration for making requests in the test server.\npub struct RequestConfig {\n    /// Determines whether cookies should be saved for future requests.\n    pub save_cookies: bool,\n    /// The default content type for all requests.\n    pub default_content_type: Option<String>,\n    /// The default scheme to use for requests (e.g., \"http\" or \"https\").\n    pub default_scheme: String,\n}\n\nimpl Default for RequestConfig {\n    fn default() -> Self {\n        RequestConfigBuilder::new().build()\n    }\n}\n\n/// Builder pattern for constructing [`RequestConfig`] instances.\npub struct RequestConfigBuilder {\n    save_cookies: bool,\n    default_content_type: Option<String>,\n    default_scheme: String,\n}\n\nimpl RequestConfigBuilder {\n    /// Creates a new [`RequestConfigBuilder`] with default values.\n    #[must_use]\n    pub fn new() -> Self {\n        Self {\n            save_cookies: false,\n            default_content_type: Some(\"application/json\".to_string()),\n            default_scheme: \"http\".to_string(),\n        }\n    }\n\n    /// Sets whether cookies should be saved for future requests.\n    #[must_use]\n    pub fn save_cookies(mut self, save: bool) -> Self {\n        self.save_cookies = save;\n        self\n    }\n\n    /// Sets the default content type for requests.\n    #[must_use]\n    pub fn default_content_type<S: Into<String>>(mut self, content_type: S) -> Self {\n        self.default_content_type = Some(content_type.into());\n        self\n    }\n\n    /// Sets the default scheme to use for requests.\n    #[must_use]\n    pub fn default_scheme<S: Into<String>>(mut self, scheme: S) -> Self {\n        self.default_scheme = scheme.into();\n        self\n    }\n\n    /// Builds and returns a `RequestConfig` instance.\n    #[must_use]\n    pub fn build(self) -> RequestConfig {\n        RequestConfig {\n            save_cookies: self.save_cookies,\n            default_content_type: self.default_content_type,\n            default_scheme: self.default_scheme,\n        }\n    }\n}\n\nimpl Default for RequestConfigBuilder {\n    fn default() -> Self {\n        Self::new()\n    }\n}\n\n// Implement the From trait for automatic conversion\nimpl From<RequestConfig> for TestServerConfig {\n    fn from(request_config: RequestConfig) -> Self {\n        Self {\n            default_content_type: request_config.default_content_type,\n            save_cookies: request_config.save_cookies,\n            ..Default::default()\n        }\n    }\n}\n\n/// The port on which the test server will run.\npub const TEST_PORT_SERVER: i32 = 5555;\n\n/// The hostname to which the test server binds.\npub const TEST_BINDING_SERVER: &str = \"localhost\";\n\n/// Constructs and returns the base URL used for the test server.\n#[must_use]\npub fn get_base_url_port(port: i32) -> String {\n    format!(\"http://{TEST_BINDING_SERVER}:{port}/\")\n}\n\n/// Returns a unique port number. Usually increments by 1 starting from 59126\n///\n/// # Panics\n///\n/// Will panic if binding to test server address fails or if getting the local address fails\npub async fn get_available_port() -> i32 {\n    let addr = format!(\"{TEST_BINDING_SERVER}:0\");\n    let listener = TcpListener::bind(addr)\n        .await\n        .expect(\"Failed to bind to address\");\n\n    i32::from(\n        listener\n            .local_addr()\n            .expect(\"Failed to get local address\")\n            .port(),\n    )\n}\n\n/// Bootstraps test application with test environment hard coded.\n///\n/// # Example\n///\n/// The provided example demonstrates how to boot the test case with the\n/// application context.\n///\n/// ```rust,ignore\n/// use myapp::app::App;\n/// use loco_rs::testing::prelude::*;\n///\n/// #[tokio::test]\n/// async fn test_create_user() {\n///     let boot = boot_test::<App>().await;\n/// }\n/// ```\n///\n/// # Errors\n/// when could not bootstrap the test environment\npub async fn boot_test<H: Hooks>() -> Result<BootResult> {\n    let config = H::load_config(&Environment::Test).await?;\n    let boot = H::boot(boot::StartMode::ServerOnly, &Environment::Test, config).await?;\n    Ok(boot)\n}\n\n/// Bootstraps the test application with a test environment and creates a new database.\n///\n/// This function initializes the test environment and sets up a fresh database for testing.\n/// The test database will be used during the test, and it will be cleaned up once the test completes.\n///\n/// ```rust,ignore\n/// use myapp::app::App;\n/// use loco_rs::testing::prelude::*;\n///\n/// #[tokio::test]\n/// async fn test_create_user() {\n///     let boot = boot_test_with_create_db::<App>().await;\n/// }\n/// ```\n///\n/// # Errors\n/// when could not bootstrap the test environment\n#[cfg(feature = \"with-db\")]\npub async fn boot_test_with_create_db<H: Hooks>() -> Result<BootResultWrapper> {\n    let mut config = H::load_config(&Environment::Test).await?;\n    let test_db = super::db::init_test_db_creation(&config.database.uri)?;\n    test_db.init_db().await;\n    config.database.uri = test_db.get_connection_str().to_string();\n    let boot = match H::boot(boot::StartMode::ServerOnly, &Environment::Test, config).await {\n        Ok(boot) => boot,\n        Err(err) => {\n            test_db.cleanup_db();\n            return Err(Error::string(&err.to_string()));\n        }\n    };\n\n    Ok(BootResultWrapper::new(boot, test_db))\n}\n\n/// Bootstraps test application with test environment hard coded,\n/// and with a unique port.\n///\n/// # Errors\n/// when could not bootstrap the test environment\n///\n/// # Example\n///\n/// The provided example demonstrates how to boot the test case with the\n/// application context, and a with a unique port.\n///\n/// ```rust,ignore\n/// use myapp::app::App;\n/// use loco_rs::testing::prelude::*;\n///\n/// #[tokio::test]\n/// async fn test_create_user() {\n///     let port = get_available_port().await;\n///     let boot = boot_test_unique_port::<App>(Some(port)).await;\n///\n///     /// .....\n///     assert!(false)\n/// }\npub async fn boot_test_unique_port<H: Hooks>(port: Option<i32>) -> Result<BootResult> {\n    let mut config = H::load_config(&Environment::Test).await?;\n    config.server = Server {\n        port: port.unwrap_or(TEST_PORT_SERVER),\n        binding: TEST_BINDING_SERVER.to_string(),\n        ..config.server\n    };\n    H::boot(boot::StartMode::ServerOnly, &Environment::Test, config).await\n}\n\n#[allow(clippy::future_not_send)]\nasync fn request_internal<F, Fut>(callback: F, boot: &BootResult, test_server_config: RequestConfig)\nwhere\n    F: FnOnce(TestServer, AppContext) -> Fut,\n    Fut: std::future::Future<Output = ()>,\n{\n    let routes = boot.router.clone().unwrap();\n    let server = TestServer::new_with_config(\n        routes.into_make_service_with_connect_info::<SocketAddr>(),\n        test_server_config,\n    )\n    .unwrap();\n\n    callback(server, boot.app_context.clone()).await;\n}\n\n/// Executes a test server request using the provided callback and the default boot process.\n///\n/// This function will boot the test environment without creating a new database.\n/// It takes a `callback` function that is called with the test server and application context.\n///\n/// # Panics\n/// When could not initialize the test request.this errors can be when could not\n/// initialize the test app\n///\n/// # Example\n///\n/// The provided example demonstrates how to create a test that check\n/// application HTTP endpoints\n///\n/// ```rust,ignore\n/// use myapp::app::App;\n/// use loco_rs::testing::prelude::*;\n///\n/// #[tokio::test]\n/// #[serial]\n/// async fn can_register() {\n///     request::<App, _, _>(|request, ctx| async move {\n///         let response = request.post(\"/auth/register\").json(&serde_json::json!({})).await;\n///     })\n///     .await;\n/// }\n/// ```\n#[allow(clippy::future_not_send)]\npub async fn request<H: Hooks, F, Fut>(callback: F)\nwhere\n    F: FnOnce(TestServer, AppContext) -> Fut,\n    Fut: std::future::Future<Output = ()>,\n{\n    request_with_config::<H, F, Fut>(RequestConfig::default(), callback).await;\n}\n/// Executes a test server request with a created database using the provided callback.\n///\n/// This function will boot the test environment and create a new database for the test.\n/// It takes a `callback` function that is called with the test server and application context.\n///\n/// ```rust,ignore\n/// use myapp::app::App;\n///\n/// #[tokio::test]\n/// async fn can_register() {\n///     request_with_create_db::<App, _, _>(|request, ctx| async move {\n///         let response = request.post(\"/auth/register\").json(&serde_json::json!({})).await;\n///     })\n///     .await;\n/// }\n/// ```\n///\n/// # Panics\n/// When could not initialize the test request.this errors can be when could not\n/// initialize the test app\n#[allow(clippy::future_not_send)]\n#[cfg(feature = \"with-db\")]\npub async fn request_with_create_db<H: Hooks, F, Fut>(callback: F)\nwhere\n    F: FnOnce(TestServer, AppContext) -> Fut,\n    Fut: std::future::Future<Output = ()>,\n{\n    request_config_with_create_db::<H, F, Fut>(RequestConfig::default(), callback).await;\n}\n\n/// Executes a test server request using a custom [`RequestConfig`].\n///\n/// This function will boot the test environment without creating a new database.\n/// It takes a `config` parameter to customize request settings and a `callback`\n/// function that is called with the test server and application context.\n///\n/// # Panics\n/// When the test request cannot be initialized, such as when the test app fails to start.\n///\n/// # Example\n/// ```rust,ignore\n/// let config = RequestConfigBuilder::new().save_cookies(true).build();\n/// request_with_config::<App, _, _>(config, |request, ctx| async move {\n///     let response = request.get(\"/endpoint\").await;\n/// });\n/// ```\npub async fn request_with_config<H: Hooks, F, Fut>(config: RequestConfig, callback: F)\nwhere\n    F: FnOnce(TestServer, AppContext) -> Fut,\n    Fut: std::future::Future<Output = ()>,\n{\n    let boot: BootResult = boot_test::<H>().await.unwrap();\n    request_internal::<F, Fut>(callback, &boot, config).await;\n}\n\n/// Executes a test server request with a created database using a custom [`RequestConfig`].\n///\n/// This function initializes the test environment, sets up a fresh database, and then runs\n/// the provided callback function with the test server and application context.\n/// The test database will be cleaned up after the test completes.\n///\n/// # Panics\n/// When the test request cannot be initialized, such as when the test app fails to start.\n///\n/// # Example\n/// ```rust,ignore\n/// let config = RequestConfigBuilder::new().save_cookies(true).build();\n/// request_config_with_create_db::<App, _, _>(config, |request, ctx| async move {\n///     let response = request.get(\"/endpoint\").await;\n/// });\n/// ```\n#[allow(clippy::future_not_send)]\n#[cfg(feature = \"with-db\")]\npub async fn request_config_with_create_db<H: Hooks, F, Fut>(config: RequestConfig, callback: F)\nwhere\n    F: FnOnce(TestServer, AppContext) -> Fut,\n    Fut: std::future::Future<Output = ()>,\n{\n    let boot_wrapper: BootResultWrapper = boot_test_with_create_db::<H>().await.unwrap();\n    request_internal::<F, Fut>(callback, &boot_wrapper.inner, config).await;\n}\n"
  },
  {
    "path": "src/testing/selector.rs",
    "content": "use scraper::{Html, Selector};\n\n/// Asserts that an element matching the given CSS selector exists in the\n/// provided HTML.\n///\n/// # Example\n///\n/// ```rust\n/// use loco_rs::testing::prelude::*;\n///\n/// let html = r#\"\n///   <html>\n///       <body>\n///           <div class=\"some-class\">Some content here</div>\n///       </body>\n///   </html>\"#;\n/// assert_css_exists(html, \".some-class\");\n/// ```\n///\n/// # Panics\n///\n/// This function will panic if no element matching the selector is found in the\n/// HTML.\npub fn assert_css_exists(html: &str, selector: &str) {\n    let document = Html::parse_document(html);\n    let parsed_selector = Selector::parse(selector).unwrap();\n    assert!(\n        document.select(&parsed_selector).count() > 0,\n        \"Element matching selector '{selector:?}' not found\"\n    );\n}\n\n/// Asserts that an element matching the given CSS selector does **not** exist\n/// in the provided HTML.\n///\n/// # Example\n///\n/// ```rust\n/// use loco_rs::testing::prelude::*;\n///\n/// let html = r#\"\n///   <html>\n///       <body>\n///           <div class=\"some-class\">Some content here</div>\n///       </body>\n///   </html>\"#;\n/// assert_css_not_exists(html, \".nonexistent-class\");\n/// ```\n///\n/// # Panics\n///\n/// This function will panic if an element matching the selector is found in the\n/// HTML.\npub fn assert_css_not_exists(html: &str, selector: &str) {\n    let document = Html::parse_document(html);\n    let parsed_selector = Selector::parse(selector).unwrap();\n    assert!(\n        document.select(&parsed_selector).count() == 0,\n        \"Element matching selector '{selector:?}' should not exist\"\n    );\n}\n\n/// Asserts that the text content of an element matching the given CSS selector\n/// exactly matches the expected text.\n///\n/// # Example\n///\n/// ```rust\n/// use loco_rs::testing::prelude::*;\n///\n/// let html = r#\"\n///   <html>\n///       <body>\n///           <h1 class=\"title\">Welcome to Loco</h1>\n///       </body>\n///   </html>\"#;\n/// assert_css_eq(html, \"h1.title\", \"Welcome to Loco\");\n/// ```\n///\n/// # Panics\n///\n/// This function will panic if the text of the found element does not match the\n/// expected text.\npub fn assert_css_eq(html: &str, selector: &str, expected_text: &str) {\n    let document = Html::parse_document(html);\n    let parsed_selector = Selector::parse(selector).unwrap();\n    let mut found = false;\n\n    for element in document.select(&parsed_selector) {\n        let text = element.text().collect::<Vec<_>>().join(\"\");\n        if text == expected_text {\n            found = true;\n            break;\n        }\n    }\n\n    assert!(\n        found,\n        \"Text does not match: Expected '{expected_text:?}' but found a different value or no \\\n         match for selector '{selector:?}'\"\n    );\n}\n\n/// Asserts that an `<a>` element matching the given CSS selector has the `href`\n/// attribute with the specified value.\n///\n/// # Example\n///\n/// ```rust\n/// use loco_rs::testing::prelude::*;\n///\n/// let html = r#\"\n///   <html>\n///       <body>\n///           <a href=\"https://loco.rs\">Link</a>\n///       </body>\n///   </html>\"#;\n/// assert_link(html, \"a\", \"https://loco.rs\");\n/// ```\n///\n/// # Panics\n///\n/// This function will panic if no `<a>` element matching the selector is found,\n/// if the element does not have the `href` attribute, or if the `href`\n/// attribute's value does not match the expected value.\npub fn assert_link(html: &str, selector: &str, expected_href: &str) {\n    // Use `assert_attribute_eq` to check that the `href` attribute exists and\n    // matches the expected value\n    assert_attribute_eq(html, selector, \"href\", expected_href);\n}\n\n/// Asserts that an element matching the given CSS selector has the specified\n/// attribute.\n///\n/// # Example\n///\n/// ```rust\n/// use loco_rs::testing::prelude::*;\n///\n/// let html = r#\"\n///   <html>\n///       <body>\n///           <button onclick=\"alert('clicked')\">Loco Website</button>\n///           <a href=\"https://loco.rs\">Link</a>\n///       </body>\n///   </html>\"#;\n/// assert_attribute_exists(html, \"button\", \"onclick\");\n/// assert_attribute_exists(html, \"a\", \"href\");\n/// ```\n///\n/// # Panics\n///\n/// This function will panic if no element matching the selector is found, or if\n/// the element does not have the specified attribute.\npub fn assert_attribute_exists(html: &str, selector: &str, attribute: &str) {\n    let document = Html::parse_document(html);\n    let parsed_selector = Selector::parse(selector).unwrap();\n\n    let mut found = false;\n\n    for element in document.select(&parsed_selector) {\n        if element.value().attr(attribute).is_some() {\n            found = true;\n            break;\n        }\n    }\n\n    assert!(\n        found,\n        \"Element matching selector '{selector:?}' does not have the attribute '{attribute}'\"\n    );\n}\n\n/// Asserts that the specified attribute of an element matching the given CSS\n/// selector matches the expected value.\n///\n/// # Example\n///\n/// ```rust\n/// use loco_rs::testing::prelude::*;\n///\n/// let html = r#\"\n///   <html>\n///       <body>\n///           <button onclick=\"alert('clicked')\">Loco Website</button>\n///           <a href=\"https://loco.rs\">Link</a>\n///       </body>\n///   </html>\"#;\n/// assert_attribute_exists(html, \"button\", \"onclick\");\n/// assert_attribute_exists(html, \"a\", \"href\");\n/// ```\n///\n/// # Panics\n///\n/// This function will panic if no element matching the selector is found, if\n/// the element does not have the specified attribute, or if the attribute's\n/// value does not match the expected value.\npub fn assert_attribute_eq(html: &str, selector: &str, attribute: &str, expected_value: &str) {\n    let document = Html::parse_document(html);\n    let parsed_selector = Selector::parse(selector).unwrap();\n\n    let mut found = false;\n\n    for element in document.select(&parsed_selector) {\n        if let Some(attr_value) = element.value().attr(attribute) {\n            if attr_value == expected_value {\n                found = true;\n                break;\n            }\n        }\n    }\n\n    assert!(\n        found,\n        \"Expected attribute '{attribute}' with value '{expected_value}' for selector \\\n         '{selector:?}', but found a different value or no value.\"\n    );\n}\n\n/// Asserts that the number of elements matching the given CSS selector in the\n/// provided HTML is exactly the expected count.\n///\n/// # Example\n///\n/// ```rust\n/// use loco_rs::testing::prelude::*;\n///\n/// let html = r#\"\n///   <html>\n///       <body>\n///         <ul id=\"posts\">\n///             <li>Post 1</li>\n///             <li>Post 2</li>\n///             <li>Post 3</li>\n///         </ul>  \n///       </body>\n///   </html>\"#;\n/// assert_count(html, \"ul#posts li\", 3);\n/// ```\n///\n/// # Panics\n///\n/// This function will panic if the number of elements matching the selector is\n/// not equal to the expected count.\npub fn assert_count(html: &str, selector: &str, expected_count: usize) {\n    let document = Html::parse_document(html);\n    let parsed_selector = Selector::parse(selector).unwrap();\n\n    let count = document.select(&parsed_selector).count();\n\n    assert!(\n        count == expected_count,\n        \"Expected {expected_count} elements matching selector '{selector:?}', but found {count} \\\n         elements.\"\n    );\n}\n\n/// Collects the text content of all elements matching the given CSS selector\n/// and asserts that they match the expected text.\n///\n/// # Example\n///\n/// ```rust\n/// use loco_rs::testing::prelude::*;\n///\n/// let html = r#\"\n///   <html>\n///       <body>\n///         <ul id=\"posts\">\n///             <li>Post 1</li>\n///             <li>Post 2</li>\n///             <li>Post 3</li>\n///         </ul>  \n///       </body>\n///   </html>\"#;\n/// assert_css_eq_list(html, \"ul#posts li\", &[\"Post 1\", \"Post 2\", \"Post 3\"]);\n/// ```\n///\n/// # Panics\n///\n/// This function will panic if the text content of the elements does not match\n/// the expected values.\npub fn assert_css_eq_list(html: &str, selector: &str, expected_texts: &[&str]) {\n    let document = Html::parse_document(html);\n    let parsed_selector = Selector::parse(selector).unwrap();\n\n    let collected_texts: Vec<String> = document\n        .select(&parsed_selector)\n        .map(|element| element.text().collect::<Vec<_>>().concat())\n        .collect();\n\n    assert_eq!(\n        collected_texts, expected_texts,\n        \"Expected texts {expected_texts:?}, but found {collected_texts:?}.\"\n    );\n}\n\n/// Parses the given HTML string and selects the elements matching the specified CSS selector.\n///\n/// # Examples\n///\n/// ```rust\n/// use loco_rs::testing::prelude::*;\n///\n/// let html = r#\"\n///     <html>\n///         <body>\n///             <div class=\"item\">Item 1</div>\n///             <div class=\"item\">Item 2</div>\n///             <div class=\"item\">Item 3</div>\n///         </body>\n///     </html>\n/// \"#;\n/// let items = select(html, \".item\");\n/// assert_eq!(items, vec![\"<div class=\\\"item\\\">Item 1</div>\", \"<div class=\\\"item\\\">Item 2</div>\", \"<div class=\\\"item\\\">Item 3</div>\"]);\n/// ```\n///\n/// # Panics\n///\n/// This function will panic when could not pase the selector\n#[must_use]\npub fn select(html: &str, selector: &str) -> Vec<String> {\n    let document = Html::parse_document(html);\n    let parsed_selector = Selector::parse(selector).unwrap();\n    document\n        .select(&parsed_selector)\n        .map(|element| element.html())\n        .collect()\n}\n\n// Test cases\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    fn setup_test_html() -> &'static str {\n        r#\"\n    <html>\n        <body>\n            <div class=\"some-class\">Some content here</div>\n            <div class=\"another-class\">Another content here</div>\n            <h1 class=\"title\">Welcome to Loco</h1>\n            <button onclick=\"alert('clicked')\">Loco Website</button>\n            <a href=\"https://loco.rs\">Link</a>\n            <ul id=\"posts\">\n                <li>Post 1</li>\n                <li>Post 2</li>\n                <li>Post 3</li>\n            </ul>\n\n            <body>\n                <table id=\"posts_table\">\n                    <tr>\n                        <td>Post 1</td>\n                        <td>Author 1</td>\n                    </tr>\n                    <tr>\n                        <td>Post 2</td>\n                        <td>Author 2</td>\n                    </tr>\n                    <tr>\n                        <td>Post 3</td>\n                        <td>Author 3</td>\n                    </tr>\n                </table>\n            </body>\n        </body>\n    </html>\n    \"#\n    }\n\n    #[test]\n    fn test_assert_css_exists() {\n        let html = setup_test_html();\n\n        assert_css_exists(html, \".some-class\");\n\n        let result = std::panic::catch_unwind(|| {\n            assert_css_exists(html, \".nonexistent-class\");\n        });\n        assert!(result.is_err(), \"Expected panic for non-existent selector\");\n        if let Err(panic_message) = result {\n            let panic_message = panic_message.downcast_ref::<String>().unwrap();\n            assert_eq!(\n                panic_message,\n                &\"Element matching selector '\\\".nonexistent-class\\\"' not found\"\n            );\n        }\n    }\n\n    #[test]\n    fn test_assert_css_not_exists() {\n        let html = setup_test_html();\n\n        assert_css_not_exists(html, \".nonexistent-class\");\n\n        let result = std::panic::catch_unwind(|| {\n            assert_css_not_exists(html, \".some-class\");\n        });\n        assert!(result.is_err(), \"Expected panic for non-existent selector\");\n        if let Err(panic_message) = result {\n            let panic_message = panic_message.downcast_ref::<String>().unwrap();\n            assert_eq!(\n                panic_message,\n                &\"Element matching selector '\\\".some-class\\\"' should not exist\"\n            );\n        }\n    }\n\n    #[test]\n    fn test_assert_css_eq() {\n        let html = setup_test_html();\n\n        assert_css_eq(html, \"h1.title\", \"Welcome to Loco\");\n\n        let result = std::panic::catch_unwind(|| {\n            assert_css_eq(html, \"h1.title\", \"Wrong text\");\n        });\n        assert!(result.is_err(), \"Expected panic for mismatched text\");\n        if let Err(panic_message) = result {\n            let panic_message = panic_message.downcast_ref::<String>().unwrap();\n            assert_eq!(\n                panic_message,\n                &\"Text does not match: Expected '\\\"Wrong text\\\"' but found a different value or \\\n                  no match for selector '\\\"h1.title\\\"'\"\n            );\n        }\n    }\n\n    #[test]\n    fn test_assert_link() {\n        let html = setup_test_html();\n\n        assert_link(html, \"a\", \"https://loco.rs\");\n\n        let result = std::panic::catch_unwind(|| {\n            assert_link(html, \"a\", \"https://nonexistent.com\");\n        });\n\n        assert!(result.is_err());\n        if let Err(panic_message) = result {\n            let panic_message = panic_message.downcast_ref::<String>().unwrap();\n            assert_eq!(\n                panic_message,\n                &\"Expected attribute 'href' with value 'https://nonexistent.com' for selector \\\n                  '\\\"a\\\"', but found a different value or no value.\"\n            );\n        }\n    }\n\n    #[test]\n    fn test_assert_attribute_exists() {\n        let html = setup_test_html();\n\n        assert_attribute_exists(html, \"button\", \"onclick\");\n        assert_attribute_exists(html, \"a\", \"href\");\n\n        let result = std::panic::catch_unwind(|| {\n            assert_attribute_exists(html, \"button\", \"href\");\n        });\n        if let Err(panic_message) = result {\n            let panic_message = panic_message.downcast_ref::<String>().unwrap();\n            assert_eq!(\n                panic_message,\n                &\"Element matching selector '\\\"button\\\"' does not have the attribute 'href'\"\n            );\n        }\n    }\n\n    #[test]\n    fn test_assert_attribute_eq() {\n        let html = setup_test_html();\n        assert_attribute_eq(html, \"button\", \"onclick\", \"alert('clicked')\");\n        assert_attribute_eq(html, \"a\", \"href\", \"https://loco.rs\");\n\n        let result = std::panic::catch_unwind(|| {\n            assert_attribute_eq(html, \"button\", \"onclick\", \"alert('wrong')\");\n        });\n\n        assert!(result.is_err());\n        if let Err(panic_message) = result {\n            let panic_message = panic_message.downcast_ref::<String>().unwrap();\n            assert_eq!(\n                panic_message,\n                &\"Expected attribute 'onclick' with value 'alert('wrong')' for selector \\\n                  '\\\"button\\\"', but found a different value or no value.\"\n            );\n        }\n    }\n\n    #[test]\n    fn test_assert_count() {\n        let html = setup_test_html();\n        assert_count(html, \"ul#posts li\", 3);\n\n        let result = std::panic::catch_unwind(|| {\n            assert_count(html, \"ul#posts li\", 1);\n        });\n\n        assert!(result.is_err());\n        if let Err(panic_message) = result {\n            let panic_message = panic_message.downcast_ref::<String>().unwrap();\n            assert_eq!(\n                panic_message,\n                &\"Expected 1 elements matching selector '\\\"ul#posts li\\\"', but found 3 elements.\"\n            );\n        }\n    }\n\n    #[test]\n    fn test_assert_css_eq_list() {\n        let html = setup_test_html();\n        assert_css_eq_list(html, \"ul#posts li\", &[\"Post 1\", \"Post 2\", \"Post 3\"]);\n\n        let result = std::panic::catch_unwind(|| {\n            assert_css_eq_list(html, \"ul#posts li\", &[\"Post 1\", \"Post 2\", \"Wrong Post\"]);\n        });\n\n        assert!(result.is_err());\n        if let Err(panic_message) = result {\n            let panic_message = panic_message.downcast_ref::<String>().unwrap();\n            assert_eq!(\n                panic_message,\n                &\"assertion `left == right` failed: Expected texts [\\\"Post 1\\\", \\\"Post 2\\\", \\\n                  \\\"Wrong Post\\\"], but found [\\\"Post 1\\\", \\\"Post 2\\\", \\\"Post 3\\\"].\\n  left: \\\n                  [\\\"Post 1\\\", \\\"Post 2\\\", \\\"Post 3\\\"]\\n right: [\\\"Post 1\\\", \\\"Post 2\\\", \\\"Wrong \\\n                  Post\\\"]\"\n            );\n        }\n    }\n\n    #[test]\n    fn test_assert_css_eq_list_table() {\n        let html = setup_test_html();\n        assert_css_eq_list(\n            html,\n            \"table tr td\",\n            &[\n                \"Post 1\", \"Author 1\", \"Post 2\", \"Author 2\", \"Post 3\", \"Author 3\",\n            ],\n        );\n\n        let result = std::panic::catch_unwind(|| {\n            assert_css_eq_list(html, \"table#posts_t tr td\", &[\"Post 1\", \"Post 2\", \"Post 3\"]);\n        });\n\n        assert!(result.is_err());\n        if let Err(panic_message) = result {\n            let panic_message = panic_message.downcast_ref::<String>().unwrap();\n            assert_eq!(\n                panic_message,\n                &\"assertion `left == right` failed: Expected texts [\\\"Post 1\\\", \\\"Post 2\\\", \\\n                  \\\"Post 3\\\"], but found [].\\n  left: []\\n right: [\\\"Post 1\\\", \\\"Post 2\\\", \\\"Post \\\n                  3\\\"]\"\n            );\n        }\n    }\n\n    #[test]\n    fn test_select() {\n        let html = setup_test_html();\n        assert_eq!(\n            select(html, \".some-class\"),\n            vec![\"<div class=\\\"some-class\\\">Some content here</div>\"]\n        );\n        assert_eq!(select(html, \"ul\"), vec![\"<ul id=\\\"posts\\\">\\n                <li>Post 1</li>\\n                <li>Post 2</li>\\n                <li>Post 3</li>\\n            </ul>\"]);\n    }\n}\n"
  },
  {
    "path": "src/tests_cfg/app.rs",
    "content": "use crate::{\n    app::{AppContext, SharedStore},\n    cache,\n    environment::Environment,\n    storage::{self, Storage},\n    tests_cfg::config::test_config,\n};\n\npub async fn get_app_context() -> AppContext {\n    // Always use in-memory cache for tests if feature is available, otherwise fall back to null\n    #[cfg(feature = \"cache_inmem\")]\n    let cache = cache::drivers::inmem::new(&crate::config::InMemCacheConfig {\n        max_capacity: 32 * 1024 * 1024, // Use explicit value instead of default\n    });\n\n    // If cache_inmem is not enabled, use null cache regardless of other features\n    #[cfg(not(feature = \"cache_inmem\"))]\n    let cache = cache::Cache::new(cache::drivers::null::new());\n\n    AppContext {\n        environment: Environment::Test,\n        #[cfg(feature = \"with-db\")]\n        db: super::db::dummy_connection().await,\n        queue_provider: None,\n        config: test_config(),\n        mailer: None,\n        storage: Storage::single(storage::drivers::mem::new()).into(),\n        cache: cache.into(),\n        shared_store: std::sync::Arc::new(SharedStore::default()),\n    }\n}\n"
  },
  {
    "path": "src/tests_cfg/config.rs",
    "content": "use std::collections::HashMap;\n\nuse crate::{\n    config::{self, Config},\n    controller::middleware,\n    logger, scheduler,\n};\n\nuse tree_fs::{Tree, TreeBuilder};\n\n#[must_use]\npub fn test_config() -> Config {\n    Config {\n        logger: config::Logger {\n            enable: false,\n            pretty_backtrace: true,\n            level: logger::LogLevel::Off,\n            format: logger::Format::Json,\n            override_filter: None,\n            file_appender: None,\n        },\n        server: config::Server {\n            binding: \"localhost\".to_string(),\n            port: 5555,\n            host: \"localhost\".to_string(),\n            ident: None,\n            middlewares: middleware::Config::default(),\n        },\n        #[cfg(feature = \"with-db\")]\n        database: get_database_config(),\n        queue: None,\n        auth: None,\n        workers: config::Workers {\n            mode: config::WorkerMode::ForegroundBlocking,\n        },\n        mailer: None,\n        initializers: None,\n        settings: None,\n        scheduler: Some(scheduler::Config {\n            jobs: HashMap::from([(\n                \"job 1\".to_string(),\n                scheduler::Job {\n                    run: \"echo loco\".to_string(),\n                    shell: true,\n                    run_on_start: false,\n                    cron: \"*/5 * * * * *\".to_string(),\n                    tags: Some(vec![\"base\".to_string()]),\n                    output: None,\n                },\n            )]),\n\n            output: scheduler::Output::STDOUT,\n        }),\n        // Always use in-memory cache for tests if available\n        #[cfg(feature = \"cache_inmem\")]\n        cache: config::CacheConfig::InMem(config::InMemCacheConfig {\n            max_capacity: 32 * 1024 * 1024, // Use explicit value instead of default\n        }),\n        // If cache_inmem is not enabled, use null cache\n        #[cfg(not(feature = \"cache_inmem\"))]\n        cache: config::CacheConfig::Null,\n    }\n}\n\n#[must_use]\npub fn get_database_config() -> config::Database {\n    config::Database {\n        uri: \"sqlite::memory:\".to_string(),\n        enable_logging: false,\n        min_connections: 1,\n        max_connections: 1,\n        connect_timeout: 500,\n        idle_timeout: 500,\n        acquire_timeout: None,\n        auto_migrate: false,\n        dangerously_truncate: false,\n        dangerously_recreate: false,\n        run_on_start: None,\n    }\n}\n\n/// Creates a `SQLite` test database configuration with a temporary file\n///\n/// Returns both the database configuration and the [`tree_fs`] temporary folder\n///\n/// # Panics\n///\n/// Panics if the temporary folder cannot be created.\n#[must_use]\npub fn get_sqlite_test_config(db_filename: &str) -> (config::Database, Tree) {\n    let tree_fs = TreeBuilder::default()\n        .drop(true)\n        .create()\n        .expect(\"create temp folder\");\n\n    let mut config = get_database_config();\n    config.uri = format!(\n        \"sqlite://{}\",\n        tree_fs\n            .root\n            .join(format!(\"{db_filename}.db?mode=rwc\"))\n            .to_str()\n            .unwrap()\n    );\n\n    (config, tree_fs)\n}\n"
  },
  {
    "path": "src/tests_cfg/controllers/auth.rs",
    "content": "use crate::controller::Routes;\n\n#[must_use]\npub fn routes() -> Routes {\n    Routes::new()\n}\n"
  },
  {
    "path": "src/tests_cfg/controllers/home.rs",
    "content": "use crate::controller::Routes;\n\n#[must_use]\npub fn routes() -> Routes {\n    Routes::new()\n}\n"
  },
  {
    "path": "src/tests_cfg/controllers/mod.rs",
    "content": "pub mod auth;\npub mod home;\n"
  },
  {
    "path": "src/tests_cfg/db.rs",
    "content": "use std::path::Path;\n\nuse async_trait::async_trait;\nuse sea_orm::Statement;\npub use sea_orm_migration::prelude::*;\n\nuse crate::{\n    app::{AppContext, Hooks, Initializer},\n    bgworker::Queue,\n    boot::{create_app, BootResult, StartMode},\n    config::Config,\n    controller::AppRoutes,\n    environment::Environment,\n    task::Tasks,\n    Result,\n};\n\n/// Get query result as string\n///\n/// Executes the SQL query and returns the first column value as a string.\n///\n/// # Panics\n///\n/// - If the database query fails.\n/// - If the query returns no result row.\n/// - If the value cannot be extracted from the first column as a String or i64.\npub async fn get_value(conn: &sea_orm::DatabaseConnection, query: &str) -> String {\n    // Execute query and get the result row\n    let row = conn\n        .query_one(Statement::from_string(\n            conn.get_database_backend(),\n            query.to_owned(),\n        ))\n        .await\n        .unwrap_or_else(|e| panic!(\"Query failed: {query}, error: {e}\"))\n        .expect(\"No result returned\");\n\n    // Get column names\n    let columns = row.column_names();\n\n    // Get first column name or empty string\n    let col_name = columns.first().map_or(\"\", |c| c.as_str());\n\n    // Try as string or number, convert to lowercase for consistency\n    row.try_get::<String>(\"\", col_name)\n        .or_else(|_| row.try_get::<i64>(\"\", col_name).map(|v| v.to_string()))\n        .unwrap_or_else(|_| panic!(\"Could not extract value for column: {col_name}\"))\n        .to_lowercase()\n}\n\n/// Creating a dummy db connection for docs\n///\n/// # Panics\n/// Disabled the connection validation, should pass always\npub async fn dummy_connection() -> sea_orm::DatabaseConnection {\n    let mut opt = sea_orm::ConnectOptions::new(\"sqlite::memory:\");\n    opt.test_before_acquire(false);\n\n    sea_orm::Database::connect(opt).await.unwrap()\n}\n\n/// Creating a failing db connection for tests\n///\n/// # Panics\n/// Set a non-existing database, disabled the connection pool creation and connection validation,\n/// it should fail immediately when it's used.\npub async fn fail_connection() -> sea_orm::DatabaseConnection {\n    let mut opt =\n        sea_orm::ConnectOptions::new(\"postgres://loco:loco@127.0.0.1:9999/non_existent_db\");\n    opt.test_before_acquire(false)\n        .connect_lazy(true)\n        .connect_timeout(std::time::Duration::from_micros(1));\n\n    sea_orm::Database::connect(opt).await.unwrap()\n}\n\npub mod test_db {\n    use std::fmt;\n\n    use sea_orm::entity::prelude::*;\n\n    #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]\n    #[sea_orm(table_name = \"loco\")]\n    pub struct Model {\n        #[sea_orm(primary_key)]\n        pub id: i32,\n        pub name: String,\n        pub created_at: DateTime,\n        pub updated_at: DateTime,\n    }\n\n    #[derive(Debug)]\n    pub enum Loco {\n        Table,\n        Id,\n        Name,\n    }\n\n    impl Iden for Loco {\n        fn unquoted(&self, s: &mut dyn fmt::Write) {\n            write!(\n                s,\n                \"{}\",\n                match self {\n                    Self::Table => \"loco\",\n                    Self::Id => \"id\",\n                    Self::Name => \"name\",\n                }\n            )\n            .unwrap();\n        }\n    }\n\n    #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]\n    pub enum Relation {}\n\n    impl ActiveModelBehavior for ActiveModel {}\n}\n\n#[derive(Debug)]\npub struct Migrator;\n\n#[async_trait::async_trait]\nimpl MigratorTrait for Migrator {\n    fn migrations() -> Vec<Box<dyn MigrationTrait>> {\n        vec![]\n    }\n}\n\n#[derive(Debug)]\npub struct AppHook;\n#[async_trait]\nimpl Hooks for AppHook {\n    fn app_version() -> String {\n        \"test\".to_string()\n    }\n\n    fn app_name() -> &'static str {\n        \"TEST\"\n    }\n\n    async fn initializers(_ctx: &AppContext) -> Result<Vec<Box<dyn Initializer>>> {\n        Ok(vec![])\n    }\n\n    fn routes(_ctx: &AppContext) -> AppRoutes {\n        AppRoutes::with_default_routes()\n    }\n\n    async fn boot(\n        mode: StartMode,\n        environment: &Environment,\n        config: Config,\n    ) -> Result<BootResult> {\n        create_app::<Self, Migrator>(mode, environment, config).await\n    }\n\n    async fn connect_workers(_ctx: &AppContext, _q: &Queue) -> Result<()> {\n        Ok(())\n    }\n\n    fn register_tasks(tasks: &mut Tasks) {\n        tasks.register(super::task::Foo);\n        tasks.register(super::task::ParseArgs);\n    }\n\n    async fn truncate(_ctx: &AppContext) -> Result<()> {\n        Ok(())\n    }\n\n    async fn seed(_ctx: &AppContext, _base: &Path) -> Result<()> {\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "src/tests_cfg/mod.rs",
    "content": "pub mod app;\npub mod config;\npub mod controllers;\n#[cfg(feature = \"with-db\")]\npub mod db;\n#[cfg(test)]\npub mod postgres;\n#[cfg(any(feature = \"bg_pg\", feature = \"bg_sqlt\"))]\npub mod queue;\n#[cfg(test)]\npub mod redis;\npub mod task;\n"
  },
  {
    "path": "src/tests_cfg/postgres.rs",
    "content": "use sqlx::PgPool;\nuse std::time::Duration;\nuse testcontainers::{\n    core::{ContainerPort, WaitFor},\n    runners::AsyncRunner,\n    ContainerAsync, GenericImage, ImageExt,\n};\n\n/// Sets up a `PostgreSQL` test container.\n///\n/// # Returns\n///\n/// A tuple containing the `PostgreSQL` connection URL and the container instance.\n///\n/// # Panics\n///\n/// This function will panic if it fails to set up, start, or connect to the PostgreSQL container.\npub async fn setup_postgres_container() -> (String, ContainerAsync<GenericImage>) {\n    let pg_image = GenericImage::new(\"postgres\", \"15\")\n        .with_wait_for(WaitFor::message_on_stdout(\n            \"database system is ready to accept connections\",\n        ))\n        .with_exposed_port(ContainerPort::Tcp(5432))\n        .with_env_var(\"POSTGRES_USER\", \"postgres\")\n        .with_env_var(\"POSTGRES_PASSWORD\", \"postgres\")\n        .with_env_var(\"POSTGRES_DB\", \"postgres\");\n\n    let container = pg_image\n        .start()\n        .await\n        .expect(\"Failed to start PostgreSQL container\");\n\n    let host_port = container\n        .get_host_port_ipv4(5432)\n        .await\n        .expect(\"Failed to get host port\");\n\n    let pg_url = format!(\"postgres://postgres:postgres@127.0.0.1:{host_port}/postgres\");\n\n    // Try to connect to PostgreSQL up to 10 times with 1 second interval\n    let mut connected = false;\n\n    for attempt in 0..10 {\n        match PgPool::connect(&pg_url).await {\n            Ok(pool) => {\n                // Try to ping with a simple query\n                match sqlx::query(\"SELECT 1\").execute(&pool).await {\n                    Ok(_) => {\n                        connected = true;\n                        break;\n                    }\n                    Err(_) => {\n                        if attempt < 9 {\n                            tokio::time::sleep(Duration::from_secs(1)).await;\n                        }\n                    }\n                }\n            }\n            Err(_) => {\n                if attempt < 9 {\n                    tokio::time::sleep(Duration::from_secs(1)).await;\n                }\n            }\n        }\n    }\n\n    assert!(\n        connected,\n        \"Failed to connect to PostgreSQL after 10 attempts\"\n    );\n\n    (pg_url, container)\n}\n"
  },
  {
    "path": "src/tests_cfg/queue.rs",
    "content": "#[cfg(any(feature = \"bg_pg\", feature = \"bg_sqlt\"))]\nuse crate::bgworker;\nuse std::path::PathBuf;\n\n#[cfg(any(feature = \"bg_pg\", feature = \"bg_sqlt\"))]\nfn queue_jobs_fixture_path() -> PathBuf {\n    PathBuf::from(env!(\"CARGO_MANIFEST_DIR\"))\n        .join(\"tests\")\n        .join(\"fixtures\")\n        .join(\"queue\")\n        .join(\"jobs.yaml\")\n}\n\n#[cfg(feature = \"bg_pg\")]\n/// # Panics\n///\n/// This function will panic if it fails to prepare or insert the seed data, causing the tests to fail quickly\n/// and preventing further test execution with incomplete setup.\npub async fn postgres_seed_data(pool: &sqlx::PgPool) {\n    let yaml_tasks =\n        std::fs::read_to_string(queue_jobs_fixture_path()).expect(\"Failed to read YAML file\");\n\n    let tasks: Vec<bgworker::pg::Job> =\n        serde_yaml::from_str(&yaml_tasks).expect(\"Failed to parse YAML\");\n    for task in tasks {\n        sqlx::query(\n            r\"\n            INSERT INTO pg_loco_queue (id, name, task_data, status, run_at, interval, created_at, updated_at)\n            VALUES ($1, $2, $3, $4, $5, NULL, $6, $7)\n            \",\n        )\n        .bind(task.id)\n        .bind(task.name)\n        .bind(task.data)\n        .bind(task.status.to_string())\n        .bind(task.run_at)\n        .bind(task.created_at)\n        .bind(task.updated_at)\n        .execute(pool)\n        .await.expect(\"execute insert query\");\n    }\n}\n\n#[cfg(feature = \"bg_sqlt\")]\n/// # Panics\n///\n/// This function will panic if it fails to prepare or insert the seed data, causing the tests to fail quickly\n/// and preventing further test execution with incomplete setup.\npub async fn sqlite_seed_data(pool: &sqlx::Pool<sqlx::Sqlite>) {\n    let yaml_tasks =\n        std::fs::read_to_string(queue_jobs_fixture_path()).expect(\"Failed to read YAML file\");\n\n    let tasks: Vec<bgworker::sqlt::Job> =\n        serde_yaml::from_str(&yaml_tasks).expect(\"Failed to parse YAML\");\n    for task in tasks {\n        sqlx::query(\n            r\"\n            INSERT INTO sqlt_loco_queue (id, name, task_data, status, run_at, interval, created_at, updated_at)\n            VALUES (?, ?, ?, ?, ?, NULL, ?, ?)\n            \"\n        )\n        .bind(task.id)\n        .bind(task.name)\n        .bind(task.data.to_string())\n        .bind(task.status.to_string())\n        .bind(task.run_at)\n        .bind(task.created_at)\n        .bind(task.updated_at)\n        .execute(pool)\n        .await.expect(\"create row\");\n    }\n\n    sqlx::query(\n        r\"\n                INSERT INTO sqlt_loco_queue_lock (id, is_locked, locked_at)\n    VALUES (1, FALSE, NULL)\n    ON CONFLICT (id) DO NOTHING;\n\n            \",\n    )\n    .execute(pool)\n    .await\n    .expect(\"execute insert query\");\n}\n"
  },
  {
    "path": "src/tests_cfg/redis.rs",
    "content": "use redis::Client;\nuse std::time::Duration;\nuse testcontainers::{\n    core::{ContainerPort, WaitFor},\n    runners::AsyncRunner,\n    ContainerAsync, GenericImage,\n};\n\n/// Sets up a Redis test container.\n///\n/// # Returns\n///\n/// A tuple containing the Redis URL and the container instance.\n///\n/// # Panics\n///\n/// This function will panic if it fails to set up, start, or connect to the Redis container.\npub async fn setup_redis_container() -> (String, ContainerAsync<GenericImage>) {\n    let redis_image = GenericImage::new(\"redis\", \"7-alpine\")\n        .with_exposed_port(ContainerPort::Tcp(6379))\n        .with_wait_for(WaitFor::message_on_stdout(\"Ready to accept connections\"));\n\n    let container = redis_image\n        .start()\n        .await\n        .expect(\"Failed to start Redis container\");\n\n    let host_port = container\n        .get_host_port_ipv4(6379)\n        .await\n        .expect(\"Failed to get host port\");\n\n    let redis_url = format!(\"redis://127.0.0.1:{host_port}\");\n\n    // Try to ping Redis up to 10 times with 1 second interval\n    let client = Client::open(redis_url.clone()).expect(\"Failed to create Redis client\");\n\n    let mut connected = false;\n    for attempt in 0..10 {\n        match client.get_multiplexed_async_connection().await {\n            Ok(mut conn) => {\n                // Try to ping\n                if redis::cmd(\"PING\")\n                    .query_async::<()>(&mut conn)\n                    .await\n                    .is_ok()\n                {\n                    // Successfully pinged Redis\n                    connected = true;\n                    break;\n                } else if attempt < 9 {\n                    tokio::time::sleep(Duration::from_secs(1)).await;\n                }\n            }\n            Err(_) => {\n                if attempt < 9 {\n                    tokio::time::sleep(Duration::from_secs(1)).await;\n                }\n            }\n        }\n    }\n\n    // Panic if we couldn't connect after all attempts\n    assert!(connected, \"Failed to connect to Redis after 10 attempts\");\n\n    (redis_url, container)\n}\n"
  },
  {
    "path": "src/tests_cfg/task.rs",
    "content": "use crate::prelude::*;\n\n#[derive(Debug)]\npub struct Foo;\n\n#[async_trait]\nimpl Task for Foo {\n    fn task(&self) -> TaskInfo {\n        TaskInfo {\n            name: \"foo\".to_string(),\n            detail: \"run foo task\".to_string(),\n        }\n    }\n    async fn run(&self, _app_context: &AppContext, _vars: &task::Vars) -> Result<()> {\n        println!(\"Foo task executed!!!\");\n        Ok(())\n    }\n}\n\n#[derive(Debug)]\npub struct ParseArgs;\n\n#[async_trait]\nimpl Task for ParseArgs {\n    fn task(&self) -> TaskInfo {\n        TaskInfo {\n            name: \"parse_args\".to_string(),\n            detail: \"Validate the paring args\".to_string(),\n        }\n    }\n    async fn run(&self, _app_context: &AppContext, vars: &task::Vars) -> Result<()> {\n        let refresh = vars.cli_arg(\"test\").is_ok_and(|test| test == \"true\");\n\n        let app = vars\n            .cli_arg(\"app\")\n            .map(std::string::ToString::to_string)\n            .unwrap_or_default();\n\n        if refresh && app == \"loco\" {\n            Ok(())\n        } else {\n            Err(Error::string(\"invalid args\"))\n        }\n    }\n}\n"
  },
  {
    "path": "src/validation.rs",
    "content": "//! This module provides utility functions for handling validation errors for\n//! structs. It useful if you want to validate model before insert to Database.\n//!\n//! # Example:\n//!\n//! In the following example you can see how you can validate a user model\n//! ```rust,ignore\n//! use loco_rs::prelude::*;\n//! pub use myapp::_entities::users::ActiveModel;\n//!\n//! // Validation structure\n//! #[derive(Debug, Validate, Deserialize)]\n//! pub struct Validator {\n//!     #[validate(length(min = 2, message = \"Name must be at least 2 characters long.\"))]\n//!     pub name: String,\n//! }\n//!\n//! impl Validatable for ActiveModel {\n//!   fn validator(&self) -> Box<dyn Validate> {\n//!     Box::new(Validator {\n//!         name: self.name.as_ref().to_owned(),\n//!     })\n//!   }\n//! }\n//!\n//! /// Override `before_save` function and run validation to make sure that we insert valid data.\n//! #[async_trait::async_trait]\n//! impl ActiveModelBehavior for ActiveModel {\n//!     async fn before_save<C>(self, _db: &C, insert: bool) -> Result<Self, DbErr>\n//!     where\n//!         C: ConnectionTrait,\n//!     {\n//!         {\n//!             self.validate()?;\n//!             Ok(self)\n//!         }\n//!     }\n//! }\n//! ```\n\n#[cfg(feature = \"with-db\")]\nuse sea_orm::DbErr;\nuse serde::{Deserialize, Serialize};\nuse std::collections::{BTreeMap, HashMap};\nuse validator::ValidationErrors;\n\n// this is a line-serialization type. it is used as an intermediate format\n// to hold validation error data when we transform from\n// validation::ValidationErrors to DbErr and encode all information in json.\n#[derive(Debug, Deserialize, Serialize)]\n#[allow(clippy::module_name_repetitions)]\npub struct ModelValidationMessage {\n    pub code: String,\n    pub message: Option<String>,\n}\n\n/// <DbErr conversion hack>\n///\n/// Convert `ModelValidationErrors` (pretty) into a `DbErr` (ugly) for database\n/// handling.\n///\n/// Because `DbErr` is used in model hooks and we implement the hooks\n/// in the trait, we MUST use `DbErr`, so we need to \"hide\" a _representation_\n/// of the error in `DbErr::Custom`, so that it can be unpacked later down the\n/// stream, in the central error response handler.\n#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]\npub struct ValidationError {\n    pub code: String,\n    pub message: Option<String>,\n    #[serde(skip_serializing_if = \"HashMap::is_empty\")]\n    pub params: HashMap<String, serde_json::Value>,\n}\n\n#[derive(Debug, thiserror::Error, Serialize, Deserialize, Clone, PartialEq, Eq)]\n#[error(\"Model validation failed\")]\npub struct ModelValidationErrors {\n    pub errors: BTreeMap<String, Vec<ValidationError>>,\n}\n\nimpl From<ValidationErrors> for ModelValidationErrors {\n    fn from(value: ValidationErrors) -> Self {\n        let mut map: BTreeMap<String, Vec<ValidationError>> = BTreeMap::new();\n        for (field, errs) in &value.field_errors() {\n            let mut list: Vec<ValidationError> = Vec::with_capacity(errs.len());\n            for err in *errs {\n                let mut params: HashMap<String, serde_json::Value> = HashMap::new();\n                for (k, v) in &err.params {\n                    params.insert(k.to_string(), v.clone());\n                }\n                list.push(ValidationError {\n                    code: err.code.to_string(),\n                    message: err.message.as_ref().map(std::string::ToString::to_string),\n                    params,\n                });\n            }\n            map.insert((*field).to_string(), list);\n        }\n        Self { errors: map }\n    }\n}\n\n#[cfg(feature = \"with-db\")]\nimpl From<ModelValidationErrors> for DbErr {\n    fn from(errors: ModelValidationErrors) -> Self {\n        into_db_error(&errors)\n    }\n}\n\n#[cfg(feature = \"with-db\")]\n#[must_use]\npub fn into_db_error(errors: &ModelValidationErrors) -> sea_orm::DbErr {\n    let compact: BTreeMap<String, Vec<ModelValidationMessage>> = errors\n        .errors\n        .iter()\n        .map(|(field, list)| {\n            let flat: Vec<ModelValidationMessage> = list\n                .iter()\n                .map(|e| ModelValidationMessage {\n                    code: e.code.clone(),\n                    message: e.message.clone(),\n                })\n                .collect();\n            (field.clone(), flat)\n        })\n        .collect();\n\n    match serde_json::to_string(&compact) {\n        Ok(s) => sea_orm::DbErr::Custom(s),\n        Err(err) => sea_orm::DbErr::Custom(format!(\n            \"[before_save] could not parse validation errors. err: {err}\"\n        )),\n    }\n}\n\n/// Implement `Validatable` for `ActiveModel` when you want it to have a\n/// `validate()` function.\npub trait ValidatorTrait {\n    /// Perform validation and return a normalized error type\n    ///\n    /// # Errors\n    ///\n    /// Returns `ModelValidationErrors` when validation fails.\n    fn validate(&self) -> Result<(), ModelValidationErrors>;\n}\n\n/// Adapter: allow using the `validator` crate seamlessly\nimpl<T: validator::Validate> ValidatorTrait for T {\n    fn validate(&self) -> Result<(), ModelValidationErrors> {\n        validator::Validate::validate(self).map_err(ModelValidationErrors::from)\n    }\n}\n\n/// Implement `Validatable` for `ActiveModel` when you want it to have a\n/// `validate()` function.\npub trait Validatable {\n    /// Perform validation\n    ///\n    /// # Errors\n    ///\n    /// This function will return an error if there are validation errors\n    fn validate(&self) -> Result<(), ModelValidationErrors> {\n        let v = self.validator();\n        validator::Validate::validate(&*v).map_err(ModelValidationErrors::from)\n    }\n    fn validator(&self) -> Box<dyn validator::Validate>;\n}\n\n#[cfg(test)]\nmod tests {\n\n    use insta::assert_debug_snapshot;\n    use rstest::rstest;\n    use serde::Deserialize;\n    use validator::Validate;\n\n    use super::*;\n\n    #[derive(Debug, Deserialize, Validate)]\n    pub struct TestValidator {\n        #[validate(length(min = 4, message = \"Invalid min characters long.\"))]\n        pub name: String,\n    }\n\n    #[cfg(feature = \"with-db\")]\n    #[rstest]\n    #[case(\"foo\")]\n    #[case(\"foo-bar\")]\n    fn can_validate_into_db_error(#[case] name: &str) {\n        let data = TestValidator {\n            name: name.to_string(),\n        };\n\n        assert_debug_snapshot!(\n            format!(\"struct-[{name}]\"),\n            validator::Validate::validate(&data)\n                .map_err(|e| into_db_error(&ModelValidationErrors::from(e)))\n        );\n    }\n\n    // Custom validator example without the `validator` crate\n    #[derive(Debug, Deserialize)]\n    pub struct CustomValidator {\n        pub name: String,\n    }\n\n    impl ValidatorTrait for CustomValidator {\n        fn validate(&self) -> Result<(), ModelValidationErrors> {\n            if self.name.len() < 4 {\n                let mut errors: BTreeMap<String, Vec<ValidationError>> = BTreeMap::new();\n                errors.insert(\n                    \"name\".to_string(),\n                    vec![ValidationError {\n                        code: \"length\".to_string(),\n                        message: Some(\"Invalid min characters long.\".to_string()),\n                        params: HashMap::new(),\n                    }],\n                );\n                return Err(ModelValidationErrors { errors });\n            }\n            Ok(())\n        }\n    }\n\n    #[rstest]\n    #[case(\"ab\")]\n    #[case(\"abcd\")]\n    fn custom_validator_works(#[case] name: &str) {\n        let v = CustomValidator {\n            name: name.to_string(),\n        };\n        let res = v.validate();\n        if name.len() < 4 {\n            assert!(res.is_err());\n        } else {\n            assert!(res.is_ok());\n        }\n    }\n}\n"
  },
  {
    "path": "tests/build_scripts/embedded_assets.rs",
    "content": "use insta::{assert_debug_snapshot, assert_snapshot};\nuse std::collections::HashMap;\nuse std::path::Path; // For creating regex filters\n\n// Import only the essential functions from build/embedded_assets.rs\n// Use a module declaration with the `#[path]` attribute to specify the file path\n#[path = \"../../build/embedded_assets.rs\"]\nmod embedded_assets;\n\n// Export only the functions we're actually testing\npub use embedded_assets::{\n    build_static_assets, collect_all_files, discover_all_directories, find_app_directory,\n    generate_asset_code, generate_empty_asset_files,\n};\n\n/// Creates a test file structure with common assets for testing.\nfn create_test_assets() -> tree_fs::Tree {\n    tree_fs::TreeBuilder::default()\n        .drop(true)\n        .add_file(\"assets/css/style.css\", \"body { color: blue; }\")\n        .add_file(\"assets/js/app.js\", \"console.log('Hello Loco');\")\n        .add_file(\"assets/views/index.html\", \"<h1>Hello World</h1>\")\n        .add_directory(\"generated\")\n        .create()\n        .unwrap()\n}\n\n/// Creates insta settings with common filters.\nfn create_insta_settings(root_path: &Path) -> insta::Settings {\n    let mut settings = insta::Settings::clone_current();\n    settings.add_filter(root_path.to_str().unwrap(), \"[TEST_ROOT]\");\n    settings.add_filter(\"\\\\\\\\\\\\\\\\\", \"/\");\n    settings\n}\n\n#[test]\nfn test_generate_empty_asset_files() {\n    // Create a temporary test environment\n    let tree_fs = tree_fs::TreeBuilder::default()\n        .drop(true)\n        .add_directory(\"generated\")\n        .create()\n        .unwrap();\n\n    let output_path = tree_fs.root.join(\"generated\");\n    generate_empty_asset_files(&output_path).unwrap();\n\n    // Verify files exist\n    let static_file_path = output_path.join(\"static_assets.rs\");\n    let templates_file_path = output_path.join(\"view_templates.rs\");\n    assert!(\n        static_file_path.exists(),\n        \"Static assets file should be created\"\n    );\n    assert!(\n        templates_file_path.exists(),\n        \"Templates file should be created\"\n    );\n\n    // Use snapshots to verify file contents\n    let static_file_content = std::fs::read_to_string(static_file_path).unwrap();\n    let templates_file_content = std::fs::read_to_string(templates_file_path).unwrap();\n\n    assert_snapshot!(\"empty_static_assets_rs\", static_file_content);\n    assert_snapshot!(\"empty_templates_rs\", templates_file_content);\n}\n\n#[test]\nfn test_generate_asset_code() {\n    let tree_fs = create_test_assets();\n    let root_path = &tree_fs.root;\n    let output_path = root_path.join(\"generated\");\n\n    // Create file mapping\n    let mut all_files = HashMap::new();\n    all_files.insert(\n        root_path\n            .join(\"assets/css/style.css\")\n            .to_str()\n            .unwrap()\n            .to_string(),\n        \"/css/style.css\".to_string(),\n    );\n    all_files.insert(\n        root_path\n            .join(\"assets/js/app.js\")\n            .to_str()\n            .unwrap()\n            .to_string(),\n        \"/js/app.js\".to_string(),\n    );\n    all_files.insert(\n        root_path\n            .join(\"assets/views/index.html\")\n            .to_str()\n            .unwrap()\n            .to_string(),\n        \"index.html\".to_string(),\n    );\n\n    generate_asset_code(&all_files, &output_path).unwrap();\n\n    // Verify files exist\n    let static_assets_path = output_path.join(\"static_assets.rs\");\n    let view_templates_path = output_path.join(\"view_templates.rs\");\n    assert!(static_assets_path.exists());\n    assert!(view_templates_path.exists());\n\n    // Snapshot file contents\n    let static_content = std::fs::read_to_string(static_assets_path).unwrap();\n    let template_content = std::fs::read_to_string(view_templates_path).unwrap();\n\n    let settings = create_insta_settings(root_path);\n    settings.bind(|| {\n        assert_snapshot!(\"static_assets_rs\", static_content);\n        assert_snapshot!(\"view_templates_rs\", template_content);\n    });\n}\n\n#[test]\nfn test_discover_all_directories() {\n    let tree = tree_fs::TreeBuilder::default()\n        .drop(true)\n        .add_directory(\"my_assets/css\")\n        .add_file(\"my_assets/css/style.css\", \"/* some css */\")\n        .add_directory(\"my_assets/js/vendor\")\n        .add_directory(\"my_assets/images\")\n        .add_directory(\"my_assets/views/user\")\n        .add_file(\"my_assets/root_file.txt\", \"I am a file\")\n        .add_directory(\"my_assets/empty_dir\")\n        .create()\n        .unwrap();\n\n    let assets_root_path = tree.root.join(\"my_assets\");\n    let discovered_dirs = discover_all_directories(&assets_root_path);\n\n    // Use insta settings for path normalization\n    let settings = create_insta_settings(&tree.root);\n    settings.bind(|| {\n        assert_debug_snapshot!(\"discovered_directories\", discovered_dirs);\n    });\n\n    // Test edge cases\n    let non_existent_path = tree.root.join(\"non_existent_assets\");\n    let discovered_for_non_existent = discover_all_directories(&non_existent_path);\n    assert!(\n        discovered_for_non_existent.is_empty(),\n        \"Should return empty for a non-existent path\"\n    );\n\n    // Test with an empty root directory\n    let empty_root_path = tree.root.join(\"actually_empty_assets\");\n    std::fs::create_dir(&empty_root_path).unwrap();\n    let discovered_for_empty = discover_all_directories(&empty_root_path);\n    assert_eq!(\n        discovered_for_empty.len(),\n        1,\n        \"Should find only the root for an empty existing directory\"\n    );\n}\n\n#[test]\nfn test_find_app_directory() {\n    // Case 1: Standard project structure\n    let tree_target = tree_fs::TreeBuilder::default()\n        .drop(true)\n        .add_file(\"my_project/Cargo.toml\", \"[package]\\nname = \\\"my_project\\\"\")\n        .add_directory(\"my_project/src\")\n        .add_directory(\"my_project/target/debug/deps\")\n        .create()\n        .unwrap();\n\n    let expected_project_root = tree_target.root.join(\"my_project\");\n    let out_dir_in_target = expected_project_root.join(\"target/debug/deps\");\n\n    let app_dir = find_app_directory(&out_dir_in_target);\n    assert!(\n        app_dir.is_some(),\n        \"Should find app directory in standard structure\"\n    );\n    if let Some(found_dir) = app_dir {\n        assert_eq!(\n            found_dir, expected_project_root,\n            \"Should find correct project root\"\n        );\n        assert!(\n            found_dir.join(\"Cargo.toml\").exists(),\n            \"Located app_dir should contain Cargo.toml\"\n        );\n    }\n\n    // Case 2: Path not within a 'target' directory\n    let tree_no_target = tree_fs::TreeBuilder::default()\n        .drop(true)\n        .add_directory(\"some_other_place/src\")\n        .create()\n        .unwrap();\n\n    let path_not_in_target = tree_no_target.root.join(\"some_other_place/src\");\n    let app_dir = find_app_directory(&path_not_in_target);\n\n    assert!(app_dir.is_some(), \"Should return fallback directory\");\n    if let Some(found_dir) = app_dir {\n        assert!(found_dir.exists(), \"Fallback directory should exist\");\n    }\n}\n\n#[test]\nfn test_build_static_assets() {\n    // Create test environment\n    let tree_fs = tree_fs::TreeBuilder::default()\n        .drop(true)\n        .add_file(\"assets/css/style.css\", \"body { color: blue; }\")\n        .add_file(\"assets/js/app.js\", \"console.log('Hello Loco');\")\n        .add_file(\"assets/views/index.html\", \"<h1>Hello World</h1>\")\n        .add_directory(\"target/debug/build/embedded_code\")\n        .create()\n        .unwrap();\n\n    let root_path = &tree_fs.root;\n    let out_dir = root_path.join(\"target/debug/build/embedded_code\");\n\n    // Call function being tested\n    build_static_assets(&out_dir);\n\n    // Verify files were generated\n    let generated_path = out_dir.join(\"generated_code\");\n    let static_assets_path = generated_path.join(\"static_assets.rs\");\n    let view_templates_path = generated_path.join(\"view_templates.rs\");\n\n    assert!(generated_path.exists(), \"Generated directory should exist\");\n    assert!(\n        static_assets_path.exists(),\n        \"Static assets file should exist\"\n    );\n    assert!(\n        view_templates_path.exists(),\n        \"View templates file should exist\"\n    );\n\n    // Snapshot generated files\n    let static_content = std::fs::read_to_string(static_assets_path).unwrap();\n    let template_content = std::fs::read_to_string(view_templates_path).unwrap();\n\n    let settings = create_insta_settings(root_path);\n    settings.bind(|| {\n        assert_snapshot!(\"build_static_assets_static\", static_content);\n        assert_snapshot!(\"build_static_assets_templates\", template_content);\n    });\n}\n\n#[test]\nfn test_collect_all_files() {\n    let tree_fs = create_test_assets();\n    let root_path = &tree_fs.root;\n    let assets_dir = root_path.join(\"assets\");\n\n    // Test collection from css directory\n    let mut all_files = HashMap::new();\n    collect_all_files(&assets_dir.join(\"css\"), &assets_dir, &mut all_files);\n\n    // Convert to sorted vector for consistent order\n    let mut file_mappings: Vec<(String, String)> = all_files\n        .iter()\n        .map(|(path, key)| (path.clone(), key.clone()))\n        .collect();\n    file_mappings.sort();\n\n    // Use insta settings for path normalization\n    let settings = create_insta_settings(root_path);\n    settings.bind(|| {\n        assert_debug_snapshot!(\"collected_css_files\", file_mappings);\n    });\n\n    // Test collection from all directories\n    let mut all_files = HashMap::new();\n    for dir in discover_all_directories(&assets_dir) {\n        collect_all_files(&dir, &assets_dir, &mut all_files);\n    }\n\n    let mut file_mappings: Vec<(String, String)> = all_files\n        .iter()\n        .map(|(path, key)| (path.clone(), key.clone()))\n        .collect();\n    file_mappings.sort();\n\n    settings.bind(|| {\n        assert_debug_snapshot!(\"collected_all_files\", file_mappings);\n    });\n}\n\n#[test]\nfn test_template_inheritance() {\n    // Create test environment with complex template inheritance (4 levels)\n    let tree_fs = tree_fs::TreeBuilder::default()\n        .drop(true)\n        // Level 1 (base)\n        .add_file(\n            \"assets/views/base.html\", \n            \"<!DOCTYPE html><html><head><title>{% block meta_title %}Base{% endblock %}</title>{% block head %}{% endblock %}</head><body>{% block body %}{% endblock %}</body></html>\"\n        )\n        // Level 2 (extends base)\n        .add_file(\n            \"assets/views/layouts/app.html\", \n            \"{% extends \\\"base.html\\\" %}{% block head %}<link rel=\\\"stylesheet\\\" href=\\\"/app.css\\\">{% endblock %}{% block body %}<nav>{% block nav %}{% endblock %}</nav><main>{% block content %}{% endblock %}</main>{% endblock %}\"\n        )\n        // Level 3 (extends app)\n        .add_file(\n            \"assets/views/layouts/authenticated.html\", \n            \"{% extends \\\"layouts/app.html\\\" %}{% block nav %}<div class=\\\"user-nav\\\">{% block user_nav %}{% endblock %}</div>{% endblock %}\"\n        )\n        // Level 4 (extends authenticated)\n        .add_file(\n            \"assets/views/dashboard/index.html\", \n            \"{% extends \\\"layouts/authenticated.html\\\" %}{% block meta_title %}Dashboard{% endblock %}{% block user_nav %}<a href=\\\"/profile\\\">Profile</a>{% endblock %}{% block content %}<h1>Dashboard</h1>{% endblock %}\"\n        )\n        // Another Level 4 template to test multiple children\n        .add_file(\n            \"assets/views/dashboard/settings.html\", \n            \"{% extends \\\"layouts/authenticated.html\\\" %}{% block meta_title %}Settings{% endblock %}{% block user_nav %}<a href=\\\"/profile\\\">Profile</a>{% endblock %}{% block content %}<h1>Settings</h1>{% endblock %}\"\n        )\n        // Independent template with no inheritance\n        .add_file(\n            \"assets/views/error.html\",\n            \"<h1>Error</h1>\"\n        )\n        .add_directory(\"target/debug/build/embedded_code\")\n        .create()\n        .unwrap();\n\n    let root_path = &tree_fs.root;\n    let out_dir = root_path.join(\"target/debug/build/embedded_code\");\n\n    // Call function being tested\n    build_static_assets(&out_dir);\n\n    // Read and snapshot the generated code\n    let generated_path = out_dir.join(\"generated_code\");\n    let view_templates_path = generated_path.join(\"view_templates.rs\");\n    let template_content = std::fs::read_to_string(view_templates_path).unwrap();\n\n    let settings = create_insta_settings(root_path);\n    settings.bind(|| {\n        assert_snapshot!(\"complex_template_inheritance\", template_content);\n    });\n}\n"
  },
  {
    "path": "tests/build_scripts/mod.rs",
    "content": "mod embedded_assets;\n"
  },
  {
    "path": "tests/build_scripts/snapshots/r#mod__build_scripts__embedded_assets__build_static_assets_static.snap",
    "content": "---\nsource: tests/build_scripts/embedded_assets.rs\nexpression: static_content\nsnapshot_kind: text\n---\n#[must_use]\npub fn get_embedded_static_assets() -> std::collections::HashMap<String, &'static [u8]> {\n    let mut assets = std::collections::HashMap::new();\n    assets.insert(\"/css/style.css\".to_string(), include_bytes!(\"[TEST_ROOT]/assets/css/style.css\") as &[u8]);\n    assets.insert(\"/js/app.js\".to_string(), include_bytes!(\"[TEST_ROOT]/assets/js/app.js\") as &[u8]);\n    assets\n}\n"
  },
  {
    "path": "tests/build_scripts/snapshots/r#mod__build_scripts__embedded_assets__build_static_assets_templates.snap",
    "content": "---\nsource: tests/build_scripts/embedded_assets.rs\nexpression: template_content\nsnapshot_kind: text\n---\n/// Returns a BTreeMap of templates in dependency order (parents before children)\n#[must_use]\npub fn get_embedded_templates() -> std::collections::BTreeMap<String, &'static str> {\n    let mut templates = std::collections::BTreeMap::new();\n    // Base template with no parent\n    templates.insert(\"index.html\".to_string(), include_str!(\"[TEST_ROOT]/assets/views/index.html\"));\n\n    templates\n}\n"
  },
  {
    "path": "tests/build_scripts/snapshots/r#mod__build_scripts__embedded_assets__collected_all_files.snap",
    "content": "---\nsource: tests/build_scripts/embedded_assets.rs\nexpression: file_mappings\nsnapshot_kind: text\n---\n[\n    (\n        \"[TEST_ROOT]/assets/css/style.css\",\n        \"/css/style.css\",\n    ),\n    (\n        \"[TEST_ROOT]/assets/js/app.js\",\n        \"/js/app.js\",\n    ),\n    (\n        \"[TEST_ROOT]/assets/views/index.html\",\n        \"index.html\",\n    ),\n]\n"
  },
  {
    "path": "tests/build_scripts/snapshots/r#mod__build_scripts__embedded_assets__collected_css_files.snap",
    "content": "---\nsource: tests/build_scripts/embedded_assets.rs\nexpression: file_mappings\nsnapshot_kind: text\n---\n[\n    (\n        \"[TEST_ROOT]/assets/css/style.css\",\n        \"/css/style.css\",\n    ),\n]\n"
  },
  {
    "path": "tests/build_scripts/snapshots/r#mod__build_scripts__embedded_assets__complex_template_inheritance.snap",
    "content": "---\nsource: tests/build_scripts/embedded_assets.rs\nexpression: template_content\nsnapshot_kind: text\n---\n/// Returns a BTreeMap of templates in dependency order (parents before children)\n#[must_use]\npub fn get_embedded_templates() -> std::collections::BTreeMap<String, &'static str> {\n    let mut templates = std::collections::BTreeMap::new();\n    // Base template with no parent\n    templates.insert(\"base.html\".to_string(), include_str!(\"[TEST_ROOT]/assets/views/base.html\"));\n    // Base template with no parent\n    templates.insert(\"error.html\".to_string(), include_str!(\"[TEST_ROOT]/assets/views/error.html\"));\n    // Template that extends base.html\n    templates.insert(\"layouts/app.html\".to_string(), include_str!(\"[TEST_ROOT]/assets/views/layouts/app.html\"));\n    // Template that extends layouts/app.html\n    templates.insert(\"layouts/authenticated.html\".to_string(), include_str!(\"[TEST_ROOT]/assets/views/layouts/authenticated.html\"));\n    // Template that extends layouts/authenticated.html\n    templates.insert(\"dashboard/index.html\".to_string(), include_str!(\"[TEST_ROOT]/assets/views/dashboard/index.html\"));\n    // Template that extends layouts/authenticated.html\n    templates.insert(\"dashboard/settings.html\".to_string(), include_str!(\"[TEST_ROOT]/assets/views/dashboard/settings.html\"));\n\n    templates\n}\n"
  },
  {
    "path": "tests/build_scripts/snapshots/r#mod__build_scripts__embedded_assets__discovered_directories.snap",
    "content": "---\nsource: tests/build_scripts/embedded_assets.rs\nexpression: discovered_dirs\nsnapshot_kind: text\n---\n[\n    \"[TEST_ROOT]/my_assets\",\n    \"[TEST_ROOT]/my_assets/css\",\n    \"[TEST_ROOT]/my_assets/empty_dir\",\n    \"[TEST_ROOT]/my_assets/images\",\n    \"[TEST_ROOT]/my_assets/js\",\n    \"[TEST_ROOT]/my_assets/js/vendor\",\n    \"[TEST_ROOT]/my_assets/views\",\n    \"[TEST_ROOT]/my_assets/views/user\",\n]\n"
  },
  {
    "path": "tests/build_scripts/snapshots/r#mod__build_scripts__embedded_assets__empty_static_assets_rs.snap",
    "content": "---\nsource: tests/build_scripts/embedded_assets.rs\nexpression: static_file_content\nsnapshot_kind: text\n---\n#[must_use]\npub fn get_embedded_static_assets() -> std::collections::HashMap<String, &'static [u8]> {\n    // No assets found\n    std::collections::HashMap::new()\n}\n"
  },
  {
    "path": "tests/build_scripts/snapshots/r#mod__build_scripts__embedded_assets__empty_templates_rs.snap",
    "content": "---\nsource: tests/build_scripts/embedded_assets.rs\nexpression: templates_file_content\nsnapshot_kind: text\n---\n#[must_use]\npub fn get_embedded_templates() -> std::collections::HashMap<String, &'static str> {\n    // No templates found\n    std::collections::HashMap::new()\n}\n"
  },
  {
    "path": "tests/build_scripts/snapshots/r#mod__build_scripts__embedded_assets__static_assets_rs.snap",
    "content": "---\nsource: tests/build_scripts/embedded_assets.rs\nexpression: static_content\nsnapshot_kind: text\n---\n#[must_use]\npub fn get_embedded_static_assets() -> std::collections::HashMap<String, &'static [u8]> {\n    let mut assets = std::collections::HashMap::new();\n    assets.insert(\"/css/style.css\".to_string(), include_bytes!(\"[TEST_ROOT]/assets/css/style.css\") as &[u8]);\n    assets.insert(\"/js/app.js\".to_string(), include_bytes!(\"[TEST_ROOT]/assets/js/app.js\") as &[u8]);\n    assets\n}\n"
  },
  {
    "path": "tests/build_scripts/snapshots/r#mod__build_scripts__embedded_assets__template_inheritance.snap",
    "content": "---\nsource: tests/build_scripts/embedded_assets.rs\nexpression: template_content\nsnapshot_kind: text\n---\n#[must_use]\npub fn get_embedded_templates() -> std::collections::HashMap<String, &'static str> {\n    let mut templates = std::collections::HashMap::new();\n    // Debug log of template keys for inheritance:\n    // Template key: \"base.html\"\n    // Template key: \"posts/list.html\"\n    templates.insert(\"base.html\".to_string(), include_str!(\"[TEST_ROOT]/assets/views/base.html\"));\n    templates.insert(\"posts/list.html\".to_string(), include_str!(\"[TEST_ROOT]/assets/views/posts/list.html\"));\n    templates\n}\n"
  },
  {
    "path": "tests/build_scripts/snapshots/r#mod__build_scripts__embedded_assets__view_templates_rs.snap",
    "content": "---\nsource: tests/build_scripts/embedded_assets.rs\nexpression: template_content\nsnapshot_kind: text\n---\n/// Returns a BTreeMap of templates in dependency order (parents before children)\n#[must_use]\npub fn get_embedded_templates() -> std::collections::BTreeMap<String, &'static str> {\n    let mut templates = std::collections::BTreeMap::new();\n    // Base template with no parent\n    templates.insert(\"index.html\".to_string(), include_str!(\"[TEST_ROOT]/assets/views/index.html\"));\n\n    templates\n}\n"
  },
  {
    "path": "tests/controller/extractor/auth/api_token.rs",
    "content": "use loco_rs::{controller::extractor::auth, prelude::*, tests_cfg};\nuse serde::{Deserialize, Serialize};\n\nuse loco_rs::model::{Authenticable, ModelError};\n\nuse crate::infra_cfg;\n\n#[derive(Debug, Deserialize, Serialize)]\npub struct TestUserResponse {\n    pub pid: String,\n    pub user_id: i32,\n    pub user_email: String,\n}\n\n// Mock user struct for testing ApiToken extractor\n#[derive(Debug, Clone)]\nstruct TestUser {\n    id: i32,\n    email: String,\n}\n\n#[async_trait::async_trait]\nimpl Authenticable for TestUser {\n    async fn find_by_claims_key(\n        _db: &sea_orm::DatabaseConnection,\n        pid: &str,\n    ) -> Result<Self, ModelError> {\n        // Simple mock: return user if pid matches, otherwise not found\n        if pid == \"test_pid_123\" {\n            Ok(Self {\n                id: 1,\n                email: \"test@example.com\".to_string(),\n            })\n        } else {\n            Err(ModelError::EntityNotFound)\n        }\n    }\n\n    async fn find_by_api_key(\n        _db: &sea_orm::DatabaseConnection,\n        api_key: &str,\n    ) -> Result<Self, ModelError> {\n        // Simple mock: return user if api_key matches, otherwise not found\n        if api_key == \"test_api_key_123\" {\n            Ok(Self {\n                id: 1,\n                email: \"test@example.com\".to_string(),\n            })\n        } else {\n            Err(ModelError::EntityNotFound)\n        }\n    }\n}\n\n// Test handler for ApiToken extractor\nasync fn api_token_handler(auth: auth::ApiToken<TestUser>) -> Result<Response> {\n    format::json(TestUserResponse {\n        pid: String::new(), // API tokens don't have PIDs\n        user_id: auth.user.id,\n        user_email: auth.user.email,\n    })\n}\n\n// Test ApiToken extractor with valid API key\n#[tokio::test]\nasync fn can_extract_api_token_valid() {\n    let ctx = tests_cfg::app::get_app_context().await;\n\n    let port = get_available_port().await;\n    let handle =\n        infra_cfg::server::start_with_route(ctx, \"/\", get(api_token_handler), Some(port)).await;\n\n    let client = reqwest::Client::new();\n    let res = client\n        .get(get_base_url_port(port))\n        .header(\"Authorization\", \"Bearer test_api_key_123\")\n        .send()\n        .await\n        .expect(\"Valid response\");\n\n    assert_eq!(res.status(), 200);\n\n    let body: TestUserResponse = res.json().await.expect(\"Valid JSON response\");\n    assert_eq!(body.pid, \"\"); // API tokens don't have PIDs\n    assert_eq!(body.user_id, 1);\n    assert_eq!(body.user_email, \"test@example.com\");\n\n    handle.abort();\n}\n\n// Test ApiToken extractor with invalid API key\n#[tokio::test]\nasync fn can_handle_api_token_invalid() {\n    let ctx = tests_cfg::app::get_app_context().await;\n\n    let port = get_available_port().await;\n    let handle =\n        infra_cfg::server::start_with_route(ctx, \"/\", get(api_token_handler), Some(port)).await;\n\n    let client = reqwest::Client::new();\n    let res = client\n        .get(get_base_url_port(port))\n        .header(\"Authorization\", \"Bearer invalid_api_key\")\n        .send()\n        .await\n        .expect(\"Valid response\");\n\n    assert_eq!(res.status(), 401);\n\n    handle.abort();\n}\n\n// Test ApiToken extractor with missing Authorization header\n#[tokio::test]\nasync fn can_handle_api_token_missing() {\n    let ctx = tests_cfg::app::get_app_context().await;\n\n    let port = get_available_port().await;\n    let handle =\n        infra_cfg::server::start_with_route(ctx, \"/\", get(api_token_handler), Some(port)).await;\n\n    let client = reqwest::Client::new();\n    let res = client\n        .get(get_base_url_port(port))\n        .send()\n        .await\n        .expect(\"Valid response\");\n\n    assert_eq!(res.status(), 401);\n\n    handle.abort();\n}\n\n// Test response serialization\n#[tokio::test]\nasync fn test_user_response_serialization() {\n    let response = TestUserResponse {\n        pid: \"test_pid\".to_string(),\n        user_id: 1,\n        user_email: \"test@example.com\".to_string(),\n    };\n\n    let json = serde_json::to_string(&response).expect(\"Should serialize\");\n    let deserialized: TestUserResponse = serde_json::from_str(&json).expect(\"Should deserialize\");\n\n    assert_eq!(response.pid, deserialized.pid);\n    assert_eq!(response.user_id, deserialized.user_id);\n    assert_eq!(response.user_email, deserialized.user_email);\n}\n"
  },
  {
    "path": "tests/controller/extractor/auth/jwt.rs",
    "content": "use loco_rs::{controller::extractor::auth, prelude::*, tests_cfg};\nuse serde::{Deserialize, Serialize};\n\nuse crate::infra_cfg;\n\n#[derive(Debug, Deserialize, Serialize)]\npub struct TestResponse {\n    pub pid: String,\n}\n\n// Test handler for JWT extractor\nasync fn jwt_handler(auth: auth::JWT) -> Result<Response> {\n    format::json(TestResponse {\n        pid: auth.claims.pid,\n    })\n}\n\n// Test JWT extractor with valid token\n#[tokio::test]\nasync fn can_extract_jwt_with_valid_token() {\n    let mut ctx = tests_cfg::app::get_app_context().await;\n    let secret = \"PqRwLF2rhHe8J22oBeHy\".to_string();\n    ctx.config.auth = Some(loco_rs::config::Auth {\n        jwt: Some(loco_rs::config::JWT {\n            location: None,\n            secret: secret.clone(),\n            expiration: 3600,\n        }),\n    });\n    let jwt = loco_rs::auth::jwt::JWT::new(&secret);\n    let token = jwt\n        .generate_token(3600, \"test_pid_123\".to_string(), serde_json::Map::new())\n        .expect(\"Failed to generate token\");\n\n    let port = get_available_port().await;\n    let handle = infra_cfg::server::start_with_route(ctx, \"/\", get(jwt_handler), Some(port)).await;\n\n    let client = reqwest::Client::new();\n    let res = client\n        .get(get_base_url_port(port))\n        .header(\"Authorization\", format!(\"Bearer {token}\"))\n        .send()\n        .await\n        .expect(\"Valid response\");\n\n    assert_eq!(res.status(), 200);\n    let body: TestResponse = res.json().await.expect(\"Valid JSON response\");\n    assert_eq!(body.pid, \"test_pid_123\");\n    handle.abort();\n}\n\n// Test JWT extractor with invalid token\n#[tokio::test]\nasync fn can_handle_invalid_jwt_token() {\n    let mut ctx = tests_cfg::app::get_app_context().await;\n    let secret = \"PqRwLF2rhHe8J22oBeHy\".to_string();\n    ctx.config.auth = Some(loco_rs::config::Auth {\n        jwt: Some(loco_rs::config::JWT {\n            location: None,\n            secret: secret.clone(),\n            expiration: 3600,\n        }),\n    });\n\n    let port = get_available_port().await;\n    let handle = infra_cfg::server::start_with_route(ctx, \"/\", get(jwt_handler), Some(port)).await;\n\n    let client = reqwest::Client::new();\n    let res = client\n        .get(get_base_url_port(port))\n        .header(\"Authorization\", \"Bearer invalid_token\")\n        .send()\n        .await\n        .expect(\"Valid response\");\n\n    assert_eq!(res.status(), 401);\n    handle.abort();\n}\n\n// Test JWT extractor with missing token\n#[tokio::test]\nasync fn can_handle_missing_jwt_token() {\n    let mut ctx = tests_cfg::app::get_app_context().await;\n    let secret = \"PqRwLF2rhHe8J22oBeHy\".to_string();\n    ctx.config.auth = Some(loco_rs::config::Auth {\n        jwt: Some(loco_rs::config::JWT {\n            location: None,\n            secret: secret.clone(),\n            expiration: 3600,\n        }),\n    });\n\n    let port = get_available_port().await;\n    let handle = infra_cfg::server::start_with_route(ctx, \"/\", get(jwt_handler), Some(port)).await;\n\n    let client = reqwest::Client::new();\n    let res = client\n        .get(get_base_url_port(port))\n        .send()\n        .await\n        .expect(\"Valid response\");\n\n    assert_eq!(res.status(), 401);\n    handle.abort();\n}\n\n// Test JWT extractor with expired token\n#[tokio::test]\nasync fn can_handle_expired_jwt_token() {\n    let mut ctx = tests_cfg::app::get_app_context().await;\n    let secret = \"PqRwLF2rhHe8J22oBeHy\".to_string();\n    ctx.config.auth = Some(loco_rs::config::Auth {\n        jwt: Some(loco_rs::config::JWT {\n            location: None,\n            secret: secret.clone(),\n            expiration: 3600,\n        }),\n    });\n    let jwt = loco_rs::auth::jwt::JWT::new(&secret);\n    let token = jwt\n        .generate_token(1, \"test_pid_123\".to_string(), serde_json::Map::new())\n        .expect(\"Failed to generate token\");\n    tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;\n\n    let port = get_available_port().await;\n    let handle = infra_cfg::server::start_with_route(ctx, \"/\", get(jwt_handler), Some(port)).await;\n\n    let client = reqwest::Client::new();\n    let res = client\n        .get(get_base_url_port(port))\n        .header(\"Authorization\", format!(\"Bearer {token}\"))\n        .send()\n        .await\n        .expect(\"Valid response\");\n\n    assert_eq!(res.status(), 401);\n    handle.abort();\n}\n\n// Test JWT extractor with malformed authorization header\n#[tokio::test]\nasync fn can_handle_malformed_authorization_header() {\n    let mut ctx = tests_cfg::app::get_app_context().await;\n    let secret = \"PqRwLF2rhHe8J22oBeHy\".to_string();\n    ctx.config.auth = Some(loco_rs::config::Auth {\n        jwt: Some(loco_rs::config::JWT {\n            location: None,\n            secret: secret.clone(),\n            expiration: 3600,\n        }),\n    });\n\n    let port = get_available_port().await;\n    let handle = infra_cfg::server::start_with_route(ctx, \"/\", get(jwt_handler), Some(port)).await;\n\n    let client = reqwest::Client::new();\n    let res = client\n        .get(get_base_url_port(port))\n        .header(\"Authorization\", \"some_token\")\n        .send()\n        .await\n        .expect(\"Valid response\");\n\n    assert_eq!(res.status(), 401);\n    handle.abort();\n}\n\n// Test JWT extractor with missing JWT configuration\n#[tokio::test]\nasync fn can_handle_missing_jwt_configuration() {\n    let ctx = tests_cfg::app::get_app_context().await;\n\n    let port = get_available_port().await;\n    let handle = infra_cfg::server::start_with_route(ctx, \"/\", get(jwt_handler), Some(port)).await;\n\n    let client = reqwest::Client::new();\n    let res = client\n        .get(get_base_url_port(port))\n        .header(\"Authorization\", \"Bearer some_token\")\n        .send()\n        .await\n        .expect(\"Valid response\");\n\n    // When JWT config is missing, it should return 500 (Internal Server Error)\n    // because the extractor can't find the JWT configuration\n    assert_eq!(res.status(), 500);\n    handle.abort();\n}\n\n// Test JWT extractor with Cookie location\n#[tokio::test]\nasync fn can_extract_jwt_from_cookie() {\n    let mut ctx = tests_cfg::app::get_app_context().await;\n\n    // Configure JWT auth to use Cookie location\n    let secret = \"PqRwLF2rhHe8J22oBeHy\".to_string();\n    ctx.config.auth = Some(loco_rs::config::Auth {\n        jwt: Some(loco_rs::config::JWT {\n            location: Some(loco_rs::config::JWTLocationConfig::Single(\n                loco_rs::config::JWTLocation::Cookie {\n                    name: \"auth_token\".to_string(),\n                },\n            )),\n            secret: secret.clone(),\n            expiration: 3600,\n        }),\n    });\n\n    // Create a valid JWT token\n    let jwt = loco_rs::auth::jwt::JWT::new(&secret);\n    let token = jwt\n        .generate_token(3600, \"test_pid_123\".to_string(), serde_json::Map::new())\n        .expect(\"Failed to generate token\");\n\n    let port = get_available_port().await;\n    let handle = infra_cfg::server::start_with_route(ctx, \"/\", get(jwt_handler), Some(port)).await;\n\n    let client = reqwest::Client::new();\n    let res = client\n        .get(get_base_url_port(port))\n        .header(\"Cookie\", format!(\"auth_token={token}\"))\n        .send()\n        .await\n        .expect(\"Valid response\");\n\n    assert_eq!(res.status(), 200);\n\n    let body: TestResponse = res.json().await.expect(\"Valid JSON response\");\n    assert_eq!(body.pid, \"test_pid_123\");\n\n    handle.abort();\n}\n\n// Test JWT extractor with Query location\n#[tokio::test]\nasync fn can_extract_jwt_from_query() {\n    let mut ctx = tests_cfg::app::get_app_context().await;\n\n    // Configure JWT auth to use Query location\n    let secret = \"PqRwLF2rhHe8J22oBeHy\".to_string();\n    ctx.config.auth = Some(loco_rs::config::Auth {\n        jwt: Some(loco_rs::config::JWT {\n            location: Some(loco_rs::config::JWTLocationConfig::Single(\n                loco_rs::config::JWTLocation::Query {\n                    name: \"token\".to_string(),\n                },\n            )),\n            secret: secret.clone(),\n            expiration: 3600,\n        }),\n    });\n\n    // Create a valid JWT token\n    let jwt = loco_rs::auth::jwt::JWT::new(&secret);\n    let token = jwt\n        .generate_token(3600, \"test_pid_123\".to_string(), serde_json::Map::new())\n        .expect(\"Failed to generate token\");\n\n    let port = get_available_port().await;\n    let handle = infra_cfg::server::start_with_route(ctx, \"/\", get(jwt_handler), Some(port)).await;\n\n    let client = reqwest::Client::new();\n    let res = client\n        .get(format!(\"{}?token={}\", get_base_url_port(port), token))\n        .send()\n        .await\n        .expect(\"Valid response\");\n\n    assert_eq!(res.status(), 200);\n\n    let body: TestResponse = res.json().await.expect(\"Valid JSON response\");\n    assert_eq!(body.pid, \"test_pid_123\");\n\n    handle.abort();\n}\n\n// Test JWT extractor with multiple locations - Cookie first, then Query fallback\n#[tokio::test]\nasync fn can_extract_jwt_with_multiple_locations_cookie_fallback() {\n    let mut ctx = tests_cfg::app::get_app_context().await;\n\n    // Configure JWT auth to use multiple locations (Cookie first, then Query)\n    let secret = \"PqRwLF2rhHe8J22oBeHy\".to_string();\n    ctx.config.auth = Some(loco_rs::config::Auth {\n        jwt: Some(loco_rs::config::JWT {\n            location: Some(loco_rs::config::JWTLocationConfig::Multiple(vec![\n                loco_rs::config::JWTLocation::Cookie {\n                    name: \"nonexistent_cookie\".to_string(), // This will fail\n                },\n                loco_rs::config::JWTLocation::Query {\n                    name: \"token\".to_string(), // This will succeed\n                },\n            ])),\n            secret: secret.clone(),\n            expiration: 3600,\n        }),\n    });\n\n    // Create a valid JWT token\n    let jwt = loco_rs::auth::jwt::JWT::new(&secret);\n    let token = jwt\n        .generate_token(3600, \"test_pid_123\".to_string(), serde_json::Map::new())\n        .expect(\"Failed to generate token\");\n\n    let port = get_available_port().await;\n    let handle = infra_cfg::server::start_with_route(ctx, \"/\", get(jwt_handler), Some(port)).await;\n\n    let client = reqwest::Client::new();\n    let res = client\n        .get(format!(\"{}?token={}\", get_base_url_port(port), token))\n        .send()\n        .await\n        .expect(\"Valid response\");\n\n    assert_eq!(res.status(), 200);\n\n    let body: TestResponse = res.json().await.expect(\"Valid JSON response\");\n    assert_eq!(body.pid, \"test_pid_123\");\n\n    handle.abort();\n}\n\n// Test JWT extractor with multiple locations - Query first, then Bearer fallback\n#[tokio::test]\nasync fn can_extract_jwt_with_multiple_locations_query_fallback() {\n    let mut ctx = tests_cfg::app::get_app_context().await;\n\n    // Configure JWT auth to use multiple locations (Query first, then Bearer)\n    let secret = \"PqRwLF2rhHe8J22oBeHy\".to_string();\n    ctx.config.auth = Some(loco_rs::config::Auth {\n        jwt: Some(loco_rs::config::JWT {\n            location: Some(loco_rs::config::JWTLocationConfig::Multiple(vec![\n                loco_rs::config::JWTLocation::Query {\n                    name: \"missing_param\".to_string(), // This will fail\n                },\n                loco_rs::config::JWTLocation::Bearer, // This will succeed\n            ])),\n            secret: secret.clone(),\n            expiration: 3600,\n        }),\n    });\n\n    // Create a valid JWT token\n    let jwt = loco_rs::auth::jwt::JWT::new(&secret);\n    let token = jwt\n        .generate_token(3600, \"test_pid_123\".to_string(), serde_json::Map::new())\n        .expect(\"Failed to generate token\");\n\n    let port = get_available_port().await;\n    let handle = infra_cfg::server::start_with_route(ctx, \"/\", get(jwt_handler), Some(port)).await;\n\n    let client = reqwest::Client::new();\n    let res = client\n        .get(get_base_url_port(port))\n        .header(\"Authorization\", format!(\"Bearer {token}\"))\n        .send()\n        .await\n        .expect(\"Valid response\");\n\n    assert_eq!(res.status(), 200);\n\n    let body: TestResponse = res.json().await.expect(\"Valid JSON response\");\n    assert_eq!(body.pid, \"test_pid_123\");\n\n    handle.abort();\n}\n\n// Test JWT extractor with multiple locations - all locations fail\n#[tokio::test]\nasync fn can_handle_multiple_locations_all_fail() {\n    let mut ctx = tests_cfg::app::get_app_context().await;\n\n    // Configure JWT auth to use multiple locations that will all fail\n    let secret = \"PqRwLF2rhHe8J22oBeHy\".to_string();\n    ctx.config.auth = Some(loco_rs::config::Auth {\n        jwt: Some(loco_rs::config::JWT {\n            location: Some(loco_rs::config::JWTLocationConfig::Multiple(vec![\n                loco_rs::config::JWTLocation::Cookie {\n                    name: \"nonexistent_cookie\".to_string(),\n                },\n                loco_rs::config::JWTLocation::Query {\n                    name: \"missing_param\".to_string(),\n                },\n            ])),\n            secret: secret.clone(),\n            expiration: 3600,\n        }),\n    });\n\n    let port = get_available_port().await;\n    let handle = infra_cfg::server::start_with_route(ctx, \"/\", get(jwt_handler), Some(port)).await;\n\n    let client = reqwest::Client::new();\n    let res = client\n        .get(get_base_url_port(port))\n        .send()\n        .await\n        .expect(\"Valid response\");\n\n    assert_eq!(res.status(), 401);\n\n    handle.abort();\n}\n\n// Test JWT extractor with Cookie location - missing cookie\n#[tokio::test]\nasync fn can_handle_cookie_location_missing_cookie() {\n    let mut ctx = tests_cfg::app::get_app_context().await;\n\n    // Configure JWT auth to use Cookie location\n    let secret = \"PqRwLF2rhHe8J22oBeHy\".to_string();\n    ctx.config.auth = Some(loco_rs::config::Auth {\n        jwt: Some(loco_rs::config::JWT {\n            location: Some(loco_rs::config::JWTLocationConfig::Single(\n                loco_rs::config::JWTLocation::Cookie {\n                    name: \"auth_token\".to_string(),\n                },\n            )),\n            secret: secret.clone(),\n            expiration: 3600,\n        }),\n    });\n\n    let port = get_available_port().await;\n    let handle = infra_cfg::server::start_with_route(ctx, \"/\", get(jwt_handler), Some(port)).await;\n\n    let client = reqwest::Client::new();\n    let res = client\n        .get(get_base_url_port(port))\n        .send()\n        .await\n        .expect(\"Valid response\");\n\n    assert_eq!(res.status(), 401);\n\n    handle.abort();\n}\n\n// Test JWT extractor with Query location - missing query parameter\n#[tokio::test]\nasync fn can_handle_query_location_missing_param() {\n    let mut ctx = tests_cfg::app::get_app_context().await;\n\n    // Configure JWT auth to use Query location\n    let secret = \"PqRwLF2rhHe8J22oBeHy\".to_string();\n    ctx.config.auth = Some(loco_rs::config::Auth {\n        jwt: Some(loco_rs::config::JWT {\n            location: Some(loco_rs::config::JWTLocationConfig::Single(\n                loco_rs::config::JWTLocation::Query {\n                    name: \"token\".to_string(),\n                },\n            )),\n            secret: secret.clone(),\n            expiration: 3600,\n        }),\n    });\n\n    let port = get_available_port().await;\n    let handle = infra_cfg::server::start_with_route(ctx, \"/\", get(jwt_handler), Some(port)).await;\n\n    let client = reqwest::Client::new();\n    let res = client\n        .get(get_base_url_port(port))\n        .send()\n        .await\n        .expect(\"Valid response\");\n\n    assert_eq!(res.status(), 401);\n\n    handle.abort();\n}\n\n// Test JWT extractor with JWT that has wrong algorithm\n#[tokio::test]\nasync fn can_handle_jwt_with_wrong_algorithm() {\n    let mut ctx = tests_cfg::app::get_app_context().await;\n    let secret = \"PqRwLF2rhHe8J22oBeHy\".to_string();\n    ctx.config.auth = Some(loco_rs::config::Auth {\n        jwt: Some(loco_rs::config::JWT {\n            location: None,\n            secret: secret.clone(),\n            expiration: 3600,\n        }),\n    });\n\n    // Create a JWT with different secret (simulating wrong algorithm)\n    // Use a valid base64-encoded secret\n    let different_secret = \"DifferentSecretKey123456789012345678901234567890\".to_string();\n    let jwt = loco_rs::auth::jwt::JWT::new(&different_secret);\n    let token = jwt\n        .generate_token(3600, \"test_pid_123\".to_string(), serde_json::Map::new())\n        .expect(\"Failed to generate token\");\n\n    let port = get_available_port().await;\n    let handle = infra_cfg::server::start_with_route(ctx, \"/\", get(jwt_handler), Some(port)).await;\n\n    let client = reqwest::Client::new();\n    let res = client\n        .get(get_base_url_port(port))\n        .header(\"Authorization\", format!(\"Bearer {token}\"))\n        .send()\n        .await\n        .expect(\"Valid response\");\n\n    assert_eq!(res.status(), 401);\n    handle.abort();\n}\n\n// Test JWT extractor with JWT that has invalid signature but valid format\n#[tokio::test]\nasync fn can_handle_jwt_with_invalid_signature() {\n    let mut ctx = tests_cfg::app::get_app_context().await;\n    let secret = \"PqRwLF2rhHe8J22oBeHy\".to_string();\n    ctx.config.auth = Some(loco_rs::config::Auth {\n        jwt: Some(loco_rs::config::JWT {\n            location: None,\n            secret: secret.clone(),\n            expiration: 3600,\n        }),\n    });\n\n    // Create a valid JWT then modify it to have invalid signature\n    let jwt = loco_rs::auth::jwt::JWT::new(&secret);\n    let mut token = jwt\n        .generate_token(3600, \"test_pid_123\".to_string(), serde_json::Map::new())\n        .expect(\"Failed to generate token\");\n\n    // Corrupt the signature by changing the last character\n    if let Some(last_char) = token.chars().last() {\n        let new_char = if last_char == 'A' { 'B' } else { 'A' };\n        token.pop();\n        token.push(new_char);\n    }\n\n    let port = get_available_port().await;\n    let handle = infra_cfg::server::start_with_route(ctx, \"/\", get(jwt_handler), Some(port)).await;\n\n    let client = reqwest::Client::new();\n    let res = client\n        .get(get_base_url_port(port))\n        .header(\"Authorization\", format!(\"Bearer {token}\"))\n        .send()\n        .await\n        .expect(\"Valid response\");\n\n    assert_eq!(res.status(), 401);\n    handle.abort();\n}\n\n// Test JWT extractor with malformed JWT structure\n#[tokio::test]\nasync fn can_handle_malformed_jwt_structure() {\n    let mut ctx = tests_cfg::app::get_app_context().await;\n    let secret = \"PqRwLF2rhHe8J22oBeHy\".to_string();\n    ctx.config.auth = Some(loco_rs::config::Auth {\n        jwt: Some(loco_rs::config::JWT {\n            location: None,\n            secret: secret.clone(),\n            expiration: 3600,\n        }),\n    });\n\n    let port = get_available_port().await;\n    let handle = infra_cfg::server::start_with_route(ctx, \"/\", get(jwt_handler), Some(port)).await;\n\n    let client = reqwest::Client::new();\n    let res = client\n        .get(get_base_url_port(port))\n        .header(\"Authorization\", \"Bearer not.a.valid.jwt\")\n        .send()\n        .await\n        .expect(\"Valid response\");\n\n    assert_eq!(res.status(), 401);\n    handle.abort();\n}\n\n// Test JWT extractor with cookie containing special characters\n#[tokio::test]\nasync fn can_extract_jwt_from_cookie_with_special_chars() {\n    let mut ctx = tests_cfg::app::get_app_context().await;\n\n    // Configure JWT auth to use Cookie location\n    let secret = \"PqRwLF2rhHe8J22oBeHy\".to_string();\n    ctx.config.auth = Some(loco_rs::config::Auth {\n        jwt: Some(loco_rs::config::JWT {\n            location: Some(loco_rs::config::JWTLocationConfig::Single(\n                loco_rs::config::JWTLocation::Cookie {\n                    name: \"auth_token\".to_string(),\n                },\n            )),\n            secret: secret.clone(),\n            expiration: 3600,\n        }),\n    });\n\n    // Create a valid JWT token\n    let jwt = loco_rs::auth::jwt::JWT::new(&secret);\n    let token = jwt\n        .generate_token(3600, \"test_pid_123\".to_string(), serde_json::Map::new())\n        .expect(\"Failed to generate token\");\n\n    let port = get_available_port().await;\n    let handle = infra_cfg::server::start_with_route(ctx, \"/\", get(jwt_handler), Some(port)).await;\n\n    let client = reqwest::Client::new();\n    let res = client\n        .get(get_base_url_port(port))\n        .header(\"Cookie\", format!(\"auth_token={token}\"))\n        .send()\n        .await\n        .expect(\"Valid response\");\n\n    assert_eq!(res.status(), 200);\n\n    let body: TestResponse = res.json().await.expect(\"Valid JSON response\");\n    assert_eq!(body.pid, \"test_pid_123\");\n\n    handle.abort();\n}\n\n// Test JWT extractor with cookie containing empty value\n#[tokio::test]\nasync fn can_handle_cookie_with_empty_value() {\n    let mut ctx = tests_cfg::app::get_app_context().await;\n\n    // Configure JWT auth to use Cookie location\n    let secret = \"PqRwLF2rhHe8J22oBeHy\".to_string();\n    ctx.config.auth = Some(loco_rs::config::Auth {\n        jwt: Some(loco_rs::config::JWT {\n            location: Some(loco_rs::config::JWTLocationConfig::Single(\n                loco_rs::config::JWTLocation::Cookie {\n                    name: \"auth_token\".to_string(),\n                },\n            )),\n            secret: secret.clone(),\n            expiration: 3600,\n        }),\n    });\n\n    let port = get_available_port().await;\n    let handle = infra_cfg::server::start_with_route(ctx, \"/\", get(jwt_handler), Some(port)).await;\n\n    let client = reqwest::Client::new();\n    let res = client\n        .get(get_base_url_port(port))\n        .header(\"Cookie\", \"auth_token=\")\n        .send()\n        .await\n        .expect(\"Valid response\");\n\n    assert_eq!(res.status(), 401);\n    handle.abort();\n}\n\n// Test JWT extractor with query parameter containing special characters\n#[tokio::test]\nasync fn can_extract_jwt_from_query_with_special_chars() {\n    let mut ctx = tests_cfg::app::get_app_context().await;\n\n    // Configure JWT auth to use Query location\n    let secret = \"PqRwLF2rhHe8J22oBeHy\".to_string();\n    ctx.config.auth = Some(loco_rs::config::Auth {\n        jwt: Some(loco_rs::config::JWT {\n            location: Some(loco_rs::config::JWTLocationConfig::Single(\n                loco_rs::config::JWTLocation::Query {\n                    name: \"token\".to_string(),\n                },\n            )),\n            secret: secret.clone(),\n            expiration: 3600,\n        }),\n    });\n\n    // Create a valid JWT token\n    let jwt = loco_rs::auth::jwt::JWT::new(&secret);\n    let token = jwt\n        .generate_token(3600, \"test_pid_123\".to_string(), serde_json::Map::new())\n        .expect(\"Failed to generate token\");\n\n    let port = get_available_port().await;\n    let handle = infra_cfg::server::start_with_route(ctx, \"/\", get(jwt_handler), Some(port)).await;\n\n    let client = reqwest::Client::new();\n    let res = client\n        .get(format!(\"{}?token={}\", get_base_url_port(port), token))\n        .send()\n        .await\n        .expect(\"Valid response\");\n\n    assert_eq!(res.status(), 200);\n\n    let body: TestResponse = res.json().await.expect(\"Valid JSON response\");\n    assert_eq!(body.pid, \"test_pid_123\");\n\n    handle.abort();\n}\n\n// Test JWT extractor with query parameter containing empty value\n#[tokio::test]\nasync fn can_handle_query_with_empty_value() {\n    let mut ctx = tests_cfg::app::get_app_context().await;\n\n    // Configure JWT auth to use Query location\n    let secret = \"PqRwLF2rhHe8J22oBeHy\".to_string();\n    ctx.config.auth = Some(loco_rs::config::Auth {\n        jwt: Some(loco_rs::config::JWT {\n            location: Some(loco_rs::config::JWTLocationConfig::Single(\n                loco_rs::config::JWTLocation::Query {\n                    name: \"token\".to_string(),\n                },\n            )),\n            secret: secret.clone(),\n            expiration: 3600,\n        }),\n    });\n\n    let port = get_available_port().await;\n    let handle = infra_cfg::server::start_with_route(ctx, \"/\", get(jwt_handler), Some(port)).await;\n\n    let client = reqwest::Client::new();\n    let res = client\n        .get(format!(\"{}?token=\", get_base_url_port(port)))\n        .send()\n        .await\n        .expect(\"Valid response\");\n\n    assert_eq!(res.status(), 401);\n    handle.abort();\n}\n\n// Test JWT extractor with query parameter containing spaces\n#[tokio::test]\nasync fn can_handle_query_with_spaces() {\n    let mut ctx = tests_cfg::app::get_app_context().await;\n\n    // Configure JWT auth to use Query location\n    let secret = \"PqRwLF2rhHe8J22oBeHy\".to_string();\n    ctx.config.auth = Some(loco_rs::config::Auth {\n        jwt: Some(loco_rs::config::JWT {\n            location: Some(loco_rs::config::JWTLocationConfig::Single(\n                loco_rs::config::JWTLocation::Query {\n                    name: \"token\".to_string(),\n                },\n            )),\n            secret: secret.clone(),\n            expiration: 3600,\n        }),\n    });\n\n    let port = get_available_port().await;\n    let handle = infra_cfg::server::start_with_route(ctx, \"/\", get(jwt_handler), Some(port)).await;\n\n    let client = reqwest::Client::new();\n    let res = client\n        .get(format!(\n            \"{}?token=invalid token with spaces\",\n            get_base_url_port(port)\n        ))\n        .send()\n        .await\n        .expect(\"Valid response\");\n\n    assert_eq!(res.status(), 401);\n    handle.abort();\n}\n\n// Test JWT extractor error message for missing token\n#[tokio::test]\nasync fn can_validate_error_message_for_missing_token() {\n    let mut ctx = tests_cfg::app::get_app_context().await;\n    let secret = \"PqRwLF2rhHe8J22oBeHy\".to_string();\n    ctx.config.auth = Some(loco_rs::config::Auth {\n        jwt: Some(loco_rs::config::JWT {\n            location: None,\n            secret: secret.clone(),\n            expiration: 3600,\n        }),\n    });\n\n    let port = get_available_port().await;\n    let handle = infra_cfg::server::start_with_route(ctx, \"/\", get(jwt_handler), Some(port)).await;\n\n    let client = reqwest::Client::new();\n    let res = client\n        .get(get_base_url_port(port))\n        .send()\n        .await\n        .expect(\"Valid response\");\n\n    assert_eq!(res.status(), 401);\n\n    // The error message should be consistent\n    let _error_text = res.text().await.expect(\"Error response should have text\");\n    // Note: The actual error message format depends on the error handling implementation\n    // This test ensures we get a 401 status for missing token\n    handle.abort();\n}\n\n// Test JWT extractor error message for invalid token\n#[tokio::test]\nasync fn can_validate_error_message_for_invalid_token() {\n    let mut ctx = tests_cfg::app::get_app_context().await;\n    let secret = \"PqRwLF2rhHe8J22oBeHy\".to_string();\n    ctx.config.auth = Some(loco_rs::config::Auth {\n        jwt: Some(loco_rs::config::JWT {\n            location: None,\n            secret: secret.clone(),\n            expiration: 3600,\n        }),\n    });\n\n    let port = get_available_port().await;\n    let handle = infra_cfg::server::start_with_route(ctx, \"/\", get(jwt_handler), Some(port)).await;\n\n    let client = reqwest::Client::new();\n    let res = client\n        .get(get_base_url_port(port))\n        .header(\"Authorization\", \"Bearer invalid_token\")\n        .send()\n        .await\n        .expect(\"Valid response\");\n\n    assert_eq!(res.status(), 401);\n\n    // The error message should be consistent for invalid tokens\n    let _error_text = res.text().await.expect(\"Error response should have text\");\n    // Note: The actual error message format depends on the error handling implementation\n    // This test ensures we get a 401 status for invalid token\n    handle.abort();\n}\n\n// Test JWT extractor error message for malformed authorization header\n#[tokio::test]\nasync fn can_validate_error_message_for_malformed_header() {\n    let mut ctx = tests_cfg::app::get_app_context().await;\n    let secret = \"PqRwLF2rhHe8J22oBeHy\".to_string();\n    ctx.config.auth = Some(loco_rs::config::Auth {\n        jwt: Some(loco_rs::config::JWT {\n            location: None,\n            secret: secret.clone(),\n            expiration: 3600,\n        }),\n    });\n\n    let port = get_available_port().await;\n    let handle = infra_cfg::server::start_with_route(ctx, \"/\", get(jwt_handler), Some(port)).await;\n\n    let client = reqwest::Client::new();\n    let res = client\n        .get(get_base_url_port(port))\n        .header(\"Authorization\", \"InvalidPrefix token\")\n        .send()\n        .await\n        .expect(\"Valid response\");\n\n    assert_eq!(res.status(), 401);\n\n    // The error message should be consistent for malformed headers\n    let _error_text = res.text().await.expect(\"Error response should have text\");\n    // Note: The actual error message format depends on the error handling implementation\n    // This test ensures we get a 401 status for malformed header\n    handle.abort();\n}\n\n// Test JWT extractor with JWT that expires exactly at current time\n#[tokio::test]\nasync fn can_handle_jwt_expires_exactly_at_current_time() {\n    let mut ctx = tests_cfg::app::get_app_context().await;\n    let secret = \"PqRwLF2rhHe8J22oBeHy\".to_string();\n    ctx.config.auth = Some(loco_rs::config::Auth {\n        jwt: Some(loco_rs::config::JWT {\n            location: None,\n            secret: secret.clone(),\n            expiration: 3600,\n        }),\n    });\n\n    // Create a JWT that expires exactly at current time (0 seconds from now)\n    let jwt = loco_rs::auth::jwt::JWT::new(&secret);\n    let token = jwt\n        .generate_token(0, \"test_pid_123\".to_string(), serde_json::Map::new())\n        .expect(\"Failed to generate token\");\n\n    let port = get_available_port().await;\n    let handle = infra_cfg::server::start_with_route(ctx, \"/\", get(jwt_handler), Some(port)).await;\n\n    let client = reqwest::Client::new();\n    let res = client\n        .get(get_base_url_port(port))\n        .header(\"Authorization\", format!(\"Bearer {token}\"))\n        .send()\n        .await\n        .expect(\"Valid response\");\n\n    // JWT should be considered expired if exp is exactly at current time\n    assert_eq!(res.status(), 401);\n    handle.abort();\n}\n\n// Test JWT extractor with JWT that expired 1 second ago\n#[tokio::test]\nasync fn can_handle_jwt_expired_one_second_ago() {\n    let mut ctx = tests_cfg::app::get_app_context().await;\n    let secret = \"PqRwLF2rhHe8J22oBeHy\".to_string();\n    ctx.config.auth = Some(loco_rs::config::Auth {\n        jwt: Some(loco_rs::config::JWT {\n            location: None,\n            secret: secret.clone(),\n            expiration: 3600,\n        }),\n    });\n\n    // Create a JWT that expired 1 second ago\n    // We'll use a negative expiration to simulate past expiration\n    let jwt = loco_rs::auth::jwt::JWT::new(&secret);\n    let token = jwt\n        .generate_token(0, \"test_pid_123\".to_string(), serde_json::Map::new())\n        .expect(\"Failed to generate token\");\n\n    // Wait 1 second to ensure the token is expired\n    tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;\n\n    let port = get_available_port().await;\n    let handle = infra_cfg::server::start_with_route(ctx, \"/\", get(jwt_handler), Some(port)).await;\n\n    let client = reqwest::Client::new();\n    let res = client\n        .get(get_base_url_port(port))\n        .header(\"Authorization\", format!(\"Bearer {token}\"))\n        .send()\n        .await\n        .expect(\"Valid response\");\n\n    assert_eq!(res.status(), 401);\n    handle.abort();\n}\n\n// Test JWT extractor with JWT that expires in 1 second (just valid)\n#[tokio::test]\nasync fn can_handle_jwt_expires_in_one_second() {\n    let mut ctx = tests_cfg::app::get_app_context().await;\n    let secret = \"PqRwLF2rhHe8J22oBeHy\".to_string();\n    ctx.config.auth = Some(loco_rs::config::Auth {\n        jwt: Some(loco_rs::config::JWT {\n            location: None,\n            secret: secret.clone(),\n            expiration: 3600,\n        }),\n    });\n\n    // Create a JWT that expires in 5 seconds to account for test setup time\n    let jwt = loco_rs::auth::jwt::JWT::new(&secret);\n    let token = jwt\n        .generate_token(5, \"test_pid_123\".to_string(), serde_json::Map::new())\n        .expect(\"Failed to generate token\");\n\n    let port = get_available_port().await;\n    let handle = infra_cfg::server::start_with_route(ctx, \"/\", get(jwt_handler), Some(port)).await;\n\n    let client = reqwest::Client::new();\n    let res = client\n        .get(get_base_url_port(port))\n        .header(\"Authorization\", format!(\"Bearer {token}\"))\n        .send()\n        .await\n        .expect(\"Valid response\");\n\n    // JWT should be valid if it expires in 5 seconds\n    assert_eq!(res.status(), 200);\n\n    let body: TestResponse = res.json().await.expect(\"Valid JSON response\");\n    assert_eq!(body.pid, \"test_pid_123\");\n\n    handle.abort();\n}\n\n// Test JWT extractor with JWT that has missing exp claim\n#[tokio::test]\nasync fn can_handle_jwt_with_missing_exp_claim() {\n    let mut ctx = tests_cfg::app::get_app_context().await;\n    let secret = \"PqRwLF2rhHe8J22oBeHy\".to_string();\n    ctx.config.auth = Some(loco_rs::config::Auth {\n        jwt: Some(loco_rs::config::JWT {\n            location: None,\n            secret: secret.clone(),\n            expiration: 3600,\n        }),\n    });\n\n    // Create a JWT manually without exp claim\n    // This simulates a JWT that was created without proper exp handling\n    let _jwt = loco_rs::auth::jwt::JWT::new(&secret);\n\n    // For this test, we'll use an invalid JWT structure that would fail validation\n    // since we can't easily create a JWT without exp claim using the current API\n    let token = \"invalid.jwt.without.exp\".to_string();\n\n    let port = get_available_port().await;\n    let handle = infra_cfg::server::start_with_route(ctx, \"/\", get(jwt_handler), Some(port)).await;\n\n    let client = reqwest::Client::new();\n    let res = client\n        .get(get_base_url_port(port))\n        .header(\"Authorization\", format!(\"Bearer {token}\"))\n        .send()\n        .await\n        .expect(\"Valid response\");\n\n    assert_eq!(res.status(), 401);\n    handle.abort();\n}\n\n// Test JWT extractor with JWT that has invalid exp claim format\n#[tokio::test]\nasync fn can_handle_jwt_with_invalid_exp_claim() {\n    let mut ctx = tests_cfg::app::get_app_context().await;\n    let secret = \"PqRwLF2rhHe8J22oBeHy\".to_string();\n    ctx.config.auth = Some(loco_rs::config::Auth {\n        jwt: Some(loco_rs::config::JWT {\n            location: None,\n            secret: secret.clone(),\n            expiration: 3600,\n        }),\n    });\n\n    // Create a JWT with invalid exp claim format\n    // This simulates a JWT with malformed exp claim\n    let _jwt = loco_rs::auth::jwt::JWT::new(&secret);\n\n    // Corrupt the JWT to simulate invalid exp claim\n    // This will make the JWT invalid\n    let token = \"invalid.jwt.with.bad.exp\".to_string();\n\n    let port = get_available_port().await;\n    let handle = infra_cfg::server::start_with_route(ctx, \"/\", get(jwt_handler), Some(port)).await;\n\n    let client = reqwest::Client::new();\n    let res = client\n        .get(get_base_url_port(port))\n        .header(\"Authorization\", format!(\"Bearer {token}\"))\n        .send()\n        .await\n        .expect(\"Valid response\");\n\n    assert_eq!(res.status(), 401);\n    handle.abort();\n}\n\n// Test JWT extractor with JWT that expires in the very distant future\n#[tokio::test]\nasync fn can_handle_jwt_with_distant_future_expiration() {\n    let mut ctx = tests_cfg::app::get_app_context().await;\n    let secret = \"PqRwLF2rhHe8J22oBeHy\".to_string();\n    ctx.config.auth = Some(loco_rs::config::Auth {\n        jwt: Some(loco_rs::config::JWT {\n            location: None,\n            secret: secret.clone(),\n            expiration: 3600,\n        }),\n    });\n\n    // Create a JWT that expires in 10 years (very distant future)\n    let jwt = loco_rs::auth::jwt::JWT::new(&secret);\n    let token = jwt\n        .generate_token(\n            315_360_000,\n            \"test_pid_123\".to_string(),\n            serde_json::Map::new(),\n        ) // 10 years in seconds\n        .expect(\"Failed to generate token\");\n\n    let port = get_available_port().await;\n    let handle = infra_cfg::server::start_with_route(ctx, \"/\", get(jwt_handler), Some(port)).await;\n\n    let client = reqwest::Client::new();\n    let res = client\n        .get(get_base_url_port(port))\n        .header(\"Authorization\", format!(\"Bearer {token}\"))\n        .send()\n        .await\n        .expect(\"Valid response\");\n\n    // JWT should be valid if it expires in the distant future\n    assert_eq!(res.status(), 200);\n\n    let body: TestResponse = res.json().await.expect(\"Valid JSON response\");\n    assert_eq!(body.pid, \"test_pid_123\");\n\n    handle.abort();\n}\n\n// Test JWT extractor with JWT that has exp claim at epoch time (1970)\n#[tokio::test]\nasync fn can_handle_jwt_with_epoch_expiration() {\n    let mut ctx = tests_cfg::app::get_app_context().await;\n    let secret = \"PqRwLF2rhHe8J22oBeHy\".to_string();\n    ctx.config.auth = Some(loco_rs::config::Auth {\n        jwt: Some(loco_rs::config::JWT {\n            location: None,\n            secret: secret.clone(),\n            expiration: 3600,\n        }),\n    });\n\n    // Create a JWT that expired at epoch time (1970)\n    // This simulates a JWT with exp=0 or very old timestamp\n    let jwt = loco_rs::auth::jwt::JWT::new(&secret);\n\n    // Generate a token with 0 expiration (epoch time)\n    let token = jwt\n        .generate_token(0, \"test_pid_123\".to_string(), serde_json::Map::new())\n        .expect(\"Failed to generate token\");\n\n    let port = get_available_port().await;\n    let handle = infra_cfg::server::start_with_route(ctx, \"/\", get(jwt_handler), Some(port)).await;\n\n    let client = reqwest::Client::new();\n    let res = client\n        .get(get_base_url_port(port))\n        .header(\"Authorization\", format!(\"Bearer {token}\"))\n        .send()\n        .await\n        .expect(\"Valid response\");\n\n    // JWT should be expired if exp is at epoch time\n    assert_eq!(res.status(), 401);\n    handle.abort();\n}\n"
  },
  {
    "path": "tests/controller/extractor/auth/jwt_with_user.rs",
    "content": "use loco_rs::{controller::extractor::auth, prelude::*, tests_cfg};\nuse serde::{Deserialize, Serialize};\n\nuse loco_rs::model::{Authenticable, ModelError};\n\nuse crate::infra_cfg;\n\n#[derive(Debug, Deserialize, Serialize)]\npub struct TestUserResponse {\n    pub pid: String,\n    pub user_id: i32,\n    pub user_email: String,\n}\n\n// Mock user struct for testing JWTWithUser extractor\n#[derive(Debug, Clone)]\nstruct TestUser {\n    id: i32,\n    email: String,\n}\n\n#[async_trait::async_trait]\nimpl Authenticable for TestUser {\n    async fn find_by_claims_key(\n        _db: &sea_orm::DatabaseConnection,\n        pid: &str,\n    ) -> Result<Self, ModelError> {\n        // Simple mock: return user if pid matches, otherwise not found\n        if pid == \"test_pid_123\" {\n            Ok(Self {\n                id: 1,\n                email: \"test@example.com\".to_string(),\n            })\n        } else {\n            Err(ModelError::EntityNotFound)\n        }\n    }\n\n    async fn find_by_api_key(\n        _db: &sea_orm::DatabaseConnection,\n        api_key: &str,\n    ) -> Result<Self, ModelError> {\n        // Simple mock: return user if api_key matches, otherwise not found\n        if api_key == \"test_api_key_123\" {\n            Ok(Self {\n                id: 1,\n                email: \"test@example.com\".to_string(),\n            })\n        } else {\n            Err(ModelError::EntityNotFound)\n        }\n    }\n}\n\n// Test handler for JWTWithUser extractor\nasync fn jwt_with_user_handler(auth: auth::JWTWithUser<TestUser>) -> Result<Response> {\n    format::json(TestUserResponse {\n        pid: auth.claims.pid,\n        user_id: auth.user.id,\n        user_email: auth.user.email,\n    })\n}\n\n// Test JWTWithUser extractor with valid token\n#[tokio::test]\nasync fn can_extract_jwt_with_user_valid_token() {\n    let mut ctx = tests_cfg::app::get_app_context().await;\n\n    // Configure JWT auth\n    let secret = \"PqRwLF2rhHe8J22oBeHy\".to_string();\n    ctx.config.auth = Some(loco_rs::config::Auth {\n        jwt: Some(loco_rs::config::JWT {\n            location: None,\n            secret: secret.clone(),\n            expiration: 3600,\n        }),\n    });\n\n    // Create a valid JWT token with known PID\n    let jwt = loco_rs::auth::jwt::JWT::new(&secret);\n    let token = jwt\n        .generate_token(3600, \"test_pid_123\".to_string(), serde_json::Map::new())\n        .expect(\"Failed to generate token\");\n\n    let port = get_available_port().await;\n    let handle =\n        infra_cfg::server::start_with_route(ctx, \"/\", get(jwt_with_user_handler), Some(port)).await;\n\n    let client = reqwest::Client::new();\n    let res = client\n        .get(get_base_url_port(port))\n        .header(\"Authorization\", format!(\"Bearer {token}\"))\n        .send()\n        .await\n        .expect(\"Valid response\");\n\n    assert_eq!(res.status(), 200);\n\n    let body: TestUserResponse = res.json().await.expect(\"Valid JSON response\");\n    assert_eq!(body.pid, \"test_pid_123\");\n    assert_eq!(body.user_id, 1);\n    assert_eq!(body.user_email, \"test@example.com\");\n\n    handle.abort();\n}\n\n// Test JWTWithUser extractor with invalid token\n#[tokio::test]\nasync fn can_handle_jwt_with_user_invalid_token() {\n    let mut ctx = tests_cfg::app::get_app_context().await;\n\n    // Configure JWT auth\n    let secret = \"PqRwLF2rhHe8J22oBeHy\".to_string();\n    ctx.config.auth = Some(loco_rs::config::Auth {\n        jwt: Some(loco_rs::config::JWT {\n            location: None,\n            secret: secret.clone(),\n            expiration: 3600,\n        }),\n    });\n\n    let port = get_available_port().await;\n    let handle =\n        infra_cfg::server::start_with_route(ctx, \"/\", get(jwt_with_user_handler), Some(port)).await;\n\n    let client = reqwest::Client::new();\n    let res = client\n        .get(get_base_url_port(port))\n        .header(\"Authorization\", \"Bearer invalid_token\")\n        .send()\n        .await\n        .expect(\"Valid response\");\n\n    assert_eq!(res.status(), 401);\n\n    handle.abort();\n}\n\n// Test JWTWithUser extractor with non-existent user\n#[tokio::test]\nasync fn can_handle_jwt_with_user_nonexistent_user() {\n    let mut ctx = tests_cfg::app::get_app_context().await;\n\n    // Configure JWT auth\n    let secret = \"PqRwLF2rhHe8J22oBeHy\".to_string();\n    ctx.config.auth = Some(loco_rs::config::Auth {\n        jwt: Some(loco_rs::config::JWT {\n            location: None,\n            secret: secret.clone(),\n            expiration: 3600,\n        }),\n    });\n\n    // Create a valid JWT token with unknown PID\n    let jwt = loco_rs::auth::jwt::JWT::new(&secret);\n    let token = jwt\n        .generate_token(3600, \"unknown_pid\".to_string(), serde_json::Map::new())\n        .expect(\"Failed to generate token\");\n\n    let port = get_available_port().await;\n    let handle =\n        infra_cfg::server::start_with_route(ctx, \"/\", get(jwt_with_user_handler), Some(port)).await;\n\n    let client = reqwest::Client::new();\n    let res = client\n        .get(get_base_url_port(port))\n        .header(\"Authorization\", format!(\"Bearer {token}\"))\n        .send()\n        .await\n        .expect(\"Valid response\");\n\n    assert_eq!(res.status(), 401);\n\n    handle.abort();\n}\n\n// Test JWTWithUser extractor with missing token\n#[tokio::test]\nasync fn can_handle_jwt_with_user_missing_token() {\n    let mut ctx = tests_cfg::app::get_app_context().await;\n\n    // Configure JWT auth\n    let secret = \"PqRwLF2rhHe8J22oBeHy\".to_string();\n    ctx.config.auth = Some(loco_rs::config::Auth {\n        jwt: Some(loco_rs::config::JWT {\n            location: None,\n            secret: secret.clone(),\n            expiration: 3600,\n        }),\n    });\n\n    let port = get_available_port().await;\n    let handle =\n        infra_cfg::server::start_with_route(ctx, \"/\", get(jwt_with_user_handler), Some(port)).await;\n\n    let client = reqwest::Client::new();\n    let res = client\n        .get(get_base_url_port(port))\n        .send()\n        .await\n        .expect(\"Valid response\");\n\n    assert_eq!(res.status(), 401);\n\n    handle.abort();\n}\n"
  },
  {
    "path": "tests/controller/extractor/auth/mod.rs",
    "content": "mod jwt;\n\n#[cfg(feature = \"with-db\")]\nmod jwt_with_user;\n\n#[cfg(feature = \"with-db\")]\nmod api_token;\n"
  },
  {
    "path": "tests/controller/extractor/mod.rs",
    "content": "mod auth;\nmod shared_store;\nmod validate;\n"
  },
  {
    "path": "tests/controller/extractor/shared_store.rs",
    "content": "use axum::extract::State;\nuse loco_rs::{controller::format, prelude::*, tests_cfg};\nuse rstest::rstest;\nuse serde::{Deserialize, Serialize};\n\nuse crate::infra_cfg;\n\n#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]\nstruct MySharedData {\n    message: String,\n}\n\nstruct MySharedDataWithoutClone {\n    message: String,\n}\n\n#[rstest]\n#[case(true)]\n#[case(false)]\n#[tokio::test]\nasync fn test_shared_store_extractor(#[case] exists: bool) {\n    async fn action(\n        State(_ctx): State<AppContext>,\n        SharedStore(shared_data): SharedStore<MySharedData>,\n    ) -> Result<Response> {\n        format::json(&shared_data)\n    }\n\n    let ctx: AppContext = tests_cfg::app::get_app_context().await;\n\n    let test_data = MySharedData {\n        message: \"Hello from SharedStore!\".to_string(),\n    };\n    if exists {\n        ctx.shared_store.insert(test_data.clone());\n    }\n\n    let port = get_available_port().await;\n    let handle = infra_cfg::server::start_with_route(ctx, \"/\", get(action), Some(port)).await;\n\n    let res = reqwest::get(get_base_url_port(port))\n        .await\n        .expect(\"Failed to make request\");\n\n    if exists {\n        assert_eq!(res.status(), axum::http::StatusCode::OK);\n\n        let body: MySharedData = res.json().await.expect(\"Failed to parse response body\");\n        assert_eq!(body, test_data);\n    } else {\n        assert_eq!(res.status(), axum::http::StatusCode::INTERNAL_SERVER_ERROR);\n    }\n\n    handle.abort();\n}\n\n#[tokio::test]\nasync fn test_shared_store_without_clone() {\n    async fn action(State(ctx): State<AppContext>) -> Result<Response> {\n        let shared_data_ref = ctx\n            .shared_store\n            .get_ref::<MySharedDataWithoutClone>()\n            .ok_or_else(|| Error::InternalServerError)?;\n        format::text(&shared_data_ref.message)\n    }\n\n    let ctx: AppContext = tests_cfg::app::get_app_context().await;\n\n    let test_data = MySharedDataWithoutClone {\n        message: \"Hello from SharedStore!\".to_string(),\n    };\n    ctx.shared_store.insert(test_data);\n\n    let port = get_available_port().await;\n    let handle = infra_cfg::server::start_with_route(ctx, \"/\", get(action), Some(port)).await;\n\n    let res = reqwest::get(get_base_url_port(port))\n        .await\n        .expect(\"Failed to make request\");\n\n    assert_eq!(res.status(), axum::http::StatusCode::OK);\n\n    let body = res.text().await.expect(\"Failed to parse response body\");\n    assert_eq!(body, \"Hello from SharedStore!\");\n\n    handle.abort();\n}\n"
  },
  {
    "path": "tests/controller/extractor/validate.rs",
    "content": "use loco_rs::{prelude::*, tests_cfg};\nuse serde::{Deserialize, Serialize};\nuse validator::Validate;\n\nuse crate::infra_cfg;\n\n#[derive(Debug, Deserialize, Serialize, Validate)]\npub struct Data {\n    #[validate(length(min = 5, message = \"message_str\"))]\n    pub name: String,\n    #[validate(email)]\n    pub email: String,\n}\n\nasync fn validation_with_response(\n    JsonValidateWithMessage(_params): JsonValidateWithMessage<Data>,\n) -> Result<Response> {\n    format::json(())\n}\n\nasync fn simple_validation(JsonValidate(_params): JsonValidate<Data>) -> Result<Response> {\n    format::json(())\n}\n\n#[tokio::test]\nasync fn can_validation_with_response() {\n    let ctx = tests_cfg::app::get_app_context().await;\n\n    let port = get_available_port().await;\n    let handle =\n        infra_cfg::server::start_with_route(ctx, \"/\", post(validation_with_response), Some(port))\n            .await;\n\n    let client = reqwest::Client::new();\n    let res = client\n        .post(get_base_url_port(port))\n        .json(&serde_json::json!({\"name\": \"test\", \"email\": \"invalid\"}))\n        .send()\n        .await\n        .expect(\"Valid response\");\n\n    assert_eq!(res.status(), 400);\n\n    let res_text = res.text().await.expect(\"response text\");\n    let res_json: serde_json::Value = serde_json::from_str(&res_text).expect(\"Valid JSON response\");\n\n    let expected_json = serde_json::json!(\n        {\n            \"errors\":{\n                \"email\":[{\"code\":\"email\",\"message\":null,\"params\":{\"value\":\"invalid\"}}],\n                \"name\":[{\"code\":\"length\",\"message\":\"message_str\",\"params\":{\"min\":5,\"value\":\"test\"}}]\n        }\n    });\n\n    assert_eq!(res_json, expected_json);\n\n    handle.abort();\n}\n\n#[tokio::test]\nasync fn can_validation_without_response() {\n    let ctx = tests_cfg::app::get_app_context().await;\n\n    let port = get_available_port().await;\n    let handle =\n        infra_cfg::server::start_with_route(ctx, \"/\", post(simple_validation), Some(port)).await;\n\n    let client = reqwest::Client::new();\n    let res = client\n        .post(get_base_url_port(port))\n        .json(&serde_json::json!({\"name\": \"test\", \"email\": \"invalid\"}))\n        .send()\n        .await\n        .expect(\"Valid response\");\n\n    assert_eq!(res.status(), 400);\n\n    let res_text = res.text().await.expect(\"response text\");\n    let res_json: serde_json::Value = serde_json::from_str(&res_text).expect(\"Valid JSON response\");\n\n    let expected_json = serde_json::json!(\n        {\n            \"error\": \"Bad Request\"\n        }\n    );\n\n    assert_eq!(res_json, expected_json);\n\n    handle.abort();\n}\n"
  },
  {
    "path": "tests/controller/from_ref.rs",
    "content": "use axum::extract::FromRef;\nuse loco_rs::{\n    app::{AppContext, SharedStore},\n    cache,\n    prelude::*,\n    tests_cfg,\n};\nuse std::sync::Arc;\n\nuse crate::infra_cfg;\n\n#[cfg(feature = \"with-db\")]\nuse sea_orm::DatabaseConnection;\n\n/// Tests that DatabaseConnection can be extracted from AppContext via FromRef\n#[cfg(feature = \"with-db\")]\n#[tokio::test]\nasync fn can_extract_db_connection_from_app_context() {\n    let ctx = tests_cfg::app::get_app_context().await;\n\n    #[allow(clippy::items_after_statements)]\n    async fn action(State(ctx): State<AppContext>) -> Result<Response> {\n        // Use FromRef to extract DatabaseConnection from AppContext\n        let _db: DatabaseConnection = DatabaseConnection::from_ref(&ctx);\n        format::json(serde_json::json!({\"extracted\": \"db\"}))\n    }\n\n    let port = get_available_port().await;\n    let handle = infra_cfg::server::start_with_route(ctx, \"/\", get(action), Some(port)).await;\n\n    let res = reqwest::get(get_base_url_port(port))\n        .await\n        .expect(\"Valid response\");\n\n    assert_eq!(res.status(), 200);\n\n    let body: serde_json::Value = res.json().await.expect(\"JSON response\");\n    assert_eq!(body[\"extracted\"], \"db\");\n\n    handle.abort();\n}\n\n/// Tests that Arc<Cache> can be extracted from AppContext via FromRef\n#[tokio::test]\nasync fn can_extract_cache_from_app_context() {\n    let ctx = tests_cfg::app::get_app_context().await;\n\n    #[allow(clippy::items_after_statements)]\n    async fn action(State(ctx): State<AppContext>) -> Result<Response> {\n        // Use FromRef to extract Arc<Cache> from AppContext\n        let _cache: Arc<cache::Cache> = Arc::from_ref(&ctx);\n        format::json(serde_json::json!({\"extracted\": \"cache\"}))\n    }\n\n    let port = get_available_port().await;\n    let handle = infra_cfg::server::start_with_route(ctx, \"/\", get(action), Some(port)).await;\n\n    let res = reqwest::get(get_base_url_port(port))\n        .await\n        .expect(\"Valid response\");\n\n    assert_eq!(res.status(), 200);\n\n    let body: serde_json::Value = res.json().await.expect(\"JSON response\");\n    assert_eq!(body[\"extracted\"], \"cache\");\n\n    handle.abort();\n}\n\n/// Tests that Arc<SharedStore> can be extracted from AppContext via FromRef\n#[tokio::test]\nasync fn can_extract_shared_store_from_app_context() {\n    let ctx = tests_cfg::app::get_app_context().await;\n\n    #[allow(clippy::items_after_statements)]\n    async fn action(State(ctx): State<AppContext>) -> Result<Response> {\n        // Use FromRef to extract Arc<SharedStore> from AppContext\n        let _store: Arc<SharedStore> = Arc::from_ref(&ctx);\n        format::json(serde_json::json!({\"extracted\": \"shared_store\"}))\n    }\n\n    let port = get_available_port().await;\n    let handle = infra_cfg::server::start_with_route(ctx, \"/\", get(action), Some(port)).await;\n\n    let res = reqwest::get(get_base_url_port(port))\n        .await\n        .expect(\"Valid response\");\n\n    assert_eq!(res.status(), 200);\n\n    let body: serde_json::Value = res.json().await.expect(\"JSON response\");\n    assert_eq!(body[\"extracted\"], \"shared_store\");\n\n    handle.abort();\n}\n"
  },
  {
    "path": "tests/controller/into_response.rs",
    "content": "use loco_rs::{controller, prelude::*, tests_cfg};\nuse serde::{Deserialize, Serialize};\n\nuse crate::infra_cfg;\n\n#[tokio::test]\nasync fn not_found() {\n    let ctx = tests_cfg::app::get_app_context().await;\n\n    #[allow(clippy::items_after_statements)]\n    async fn action() -> Result<Response> {\n        controller::not_found()\n    }\n\n    let port = get_available_port().await;\n    let handle = infra_cfg::server::start_with_route(ctx, \"/\", get(action), Some(port)).await;\n\n    let res = reqwest::get(get_base_url_port(port))\n        .await\n        .expect(\"Valid response\");\n\n    assert_eq!(res.status(), 404);\n\n    let res_text = res.text().await.expect(\"response text\");\n    let res_json: serde_json::Value = serde_json::from_str(&res_text).expect(\"Valid JSON response\");\n\n    let expected_json = serde_json::json!({\n        \"error\": \"not_found\",\n        \"description\": \"Resource was not found\"\n    });\n\n    assert_eq!(res_json, expected_json);\n\n    handle.abort();\n}\n\n#[tokio::test]\nasync fn internal_server_error() {\n    let ctx = tests_cfg::app::get_app_context().await;\n\n    #[allow(clippy::items_after_statements)]\n    async fn action() -> Result<Response> {\n        Err(Error::InternalServerError)\n    }\n\n    let port = get_available_port().await;\n    let handle = infra_cfg::server::start_with_route(ctx, \"/\", get(action), Some(port)).await;\n\n    let res = reqwest::get(get_base_url_port(port))\n        .await\n        .expect(\"Valid response\");\n\n    assert_eq!(res.status(), 500);\n\n    let res_text = res.text().await.expect(\"response text\");\n    let res_json: serde_json::Value = serde_json::from_str(&res_text).expect(\"Valid JSON response\");\n\n    let expected_json = serde_json::json!({\n        \"error\": \"internal_server_error\",\n        \"description\": \"Internal Server Error\",\n    });\n\n    assert_eq!(res_json, expected_json);\n\n    handle.abort();\n}\n\n#[tokio::test]\nasync fn unauthorized() {\n    let ctx = tests_cfg::app::get_app_context().await;\n\n    #[allow(clippy::items_after_statements)]\n    async fn action() -> Result<Response> {\n        controller::unauthorized(\"user not unauthorized\")\n    }\n\n    let port = get_available_port().await;\n    let handle = infra_cfg::server::start_with_route(ctx, \"/\", get(action), Some(port)).await;\n\n    let res = reqwest::get(get_base_url_port(port))\n        .await\n        .expect(\"Valid response\");\n\n    assert_eq!(res.status(), 401);\n\n    let res_text = res.text().await.expect(\"response text\");\n    let res_json: serde_json::Value = serde_json::from_str(&res_text).expect(\"Valid JSON response\");\n\n    let expected_json = serde_json::json!({\n        \"error\": \"unauthorized\",\n        \"description\": \"You do not have permission to access this resource\"\n    });\n\n    assert_eq!(res_json, expected_json);\n\n    handle.abort();\n}\n\n#[tokio::test]\nasync fn fallback() {\n    let ctx = tests_cfg::app::get_app_context().await;\n\n    #[allow(clippy::items_after_statements)]\n    async fn action() -> Result<Response> {\n        Err(Error::Message(String::new()))\n    }\n\n    let port = get_available_port().await;\n    let handle = infra_cfg::server::start_with_route(ctx, \"/\", get(action), Some(port)).await;\n\n    let res = reqwest::get(get_base_url_port(port))\n        .await\n        .expect(\"Valid response\");\n\n    assert_eq!(res.status(), 500);\n\n    let res_text = res.text().await.expect(\"response text\");\n    let res_json: serde_json::Value = serde_json::from_str(&res_text).expect(\"Valid JSON response\");\n\n    let expected_json = serde_json::json!({\n        \"error\": \"internal_server_error\",\n        \"description\": \"Internal Server Error\",\n    });\n\n    assert_eq!(res_json, expected_json);\n\n    handle.abort();\n}\n\n#[tokio::test]\nasync fn custom_error() {\n    let ctx = tests_cfg::app::get_app_context().await;\n\n    #[allow(clippy::items_after_statements)]\n    async fn action() -> Result<Response> {\n        Err(Error::CustomError(\n            axum::http::StatusCode::PAYLOAD_TOO_LARGE,\n            controller::ErrorDetail {\n                error: Some(\"Payload Too Large\".to_string()),\n                description: Some(\"413 Payload Too Large\".to_string()),\n                errors: None,\n            },\n        ))\n    }\n\n    let port = get_available_port().await;\n    let handle = infra_cfg::server::start_with_route(ctx, \"/\", get(action), Some(port)).await;\n\n    let res = reqwest::get(get_base_url_port(port))\n        .await\n        .expect(\"Valid response\");\n\n    assert_eq!(res.status(), 413);\n\n    let res_text = res.text().await.expect(\"response text\");\n    let res_json: serde_json::Value = serde_json::from_str(&res_text).expect(\"Valid JSON response\");\n\n    let expected_json = serde_json::json!({\n        \"error\": \"Payload Too Large\",\n        \"description\": \"413 Payload Too Large\"\n    });\n\n    assert_eq!(res_json, expected_json);\n\n    handle.abort();\n}\n\n#[tokio::test]\nasync fn json_rejection() {\n    let ctx = tests_cfg::app::get_app_context().await;\n\n    #[allow(clippy::items_after_statements)]\n    #[derive(Debug, Deserialize, Serialize)]\n    pub struct Data {\n        pub email: String,\n    }\n\n    #[allow(clippy::items_after_statements)]\n    async fn action(Json(_params): Json<Data>) -> Result<Response> {\n        format::json(())\n    }\n\n    let port = get_available_port().await;\n    let handle = infra_cfg::server::start_with_route(ctx, \"/\", post(action), Some(port)).await;\n\n    let client = reqwest::Client::new();\n    let res = client\n        .post(get_base_url_port(port))\n        .json(&serde_json::json!({}))\n        .send()\n        .await\n        .expect(\"Valid response\");\n\n    assert_eq!(res.status(), 422);\n\n    let res_text = res.text().await.expect(\"response text\");\n    let res_json: serde_json::Value = serde_json::from_str(&res_text).expect(\"Valid JSON response\");\n\n    let expected_json = serde_json::json!({\n        \"error\": \"Bad Request\",\n    });\n\n    assert_eq!(res_json, expected_json);\n\n    handle.abort();\n}\n"
  },
  {
    "path": "tests/controller/middlewares.rs",
    "content": "use std::{collections::BTreeMap, path::PathBuf};\n\nuse axum::http::StatusCode;\nuse insta::assert_debug_snapshot;\nuse loco_rs::{controller::middleware, prelude::*, tests_cfg};\nuse rstest::rstest;\n\nuse crate::infra_cfg;\n\nmacro_rules! configure_insta {\n    ($($expr:expr),*) => {\n        let mut settings = insta::Settings::clone_current();\n        settings.set_prepend_module_to_snapshot(false);\n        settings.set_snapshot_suffix(\"middlewares\");\n        let _guard = settings.bind_to_scope();\n    };\n}\n\n#[rstest]\n#[case(true)]\n#[case(false)]\n#[tokio::test]\nasync fn panic(#[case] enable: bool) {\n    configure_insta!();\n\n    #[allow(clippy::items_after_statements)]\n    async fn action() -> Result<Response> {\n        panic!(\"panic!\")\n    }\n\n    let mut ctx: AppContext = tests_cfg::app::get_app_context().await;\n    ctx.config.server.middlewares.catch_panic =\n        Some(middleware::catch_panic::CatchPanic { enable });\n\n    let port = get_available_port().await;\n    let handle = infra_cfg::server::start_with_route(ctx, \"/\", get(action), Some(port)).await;\n    let res = reqwest::get(get_base_url_port(port)).await;\n\n    if enable {\n        let res = res.expect(\"valid response\");\n        assert_debug_snapshot!(\n            format!(\"panic\"),\n            (res.status().to_string(), res.text().await)\n        );\n    } else {\n        assert!(res.is_err());\n    }\n\n    handle.abort();\n}\n\n#[rstest]\n#[case(true)]\n#[case(false)]\n#[tokio::test]\nasync fn etag(#[case] enable: bool) {\n    async fn action() -> Result<Response> {\n        format::render().etag(\"loco-etag\")?.text(\"content\")\n    }\n\n    let mut ctx: AppContext = tests_cfg::app::get_app_context().await;\n\n    ctx.config.server.middlewares.etag = Some(middleware::etag::Etag { enable });\n\n    let port = get_available_port().await;\n    let handle = infra_cfg::server::start_with_route(ctx, \"/\", get(action), Some(port)).await;\n\n    let res = reqwest::Client::new()\n        .get(get_base_url_port(port))\n        .header(\"if-none-match\", \"loco-etag\")\n        .send()\n        .await\n        .expect(\"response\");\n\n    if enable {\n        assert_eq!(res.status(), StatusCode::NOT_MODIFIED);\n    } else {\n        assert_eq!(res.status(), StatusCode::OK);\n    }\n\n    handle.abort();\n}\n\n#[rstest]\n#[case(true, \"remote: 51.50.51.50\")]\n#[case(false, \"--\")]\n#[tokio::test]\nasync fn remote_ip(#[case] enable: bool, #[case] expected: &str) {\n    #[allow(clippy::items_after_statements)]\n    async fn action(remote_ip: RemoteIP) -> Result<Response> {\n        format::text(&remote_ip.to_string())\n    }\n\n    let mut ctx: AppContext = tests_cfg::app::get_app_context().await;\n\n    ctx.config.server.middlewares.remote_ip = Some(middleware::remote_ip::RemoteIpMiddleware {\n        enable,\n        trusted_proxies: Some(vec![\"192.1.1.1/8\".to_string()]),\n    });\n\n    let port = get_available_port().await;\n    let handle = infra_cfg::server::start_with_route(ctx, \"/\", get(action), Some(port)).await;\n\n    let res = reqwest::Client::new()\n        .get(get_base_url_port(port))\n        .header(\n            \"x-forwarded-for\",\n            reqwest::header::HeaderValue::from_static(\"51.50.51.50,192.1.1.1\"),\n        )\n        .send()\n        .await\n        .expect(\"response\");\n\n    assert_eq!(res.text().await.expect(\"string\"), expected.to_string());\n\n    handle.abort();\n}\n\n#[rstest]\n#[case(true)]\n#[case(false)]\n#[tokio::test]\nasync fn timeout(#[case] enable: bool) {\n    #[allow(clippy::items_after_statements)]\n    async fn action() -> Result<Response> {\n        tokio::time::sleep(tokio::time::Duration::from_secs(3)).await;\n        format::render().text(\"loco\")\n    }\n\n    let mut ctx: AppContext = tests_cfg::app::get_app_context().await;\n\n    ctx.config.server.middlewares.timeout_request =\n        Some(middleware::timeout::TimeOut { enable, timeout: 2 });\n\n    let port = get_available_port().await;\n    let handle = infra_cfg::server::start_with_route(ctx, \"/\", get(action), Some(port)).await;\n\n    let res = reqwest::get(get_base_url_port(port))\n        .await\n        .expect(\"response\");\n\n    if enable {\n        assert_eq!(res.status(), StatusCode::REQUEST_TIMEOUT);\n    } else {\n        assert_eq!(res.status(), StatusCode::OK);\n    }\n\n    handle.abort();\n}\n\n#[rstest]\n#[case(true, \"default\", None, None, None)]\n#[case(true, \"with_allow_headers\", Some(vec![\"token\".to_string(), \"user\".to_string()]), None, None)]\n#[case(true, \"with_allow_methods\", None, Some(vec![\"post\".to_string(), \"get\".to_string()]), None)]\n#[case(true, \"with_max_age\", None, None, Some(20))]\n#[case(false, \"disabled\", None, None, None)]\n#[tokio::test]\nasync fn cors(\n    #[case] enable: bool,\n    #[case] test_name: &str,\n    #[case] allow_headers: Option<Vec<String>>,\n    #[case] allow_methods: Option<Vec<String>>,\n    #[case] max_age: Option<u64>,\n) {\n    use loco_rs::controller::middleware::cors::Cors;\n\n    configure_insta!();\n\n    let mut ctx: AppContext = tests_cfg::app::get_app_context().await;\n\n    let mut middleware = Cors {\n        enable,\n        ..Default::default()\n    };\n\n    if let Some(allow_headers) = allow_headers {\n        middleware.allow_headers = allow_headers;\n    }\n    if let Some(allow_methods) = allow_methods {\n        middleware.allow_methods = allow_methods;\n    }\n    middleware.max_age = max_age;\n\n    ctx.config.server.middlewares.cors = Some(middleware);\n\n    let port = get_available_port().await;\n    let handle = infra_cfg::server::start_from_ctx(ctx, Some(port)).await;\n\n    let res = reqwest::Client::new()\n        .request(reqwest::Method::OPTIONS, get_base_url_port(port))\n        .send()\n        .await\n        .expect(\"valid response\");\n\n    assert_debug_snapshot!(\n        format!(\"cors_[{test_name}]\"),\n        (\n            format!(\n                \"access-control-allow-origin: {:?}\",\n                res.headers().get(\"access-control-allow-origin\")\n            ),\n            format!(\"vary: {:?}\", res.headers().get(\"vary\")),\n            format!(\n                \"access-control-allow-methods: {:?}\",\n                res.headers().get(\"access-control-allow-methods\")\n            ),\n            format!(\n                \"access-control-allow-headers: {:?}\",\n                res.headers().get(\"access-control-allow-headers\")\n            ),\n            format!(\"allow: {:?}\", res.headers().get(\"allow\")),\n        )\n    );\n\n    handle.abort();\n}\n\n#[rstest]\n#[case(middleware::limit_payload::DefaultBodyLimitKind::Limit(0x1B))]\n#[case(middleware::limit_payload::DefaultBodyLimitKind::Disable)]\n#[tokio::test]\nasync fn limit_payload(#[case] limit: middleware::limit_payload::DefaultBodyLimitKind) {\n    configure_insta!();\n\n    let mut ctx: AppContext = tests_cfg::app::get_app_context().await;\n\n    ctx.config.server.middlewares.limit_payload =\n        Some(middleware::limit_payload::LimitPayload { body_limit: limit });\n\n    let port = get_available_port().await;\n    let handle = infra_cfg::server::start_from_ctx(ctx, Some(port)).await;\n\n    let res = reqwest::Client::new()\n        .request(reqwest::Method::POST, get_base_url_port(port))\n        .body(\"send body\".repeat(100))\n        .send()\n        .await\n        .expect(\"valid response\");\n\n    match limit {\n        middleware::limit_payload::DefaultBodyLimitKind::Disable => {\n            assert_eq!(res.status(), StatusCode::OK);\n        }\n        middleware::limit_payload::DefaultBodyLimitKind::Limit(_) => {\n            assert_eq!(res.status(), StatusCode::PAYLOAD_TOO_LARGE);\n        }\n    }\n\n    handle.abort();\n}\n\n#[cfg(not(feature = \"embedded_assets\"))]\n#[tokio::test]\nasync fn static_assets() {\n    configure_insta!();\n\n    let base_static_assets_path = PathBuf::from(\"assets\").join(\"static\");\n    let static_asset_path = tree_fs::TreeBuilder::default()\n        .drop(true)\n        .add(\n            base_static_assets_path.join(\"404.html\"),\n            \"<h1>404 not found</h1>\",\n        )\n        .add(\n            base_static_assets_path.join(\"static.html\"),\n            \"<h1>static content</h1>\",\n        )\n        .create()\n        .expect(\"create static tree file\");\n\n    let mut ctx: AppContext = tests_cfg::app::get_app_context().await;\n    let base_static_path = static_asset_path.root.join(base_static_assets_path);\n    ctx.config.server.middlewares.static_assets = Some(middleware::static_assets::StaticAssets {\n        enable: true,\n        must_exist: true,\n        folder: middleware::static_assets::FolderConfig {\n            uri: \"/static\".to_string(),\n            path: base_static_path.clone(),\n        },\n        fallback: base_static_path.join(\"404.html\"),\n        precompressed: false,\n        cache_control: None,\n    });\n\n    let port = get_available_port().await;\n    let handle = infra_cfg::server::start_from_ctx(ctx, Some(port)).await;\n\n    let get_static_html = reqwest::get(format!(\"{}static/static.html\", get_base_url_port(port)))\n        .await\n        .expect(\"valid response\");\n\n    assert_eq!(\n        get_static_html.text().await.expect(\"text response\"),\n        \"<h1>static content</h1>\".to_string()\n    );\n\n    let get_fallback = reqwest::get(format!(\"{}static/logo.png\", get_base_url_port(port)))\n        .await\n        .expect(\"valid response\");\n\n    assert_eq!(\n        get_fallback.text().await.expect(\"text response\"),\n        \"<h1>404 not found</h1>\".to_string()\n    );\n\n    handle.abort();\n}\n\n#[rstest]\n#[case(None, None)]\n#[case(Some(\"empty\".to_string()), None)]\n#[case(Some(\"github\".to_string()), Some(BTreeMap::from([(\n        \"Content-Security-Policy\".to_string(),\n        \"default-src 'self' https\".to_string(),\n    )])))]\n#[tokio::test]\nasync fn secure_headers(\n    #[case] preset: Option<String>,\n    #[case] overrides: Option<BTreeMap<String, String>>,\n) {\n    configure_insta!();\n\n    let mut ctx: AppContext = tests_cfg::app::get_app_context().await;\n\n    ctx.config.server.middlewares.secure_headers = Some(\n        loco_rs::controller::middleware::secure_headers::SecureHeader {\n            enable: true,\n            preset: preset.clone().unwrap_or_else(|| \"github\".to_string()),\n            overrides: overrides.clone(),\n        },\n    );\n\n    let port = get_available_port().await;\n    let handle = infra_cfg::server::start_from_ctx(ctx, Some(port)).await;\n\n    let res = reqwest::Client::new()\n        .request(reqwest::Method::POST, get_base_url_port(port))\n        .send()\n        .await\n        .expect(\"response\");\n\n    let policy = res.headers().get(\"content-security-policy\");\n    let overrides_str = overrides.map_or(\"none\".to_string(), |k| {\n        k.keys()\n            .map(std::string::ToString::to_string)\n            .collect::<Vec<_>>()\n            .join(\",\")\n    });\n    assert_debug_snapshot!(\n        format!(\n            \"secure_headers_[{}]_overrides[{}]\",\n            preset.unwrap_or_else(|| \"none\".to_string()),\n            overrides_str\n        ),\n        policy\n    );\n\n    handle.abort();\n}\n\n#[rstest]\n#[case(None, false, None)]\n#[case(Some(StatusCode::BAD_REQUEST), false, None)]\n#[case(None, true, None)]\n#[case(None, false, Some(\"text fallback response\".to_string()))]\n#[tokio::test]\nasync fn fallback(\n    #[case] code: Option<StatusCode>,\n    #[case] file: bool,\n    #[case] not_found: Option<String>,\n) {\n    let mut ctx: AppContext = tests_cfg::app::get_app_context().await;\n\n    let maybe_file = if file {\n        Some(\n            tree_fs::TreeBuilder::default()\n                .drop(true)\n                .add(\n                    PathBuf::from(\"static_content.html\"),\n                    \"<h1>fallback response</h1>\",\n                )\n                .create()\n                .unwrap(),\n        )\n    } else {\n        None\n    };\n\n    let mut fallback_config = middleware::fallback::Fallback {\n        enable: true,\n        file: maybe_file.as_ref().map(|tree_fs| {\n            tree_fs\n                .root\n                .join(\"static_content.html\")\n                .display()\n                .to_string()\n        }),\n        not_found: not_found.clone(),\n        ..Default::default()\n    };\n\n    if let Some(code) = code {\n        fallback_config.code = code;\n    };\n\n    ctx.config.server.middlewares.fallback = Some(fallback_config);\n\n    let port = get_available_port().await;\n    let handle = infra_cfg::server::start_from_ctx(ctx, Some(port)).await;\n\n    let res = reqwest::get(format!(\"{}not-found\", get_base_url_port(port)))\n        .await\n        .expect(\"valid response\");\n\n    if let Some(code) = code {\n        assert_eq!(res.status(), code);\n    } else {\n        assert_eq!(res.status(), StatusCode::OK);\n    }\n\n    let response_text = res.text().await.expect(\"response text\");\n    if maybe_file.is_some() {\n        assert_eq!(response_text, \"<h1>fallback response</h1>\".to_string());\n    }\n\n    if let Some(not_found_text) = not_found {\n        assert_eq!(response_text, not_found_text);\n    }\n\n    handle.abort();\n}\n\n#[rstest]\n#[case(None)]\n#[case(Some(\"custom\".to_string()))]\n#[tokio::test]\nasync fn powered_by_header(#[case] ident: Option<String>) {\n    configure_insta!();\n\n    let mut ctx: AppContext = tests_cfg::app::get_app_context().await;\n\n    ctx.config.server.ident.clone_from(&ident);\n\n    let port = get_available_port().await;\n    let handle = infra_cfg::server::start_from_ctx(ctx, Some(port)).await;\n\n    let res = reqwest::get(get_base_url_port(port))\n        .await\n        .expect(\"valid response\");\n\n    let header_value = res.headers().get(\"x-powered-by\").expect(\"exists header\");\n    if let Some(ident_str) = ident {\n        assert_eq!(header_value.to_str().expect(\"value\"), ident_str);\n    } else {\n        assert_eq!(header_value.to_str().expect(\"value\"), \"loco.rs\");\n    }\n\n    handle.abort();\n}\n"
  },
  {
    "path": "tests/controller/mod.rs",
    "content": "mod extractor;\nmod from_ref;\nmod into_response;\nmod middlewares;\n"
  },
  {
    "path": "tests/controller/snapshots/cors_[default]@middlewares.snap",
    "content": "---\nsource: tests/controller/middlewares.rs\nexpression: \"(format!(\\\"access-control-allow-origin: {:?}\\\",\\n        res.headers().get(\\\"access-control-allow-origin\\\")),\\n    format!(\\\"vary: {:?}\\\", res.headers().get(\\\"vary\\\")),\\n    format!(\\\"access-control-allow-methods: {:?}\\\",\\n        res.headers().get(\\\"access-control-allow-methods\\\")),\\n    format!(\\\"access-control-allow-headers: {:?}\\\",\\n        res.headers().get(\\\"access-control-allow-headers\\\")),\\n    format!(\\\"allow: {:?}\\\", res.headers().get(\\\"allow\\\")))\"\n---\n(\n    \"access-control-allow-origin: Some(\\\"*\\\")\",\n    \"vary: Some(\\\"origin, access-control-request-method, access-control-request-headers\\\")\",\n    \"access-control-allow-methods: Some(\\\"*\\\")\",\n    \"access-control-allow-headers: Some(\\\"*\\\")\",\n    \"allow: Some(\\\"GET,HEAD,POST\\\")\",\n)\n"
  },
  {
    "path": "tests/controller/snapshots/cors_[disabled]@middlewares.snap",
    "content": "---\nsource: tests/controller/middlewares.rs\nexpression: \"(format!(\\\"access-control-allow-origin: {:?}\\\",\\n        res.headers().get(\\\"access-control-allow-origin\\\")),\\n    format!(\\\"vary: {:?}\\\", res.headers().get(\\\"vary\\\")),\\n    format!(\\\"access-control-allow-methods: {:?}\\\",\\n        res.headers().get(\\\"access-control-allow-methods\\\")),\\n    format!(\\\"access-control-allow-headers: {:?}\\\",\\n        res.headers().get(\\\"access-control-allow-headers\\\")),\\n    format!(\\\"allow: {:?}\\\", res.headers().get(\\\"allow\\\")))\"\n---\n(\n    \"access-control-allow-origin: None\",\n    \"vary: None\",\n    \"access-control-allow-methods: None\",\n    \"access-control-allow-headers: None\",\n    \"allow: Some(\\\"GET,HEAD,POST\\\")\",\n)\n"
  },
  {
    "path": "tests/controller/snapshots/cors_[with_allow_headers]@middlewares.snap",
    "content": "---\nsource: tests/controller/middlewares.rs\nexpression: \"(format!(\\\"access-control-allow-origin: {:?}\\\",\\n        res.headers().get(\\\"access-control-allow-origin\\\")),\\n    format!(\\\"vary: {:?}\\\", res.headers().get(\\\"vary\\\")),\\n    format!(\\\"access-control-allow-methods: {:?}\\\",\\n        res.headers().get(\\\"access-control-allow-methods\\\")),\\n    format!(\\\"access-control-allow-headers: {:?}\\\",\\n        res.headers().get(\\\"access-control-allow-headers\\\")),\\n    format!(\\\"allow: {:?}\\\", res.headers().get(\\\"allow\\\")))\"\n---\n(\n    \"access-control-allow-origin: Some(\\\"*\\\")\",\n    \"vary: Some(\\\"origin, access-control-request-method, access-control-request-headers\\\")\",\n    \"access-control-allow-methods: Some(\\\"*\\\")\",\n    \"access-control-allow-headers: Some(\\\"token,user\\\")\",\n    \"allow: Some(\\\"GET,HEAD,POST\\\")\",\n)\n"
  },
  {
    "path": "tests/controller/snapshots/cors_[with_allow_methods]@middlewares.snap",
    "content": "---\nsource: tests/controller/middlewares.rs\nexpression: \"(format!(\\\"access-control-allow-origin: {:?}\\\",\\n        res.headers().get(\\\"access-control-allow-origin\\\")),\\n    format!(\\\"vary: {:?}\\\", res.headers().get(\\\"vary\\\")),\\n    format!(\\\"access-control-allow-methods: {:?}\\\",\\n        res.headers().get(\\\"access-control-allow-methods\\\")),\\n    format!(\\\"access-control-allow-headers: {:?}\\\",\\n        res.headers().get(\\\"access-control-allow-headers\\\")),\\n    format!(\\\"allow: {:?}\\\", res.headers().get(\\\"allow\\\")))\"\n---\n(\n    \"access-control-allow-origin: Some(\\\"*\\\")\",\n    \"vary: Some(\\\"origin, access-control-request-method, access-control-request-headers\\\")\",\n    \"access-control-allow-methods: Some(\\\"post,get\\\")\",\n    \"access-control-allow-headers: Some(\\\"*\\\")\",\n    \"allow: Some(\\\"GET,HEAD,POST\\\")\",\n)\n"
  },
  {
    "path": "tests/controller/snapshots/cors_[with_max_age]@middlewares.snap",
    "content": "---\nsource: tests/controller/middlewares.rs\nexpression: \"(format!(\\\"access-control-allow-origin: {:?}\\\",\\n        res.headers().get(\\\"access-control-allow-origin\\\")),\\n    format!(\\\"vary: {:?}\\\", res.headers().get(\\\"vary\\\")),\\n    format!(\\\"access-control-allow-methods: {:?}\\\",\\n        res.headers().get(\\\"access-control-allow-methods\\\")),\\n    format!(\\\"access-control-allow-headers: {:?}\\\",\\n        res.headers().get(\\\"access-control-allow-headers\\\")),\\n    format!(\\\"allow: {:?}\\\", res.headers().get(\\\"allow\\\")))\"\n---\n(\n    \"access-control-allow-origin: Some(\\\"*\\\")\",\n    \"vary: Some(\\\"origin, access-control-request-method, access-control-request-headers\\\")\",\n    \"access-control-allow-methods: Some(\\\"*\\\")\",\n    \"access-control-allow-headers: Some(\\\"*\\\")\",\n    \"allow: Some(\\\"GET,HEAD,POST\\\")\",\n)\n"
  },
  {
    "path": "tests/controller/snapshots/panic@middlewares.snap",
    "content": "---\nsource: tests/controller/middlewares.rs\nexpression: \"(res.status().to_string(), res.text().await)\"\n---\n(\n    \"500 Internal Server Error\",\n    Ok(\n        \"{\\\"error\\\":\\\"internal_server_error\\\",\\\"description\\\":\\\"Internal Server Error\\\"}\",\n    ),\n)\n"
  },
  {
    "path": "tests/controller/snapshots/secure_headers_[empty]_overrides[none]@middlewares.snap",
    "content": "---\nsource: tests/controller/middlewares.rs\nexpression: policy\n---\nNone\n"
  },
  {
    "path": "tests/controller/snapshots/secure_headers_[github]_overrides[Content-Security-Policy]@middlewares.snap",
    "content": "---\nsource: tests/controller/middlewares.rs\nexpression: policy\n---\nSome(\n    \"default-src 'self' https\",\n)\n"
  },
  {
    "path": "tests/controller/snapshots/secure_headers_[none]_overrides[none]@middlewares.snap",
    "content": "---\nsource: tests/controller/middlewares.rs\nexpression: policy\n---\nSome(\n    \"default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src https:; style-src 'self' https: 'unsafe-inline'\",\n)\n"
  },
  {
    "path": "tests/fixtures/email_template/test/html.t",
    "content": ";<html>\n\n<body>\n  This is a test content\n  <a href=\"http://localhost:/verify/{{ verifyToken }}\">\n    Some test\n  </a>\n</body>\n\n</html>\n"
  },
  {
    "path": "tests/fixtures/email_template/test/subject.t",
    "content": "Test {{ name }}\n"
  },
  {
    "path": "tests/fixtures/email_template/test/text.t",
    "content": "Welcome to test: {{ name }},\n\n  http://localhost/verify/<%= verifyToken %>\n"
  },
  {
    "path": "tests/fixtures/queue/jobs.yaml",
    "content": "- id: \"01JDM0X8EVAM823JZBGKYNBA99\"\n  name: \"UserAccountActivation\"\n  task_data:\n    user_id: 133\n    email: \"user11@example.com\"\n    activation_token: \"abcdef123456\"\n  status: \"queued\"\n  run_at: \"2024-11-28T08:19:08Z\"\n  created_at: \"2024-11-28T08:03:25Z\"\n  updated_at: \"2024-11-28T08:03:25Z\"\n\n- id: \"01JDM0X8EVAM823JZBGKYNBA98\"\n  name: \"PasswordChangeNotification\"\n  task_data:\n    user_id: 134\n    email: \"user12@example.com\"\n    change_time: \"2024-11-27T12:30:00Z\"\n  status: \"completed\"\n  run_at: \"2024-11-28T08:04:25Z\"\n  created_at: \"2024-11-28T08:03:25Z\"\n  updated_at: \"2024-11-28T08:03:25Z\"\n\n- id: \"01JDM0X8EVAM823JZBGKYNBA97\"\n  name: \"SendInvoice\"\n  task_data:\n    user_id: 135\n    email: \"user13@example.com\"\n    invoice_id: \"INV-2024-01\"\n  status: \"processing\"\n  run_at: \"2024-11-28T08:04:25Z\"\n  created_at: \"2024-11-28T08:03:25Z\"\n  updated_at: \"2024-11-28T08:03:25Z\"\n\n- id: \"01JDM0X8EVAM823JZBGKYNBA96\"\n  name: \"UserDeactivation\"\n  task_data:\n    user_id: 136\n    email: \"user14@example.com\"\n    deactivation_reason: \"user requested\"\n  status: \"failed\"\n  run_at: \"2024-11-28T08:04:25Z\"\n  created_at: \"2024-11-28T08:03:25Z\"\n  updated_at: \"2024-11-28T08:03:25Z\"\n\n- id: \"01JDM0X8EVAM823JZBGKYNBA95\"\n  name: \"SubscriptionReminder\"\n  task_data:\n    user_id: 137\n    email: \"user15@example.com\"\n    renewal_date: \"2024-12-01\"\n  status: \"queued\"\n  run_at: \"2024-11-28T08:04:25Z\"\n  created_at: \"2024-11-28T08:03:25Z\"\n  updated_at: \"2024-11-28T08:03:25Z\"\n\n- id: \"01JDM0X8EVAM823JZBGKYNBA94\"\n  name: \"DataBackup\"\n  task_data:\n    backup_id: \"backup-12345\"\n    user_id: 138\n    email: \"user16@example.com\"\n  status: \"cancelled\"\n  run_at: \"2024-11-28T08:04:25Z\"\n  created_at: \"2024-11-28T08:03:25Z\"\n  updated_at: \"2024-11-28T08:03:25Z\"\n\n- id: \"01JDM0X8EVAM823JZBGKYNBA93\"\n  name: \"SecurityAlert\"\n  task_data:\n    user_id: 139\n    email: \"user17@example.com\"\n    alert_type: \"login attempt from new device\"\n  status: \"queued\"\n  run_at: \"2024-11-28T08:04:25Z\"\n  created_at: \"2024-11-28T08:03:25Z\"\n  updated_at: \"2024-11-28T08:03:25Z\"\n\n- id: \"01JDM0X8EVAM823JZBGKYNBA92\"\n  name: \"WeeklyReportEmail\"\n  task_data:\n    user_id: 140\n    email: \"user18@example.com\"\n    report_period: \"2024-11-20 to 2024-11-27\"\n  status: \"processing\"\n  run_at: \"2024-11-28T08:04:25Z\"\n  created_at: \"2024-11-28T08:03:25Z\"\n  updated_at: \"2024-11-28T08:03:25Z\"\n\n- id: \"01JDM0X8EVAM823JZBGKYNBA91\"\n  name: \"AccountDeletion\"\n  task_data:\n    user_id: 142\n    email: \"user20@example.com\"\n    deletion_request_time: \"2024-11-27T14:00:00Z\"\n  status: \"queued\"\n  run_at: \"2024-11-28T08:04:25Z\"\n  created_at: \"2024-11-28T08:03:25Z\"\n  updated_at: \"2024-11-28T08:03:25Z\"\n\n- id: \"01JDM0X8EVAM823JZBGKYNBA90\"\n  name: \"UserAccountActivation\"\n  task_data:\n    user_id: 143\n    email: \"user21@example.com\"\n    activation_token: \"xyz987654\"\n  status: \"completed\"\n  run_at: \"2024-11-28T08:04:25Z\"\n  created_at: \"2024-11-28T08:03:25Z\"\n  updated_at: \"2024-11-28T08:03:25Z\"\n\n- id: \"01JDM0X8EVAM823JZBGKYNBA89\"\n  name: \"PasswordChangeNotification\"\n  task_data:\n    user_id: 144\n    email: \"user22@example.com\"\n    change_time: \"2024-11-27T15:00:00Z\"\n  status: \"completed\"\n  run_at: \"2024-11-28T08:04:25Z\"\n  created_at: \"2024-11-28T08:03:25Z\"\n  updated_at: \"2024-11-28T08:03:25Z\"\n\n- id: \"01JDM0X8EVAM823JZBGKYNBA88\"\n  name: \"SendInvoice\"\n  task_data:\n    user_id: 145\n    email: \"user23@example.com\"\n    invoice_id: \"INV-2024-02\"\n  status: \"processing\"\n  run_at: \"2024-11-28T08:04:25Z\"\n  created_at: \"2024-11-28T08:03:25Z\"\n  updated_at: \"2024-11-28T08:03:25Z\"\n\n- id: \"01JDM0X8EVAM823JZBGKYNBA87\"\n  name: \"UserDeactivation\"\n  task_data:\n    user_id: 146\n    email: \"user24@example.com\"\n    deactivation_reason: \"account inactive\"\n  status: \"failed\"\n  run_at: \"2024-11-28T08:04:25Z\"\n  created_at: \"2024-11-28T08:03:25Z\"\n  updated_at: \"2024-11-28T08:03:25Z\"\n\n- id: \"01JDM0X8EVAM823JZBGKYNBA86\"\n  name: \"SubscriptionReminder\"\n  task_data:\n    user_id: 147\n    email: \"user25@example.com\"\n    renewal_date: \"2024-12-05\"\n  status: \"queued\"\n  run_at: \"2024-11-28T08:04:25Z\"\n  created_at: \"2024-11-28T08:03:25Z\"\n  updated_at: \"2024-11-28T08:03:25Z\""
  },
  {
    "path": "tests/infra_cfg/mod.rs",
    "content": "pub mod server;\n"
  },
  {
    "path": "tests/infra_cfg/server.rs",
    "content": "//! # Server Infrastructure Utilities for Loco Framework Testing\n//!\n//! This module provides utility functions to test a server using the Loco\n//! framework. It includes helper functions to start the server from different\n//! configurations, such as from boot parameters, application context, or a\n//! custom route. These utilities are designed for test environments and use\n//! hardcoded ports and bindings.\n\nuse loco_rs::{boot, controller::AppRoutes, prelude::*, tests_cfg::db::AppHook};\n\n/// A simple asynchronous handler for GET requests.\nasync fn get_action() -> Result<Response> {\n    format::render().text(\"text response\")\n}\n\n/// A simple asynchronous handler for POST requests.\nasync fn post_action(_body: axum::body::Bytes) -> Result<Response> {\n    format::render().text(\"text response\")\n}\n\n/// Starts the server using the provided Loco [`boot::BootResult`] result.\n/// It uses hardcoded server parameters such as the port and binding address.\n///\n/// This function spawns a server task that runs asynchronously and sleeps for 2\n/// seconds to ensure the server is fully initialized before handling requests.\npub async fn start_from_boot(\n    boot_result: boot::BootResult,\n    port: Option<i32>,\n) -> tokio::task::JoinHandle<()> {\n    let handle = tokio::spawn(async move {\n        boot::start::<AppHook>(\n            boot_result,\n            boot::ServeParams {\n                port: port.unwrap_or(TEST_PORT_SERVER),\n                binding: TEST_BINDING_SERVER.to_string(),\n            },\n            false,\n        )\n        .await\n        .expect(\"start the server\");\n    });\n\n    tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;\n    handle\n}\n\n/// Starts the server with a basic route (GET and POST) at the root (`/`), using\n/// the given application context.\npub async fn start_from_ctx(ctx: AppContext, port: Option<i32>) -> tokio::task::JoinHandle<()> {\n    let app_router = AppRoutes::empty()\n        .add_route(\n            Routes::new()\n                .add(\"/\", get(get_action))\n                .add(\"/\", post(post_action)),\n        )\n        .to_router::<AppHook>(ctx.clone(), axum::Router::new())\n        .expect(\"to router\");\n\n    let boot = boot::BootResult {\n        app_context: ctx,\n        router: Some(app_router),\n        worker: None,\n        run_scheduler: false,\n    };\n\n    start_from_boot(boot, port).await\n}\n\n/// Starts the server with a custom route specified by the URI and the HTTP\n/// method handler.\npub async fn start_with_route(\n    ctx: AppContext,\n    uri: &str,\n    method: axum::routing::MethodRouter<AppContext>,\n    port: Option<i32>,\n) -> tokio::task::JoinHandle<()> {\n    let app_router = AppRoutes::empty()\n        .add_route(Routes::new().add(uri, method))\n        .to_router::<AppHook>(ctx.clone(), axum::Router::new())\n        .expect(\"to router\");\n\n    let boot = boot::BootResult {\n        app_context: ctx,\n        router: Some(app_router),\n        worker: None,\n        run_scheduler: false,\n    };\n    start_from_boot(boot, port).await\n}\n"
  },
  {
    "path": "tests/mod.rs",
    "content": "mod build_scripts;\nmod controller;\nmod infra_cfg;\n"
  },
  {
    "path": "xtask/.rustfmt.toml",
    "content": "max_width = 100\nuse_small_heuristics = \"Default\"\n"
  },
  {
    "path": "xtask/Cargo.toml",
    "content": "[package]\nname = \"xtask\"\nversion = \"0.2.0\"\nedition = \"2021\"\n\n[lib]\nname = \"xtask\"\npath = \"src/lib.rs\"\n\n[[bin]]\nname = \"xtask\"\npath = \"src/bin/main.rs\"\nrequired-features = []\n\n[dependencies]\nclap = { version = \"4.4.7\", features = [\"derive\"] }\neyre = \"0.6\"\nduct = { workspace = true }\ncargo_metadata = \"0.18.1\"\nrequestty = \"0.5.0\"\nregex = { version = \"1.10.2\" }\nthiserror = \"1\"\ntabled = \"0.14.0\"\ncolored = { version = \"3.0\" }\n\n[dev-dependencies]\n"
  },
  {
    "path": "xtask/README.md",
    "content": "# Loco xtask\n\nThe Loco xtask serves as a loco development helper, streamlining various tasks on the library, such as running all tests with a single command and preparing for a new release and maybe more.\n\n## Bump version\n\nTo release a new Loco version, execute the following command:\n\n```rust\ncargo run bump-version VERSION\n```\n\nThe `bump-version` command performs the following steps:\n\n- Updates the Loco library in [cargo.toml](../Cargo.toml)\n- Replaces all starters with ../../loco-rs to enable CI testing for the targeted release version\n  - If the CI process fails, the operation is halted\n- Locks all starters to the specified Loco version\n\n### Release Steps\n\n- Create new branch `git checkout -b bump-version-[VERSION]`\n- run the following script for update all relevant resources\n  ```sh\n  cd xtask\n  cargo run bump-version VERSION\n  ```\n- push the branch and wait for CI will pass\n- publish the new crate\n- merge to to main\n"
  },
  {
    "path": "xtask/src/bin/main.rs",
    "content": "use std::env;\n\nuse cargo_metadata::{semver::Version, MetadataCommand, Package};\nuse clap::{\n    ArgAction::{SetFalse, SetTrue},\n    Parser, Subcommand,\n};\nuse xtask::versions;\n\n#[derive(Parser)]\n#[command(author, version, about, long_about = None)]\n#[command(propagate_version = true)]\nstruct Cli {\n    #[command(subcommand)]\n    command: Commands,\n}\n\n#[derive(Subcommand)]\nenum Commands {\n    /// Run test on all Loco resources\n    Test {\n        /// Test only Loco as a library\n        #[arg(short, long, action = SetTrue)]\n        quick: bool,\n    },\n    /// Bump loco version in all dependencies places\n    DeprecatedBumpVersion {\n        #[arg(name = \"VERSION\")]\n        new_version: Version,\n        #[arg(short, long, action = SetFalse)]\n        exclude_starters: bool,\n    },\n    Bump {\n        #[arg(name = \"VERSION\")]\n        new_version: Version,\n    },\n}\n\nfn main() -> eyre::Result<()> {\n    let cli = Cli::parse();\n    let project_dir = env::current_dir()?;\n    println!(\"running in: {project_dir:?}\");\n\n    let res = match cli.command {\n        Commands::Test { quick } => {\n            let res = if quick {\n                vec![xtask::ci::run(project_dir.as_path()).expect(\"test should have run\")]\n            } else {\n                xtask::ci::all_resources(project_dir.as_path())?\n            };\n            println!(\"{}\", xtask::out::print_ci_results(&res));\n            xtask::CmdExit::ok()\n        }\n        Commands::DeprecatedBumpVersion {\n            new_version,\n            exclude_starters,\n        } => {\n            let meta = MetadataCommand::new()\n                .manifest_path(\"./Cargo.toml\")\n                .current_dir(&project_dir)\n                .exec()\n                .unwrap();\n            let root: &Package = meta.root_package().unwrap();\n            if xtask::prompt::confirmation(&format!(\n                \"upgrading loco version from {} to {}\",\n                root.version, new_version,\n            ))? {\n                xtask::bump_version::BumpVersion {\n                    base_dir: project_dir,\n                    version: new_version,\n                    bump_starters: exclude_starters,\n                }\n                .run()?;\n            }\n            xtask::CmdExit::ok()\n        }\n        Commands::Bump { new_version } => {\n            let meta = MetadataCommand::new()\n                .manifest_path(\"./Cargo.toml\")\n                .current_dir(&project_dir)\n                .exec()\n                .unwrap();\n            let root: &Package = meta.root_package().unwrap();\n            if xtask::prompt::confirmation(&format!(\n                \"upgrading loco version from {} to {}\",\n                root.version, new_version,\n            ))? {\n                versions::bump_version(&new_version.to_string())?;\n            }\n            xtask::CmdExit::ok()\n        }\n    };\n\n    res.exit();\n    Ok(())\n}\n"
  },
  {
    "path": "xtask/src/bump_version.rs",
    "content": "use std::{\n    fs,\n    io::{Read, Write},\n    path::{Path, PathBuf},\n    sync::OnceLock,\n};\n\nuse cargo_metadata::semver::Version;\nuse colored::Colorize;\nuse regex::Regex;\n\nuse crate::{\n    ci,\n    errors::{Error, Result},\n    out, utils,\n};\n\nstatic REPLACE_LOCO_LIB_VERSION_: OnceLock<Regex> = OnceLock::new();\nstatic REPLACE_LOCO_PACKAGE_VERSION: OnceLock<Regex> = OnceLock::new();\n\nfn get_replace_loco_lib_version() -> &'static Regex {\n    REPLACE_LOCO_LIB_VERSION_.get_or_init(|| {\n        Regex::new(\n            r#\"(?P<name>name\\s*=\\s*\".+\\s+version\\s*=\\s*\")(?P<version>[0-9]+\\.[0-9]+\\.[0-9]+)\"#,\n        )\n        .unwrap()\n    })\n}\n\nfn get_replace_loco_package_version() -> &'static Regex {\n    REPLACE_LOCO_PACKAGE_VERSION\n        .get_or_init(|| Regex::new(r#\"loco-rs = \\{ (version|path) = \"[^\"]+\"\"#).unwrap())\n}\npub struct BumpVersion {\n    pub base_dir: PathBuf,\n    pub version: Version,\n    pub bump_starters: bool,\n}\n\nimpl BumpVersion {\n    /// Bump all necessary loco resources with the given version.\n    ///\n    /// # Errors\n    /// Returns an error when it could not update one of the resources.\n    pub fn run(&self) -> Result<()> {\n        self.bump_loco_framework(\".\")?;\n        self.bump_loco_framework(\"loco-gen\")?;\n        self.bump_subcrates_version(&[\"loco-gen\"])?;\n\n        // change starters from fixed (v0.1.x) to local (\"../../\") in order\n        // to test all starters against what is going to be released\n        // when finished successfully, you're allowed to bump all starters to the new\n        // version\n        if self.bump_starters {\n            self.modify_starters_loco_version(\"loco-rs = { path = \\\"../../\\\"\")?;\n\n            println!(\"Testing starters CI\");\n\n            let starter_projects: Vec<ci::RunResults> =\n                ci::run_all_in_folder(&self.base_dir.join(utils::FOLDER_STARTERS))?;\n\n            println!(\"Starters CI results:\");\n            println!(\"{}\", out::print_ci_results(&starter_projects));\n            for starter in &starter_projects {\n                if !starter.is_valid() {\n                    return Err(Error::Message(format!(\n                        \"starter {} ins not passing the CI\",\n                        starter.path.display()\n                    )));\n                }\n            }\n\n            self.modify_starters_loco_version(&format!(\n                \"loco-rs = {{ version = \\\"{}\\\"\",\n                self.version\n            ))?;\n            println!(\"{}\", \"Bump loco starters finished successfully\".green());\n        }\n\n        Ok(())\n    }\n\n    /// Bump the version of the loco library in the root package's Cargo.toml\n    /// file.\n    ///\n    /// # Errors\n    /// Returns an error when it could not parse the loco Cargo.toml file or has\n    /// an error updating the file.\n    fn bump_loco_framework(&self, path: &str) -> Result<()> {\n        println!(\"bumping to `{}` on `{path}`\", self.version);\n\n        let mut content = String::new();\n\n        let cargo_toml_file = self.base_dir.join(path).join(\"Cargo.toml\");\n        fs::File::open(&cargo_toml_file)?.read_to_string(&mut content)?;\n\n        if !get_replace_loco_lib_version().is_match(&content) {\n            return Err(Error::BumpVersion {\n                path: cargo_toml_file,\n                package: \"root_package\".to_string(),\n            });\n        }\n\n        let content = get_replace_loco_lib_version()\n            .replace(&content, |captures: &regex::Captures<'_>| {\n                format!(\"{}{}\", &captures[\"name\"], self.version)\n            });\n\n        let mut modified_file = fs::File::create(cargo_toml_file)?;\n        modified_file.write_all(content.as_bytes())?;\n\n        Ok(())\n    }\n\n    fn bump_subcrates_version(&self, crates: &[&str]) -> Result<()> {\n        let mut content = String::new();\n\n        let cargo_toml_file = self.base_dir.join(\"Cargo.toml\");\n        fs::File::open(&cargo_toml_file)?.read_to_string(&mut content)?;\n\n        println!(\"in root package:\");\n        for subcrate in crates {\n            println!(\"bumping subcrate `{}` to `{}`\", subcrate, self.version);\n            let re = Regex::new(&format!(\n                r#\"{subcrate}\\s*=\\s*\\{{\\s*version\\s*=\\s*\"[0-9]+\\.[0-9]+\\.[0-9]+\",\\s*path\\s*=\\s*\"[^\"]+\"\\s*\\}}\"#,\n            ))\n            .unwrap();\n\n            if !re.is_match(&content) {\n                return Err(Error::BumpVersion {\n                    path: cargo_toml_file.clone(),\n                    package: subcrate.to_string(),\n                });\n            }\n\n            // Replace the full version line with the new version, keeping the structure\n            // intact\n            content = re\n                .replace(\n                    &content,\n                    format!(\n                        r#\"{subcrate} = {{ version = \"{}\", path = \"./{subcrate}\" }}\"#,\n                        self.version\n                    ),\n                )\n                .to_string();\n        }\n\n        let mut modified_file = fs::File::create(cargo_toml_file)?;\n        modified_file.write_all(content.as_bytes())?;\n        Ok(())\n    }\n\n    /// Update the dependencies of loco-rs in all starter projects to the given\n    /// version.\n    ///\n    /// # Errors\n    /// Returns an error when it could not parse a loco Cargo.toml file or has\n    /// an error updating the file.\n    pub fn modify_starters_loco_version(&self, replace_with: &str) -> Result<()> {\n        let starter_projects =\n            utils::get_cargo_folders(&self.base_dir.join(utils::FOLDER_STARTERS))?;\n\n        for starter_project in starter_projects {\n            Self::replace_loco_rs_version(&starter_project, replace_with)?;\n        }\n\n        Ok(())\n    }\n\n    fn replace_loco_rs_version(path: &Path, replace_with: &str) -> Result<()> {\n        let mut content = String::new();\n        let cargo_toml_file = path.join(\"Cargo.toml\");\n        fs::File::open(&cargo_toml_file)?.read_to_string(&mut content)?;\n\n        if !get_replace_loco_package_version().is_match(&content) {\n            return Err(Error::BumpVersion {\n                path: cargo_toml_file,\n                package: \"loco-rs\".to_string(),\n            });\n        }\n        content = get_replace_loco_package_version()\n            .replace_all(&content, |_captures: &regex::Captures<'_>| {\n                replace_with.to_string()\n            })\n            .to_string();\n\n        let mut modified_file = fs::File::create(cargo_toml_file)?;\n        modified_file.write_all(content.as_bytes())?;\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "xtask/src/ci.rs",
    "content": "use std::{\n    path::{Path, PathBuf},\n    process::Output,\n};\n\nuse duct::cmd;\n\nuse crate::{errors::Result, utils};\n\nconst FMT_TEST: [&str; 3] = [\"test\", \"--all-features\", \"--all\"];\nconst FMT_ARGS: [&str; 2] = [\"fmt\", \"--all\"];\nconst FMT_CLIPPY: [&str; 8] = [\n    \"clippy\",\n    \"--\",\n    \"-W\",\n    \"clippy::pedantic\",\n    \"-W\",\n    \"rust-2021-compatibility\",\n    \"-W\",\n    \"rust-2018-idioms\",\n];\n\n#[derive(Default, Debug)]\npub struct RunResults {\n    pub path: PathBuf,\n    pub fmt: bool,\n    pub clippy: bool,\n    pub test: bool,\n}\n\nimpl RunResults {\n    #[must_use]\n    pub fn is_valid(&self) -> bool {\n        self.fmt && self.clippy && self.test\n    }\n}\n\n/// Run CI on all Loco resources (lib, cli, starters, examples, etc.).\n///\n/// # Errors\n/// when could not run ci on the given resource\npub fn all_resources(base_dir: &Path) -> Result<Vec<RunResults>> {\n    let mut result = vec![];\n    result.push(run(base_dir).expect(\"loco lib mast be tested\"));\n    result.extend(run_all_in_folder(&base_dir.join(\"examples\"))?);\n    result.extend(run_all_in_folder(&base_dir.join(\"loco-new\"))?);\n\n    Ok(result)\n}\n\n/// Run CI on inner folders.\n///\n/// For example, run CI on all examples/starters folders dynamically by\n/// selecting the first root folder and running CI one level down.\n///\n/// # Errors\n/// when could not get cargo folders\npub fn run_all_in_folder(root_folder: &Path) -> Result<Vec<RunResults>> {\n    let cargo_projects = utils::get_cargo_folders(root_folder)?;\n    let mut results = vec![];\n\n    for project in cargo_projects {\n        if let Some(res) = run(&project) {\n            results.push(res);\n        }\n    }\n    Ok(results)\n}\n\n/// Run the entire CI flow on the given folder path.\n///\n/// Returns `None` if it is not a Rust folder.\n#[must_use]\npub fn run(dir: &Path) -> Option<RunResults> {\n    if dir.join(\"Cargo.toml\").exists() {\n        Some(RunResults {\n            path: dir.to_path_buf(),\n            fmt: cargo_fmt(dir).is_ok(),\n            clippy: cargo_clippy(dir).is_ok(),\n            test: cargo_test(dir, false).is_ok(),\n        })\n    } else {\n        None\n    }\n}\n\n/// Run cargo test on the given directory.\npub fn cargo_test(dir: &Path, serial: bool) -> Result<Output> {\n    let mut params = FMT_TEST.to_vec();\n    if serial {\n        params.push(\"--\");\n        params.push(\"--test-threads\");\n        params.push(\"1\");\n    }\n    println!(\n        \"Running `cargo {}` in folder {}\",\n        params.join(\" \"),\n        dir.display()\n    );\n    Ok(cmd(\"cargo\", params.as_slice()).dir(dir).run()?)\n}\n\n/// Run cargo fmt on the given directory.\npub fn cargo_fmt(dir: &Path) -> Result<Output> {\n    println!(\n        \"Running `cargo {}` in folder {}\",\n        FMT_ARGS.join(\" \"),\n        dir.display()\n    );\n    Ok(cmd(\"cargo\", FMT_ARGS.as_slice()).dir(dir).run()?)\n}\n\n/// Run cargo clippy on the given directory.\npub fn cargo_clippy(dir: &Path) -> Result<Output> {\n    println!(\n        \"Running `cargo {}` in folder {}\",\n        FMT_CLIPPY.join(\" \"),\n        dir.display()\n    );\n    Ok(cmd(\"cargo\", FMT_CLIPPY.as_slice()).dir(dir).run()?)\n}\n"
  },
  {
    "path": "xtask/src/errors.rs",
    "content": "use std::path::PathBuf;\n#[derive(thiserror::Error, Debug)]\npub enum Error {\n    #[error(\"{0}\")]\n    Message(String),\n\n    #[error(\n        \"could not bump package {} version. not found in path {:?}\",\n        package,\n        path\n    )]\n    BumpVersion { path: PathBuf, package: String },\n\n    #[error(transparent)]\n    IO(#[from] std::io::Error),\n}\n\npub type Result<T> = std::result::Result<T, Error>;\n"
  },
  {
    "path": "xtask/src/lib.rs",
    "content": "use std::process::exit;\npub mod bump_version;\npub mod ci;\npub mod errors;\npub mod out;\npub mod prompt;\npub mod utils;\npub mod versions;\n\n#[derive(Debug)]\npub struct CmdExit {\n    pub code: i32,\n    pub message: Option<String>,\n}\n\nimpl CmdExit {\n    #[must_use]\n    pub fn error_with_message(message: &str) -> Self {\n        Self {\n            code: 1,\n            message: Some(format!(\"🙀 {message}\")),\n        }\n    }\n\n    #[must_use]\n    pub fn ok_with_message(message: &str) -> Self {\n        Self {\n            code: 0,\n            message: Some(message.to_string()),\n        }\n    }\n\n    #[must_use]\n    pub const fn ok() -> Self {\n        Self {\n            code: 0,\n            message: None,\n        }\n    }\n\n    pub fn exit(&self) {\n        if let Some(message) = &self.message {\n            eprintln!(\"{message}\");\n        };\n\n        exit(self.code);\n    }\n}\n"
  },
  {
    "path": "xtask/src/out.rs",
    "content": "use tabled::settings::Style;\n\nuse crate::ci::RunResults;\n\npub fn print_ci_results(result: &Vec<RunResults>) -> String {\n    let mut builder = tabled::builder::Builder::default();\n\n    builder.push_record(vec![\"path\", \"fmt\", \"clippy\", \"test\"]);\n\n    for ci in result {\n        builder.push_record(vec![\n            format!(\"{}\", ci.path.display()),\n            ci.fmt.to_string(),\n            ci.clippy.to_string(),\n            ci.test.to_string(),\n        ]);\n    }\n\n    builder.build().with(Style::modern()).to_string()\n}\n"
  },
  {
    "path": "xtask/src/prompt.rs",
    "content": "/// Prompt confirmation message\n///\n/// # Errors\n/// return an error when could prompt the message or could not parse the input\npub fn confirmation(message: &str) -> eyre::Result<bool> {\n    let question = requestty::Question::confirm(\"confirm\")\n        .message(message)\n        .build();\n\n    let res = requestty::prompt_one(question)?;\n    let answer = res\n        .as_bool()\n        .ok_or_else(|| eyre::eyre!(\"app selection name is empty\"))?;\n\n    Ok(answer)\n}\n"
  },
  {
    "path": "xtask/src/utils.rs",
    "content": "use std::{\n    fs,\n    path::{Path, PathBuf},\n};\n\npub const FOLDER_EXAMPLES: &str = \"examples\";\npub const FOLDER_STARTERS: &str = \"starters\";\npub const FOLDER_LOCO_CLI: &str = \"loco-cli\";\n\n/// return a lost of cargo project in the given path\n///\n/// # Errors\n/// when could not read the given dir path\npub fn get_cargo_folders(path: &Path) -> std::io::Result<Vec<PathBuf>> {\n    let paths = fs::read_dir(path)?;\n    Ok(paths\n        .filter_map(std::result::Result::ok)\n        .filter_map(|dir| {\n            if dir.path().join(\"Cargo.toml\").exists() {\n                Some(dir.path())\n            } else {\n                None\n            }\n        })\n        .collect())\n}\n"
  },
  {
    "path": "xtask/src/versions.rs",
    "content": "use std::{\n    env::{self, current_dir},\n    path::Path,\n};\n\nuse duct::cmd;\nuse regex::Regex;\n\nuse crate::{\n    ci::{cargo_clippy, cargo_fmt},\n    errors::Result,\n};\n\nfn bump_version_in_file(\n    file_path: &str,\n    version_regex: &str,\n    replacement_version: &str,\n    once: bool,\n) {\n    let path = Path::new(file_path);\n\n    // Read the content of the file\n    if path.exists() {\n        println!(\"bumping in {file_path}\");\n        let file_content = std::fs::read_to_string(file_path).expect(\"read file\");\n\n        // Apply regex replacement\n        let re = Regex::new(version_regex).expect(\"Invalid regex\");\n        if !re.is_match(&file_content) {\n            println!(\"cannot match on {file_path}\");\n            return;\n        }\n        let new_content = if once {\n            re.replace(&file_content, replacement_version)\n        } else {\n            re.replace_all(&file_content, replacement_version)\n        };\n\n        std::fs::write(path, new_content.to_string()).expect(\"write file\");\n    }\n}\n\npub fn bump_version(version: &str) -> Result<()> {\n    // testing loco-new will test 4 combinations of starters\n    // sets LOCO_DEV_MODE_PATH=/<path-to>/projects/loco/ and shared cargo build path\n    let new_path = Path::new(\"loco-new\");\n    cargo_fmt(new_path)?;\n    cargo_clippy(new_path)?;\n    if env::var(\"LOCO_DEV_MODE_PATH\").is_err() {\n        let loco_path = current_dir()?.to_string_lossy().to_string();\n        println!(\"setting LOCO_DEV_MODE_PATH to `{loco_path}`\");\n        env::set_var(\"LOCO_DEV_MODE_PATH\", loco_path);\n\n        // this should accelerate starters compilation\n        println!(\"setting CARGO_SHARED_PATH\");\n        env::set_var(\"CARGO_SHARED_PATH\", \"/tmp/cargo-shared-path\");\n    }\n\n    cmd(\"cargo\", [\"test\", \"--\", \"--test-threads\", \"1\"].as_slice())\n        .dir(new_path)\n        .run()?;\n    env::remove_var(\"CARGO_SHARED_PATH\");\n\n    // replace main versions\n    let version_replacement = format!(r#\"version = \"{version}\"\"#);\n    bump_version_in_file(\"Cargo.toml\", r\"(?m)^version.*$\", &version_replacement, true);\n\n    bump_version_in_file(\n        \"loco-gen/Cargo.toml\",\n        r\"(?m)^version.*$\",\n        &version_replacement,\n        true,\n    );\n\n    // sync new version to subcrates in main Cargo.toml\n    let loco_gen_dep = format!(r#\"loco-gen = {{ version = \"{version}\",\"#);\n    bump_version_in_file(\"Cargo.toml\", r\"(?m)^loco-gen [^,]*,\", &loco_gen_dep, false);\n\n    // replace the loco new version pointer\n    // pub const LOCO_VERSION: &str = \"0.13\";\n    let const_version_replacement = format!(r#\"pub const LOCO_VERSION: &str = \"{version}\";\"#);\n    bump_version_in_file(\n        \"loco-new/src/lib.rs\",\n        r#\"(?m)^pub const LOCO_VERSION: &str = \"0.13\";$\"#,\n        &const_version_replacement,\n        true,\n    );\n\n    println!(\n        \"\n    PUBLISHING\n    \n    = framework = \n    \n    $ cd loco-gen && cargo publish\n    $ cargo publish\n    \n    = loco 'new' CLI =\n    \n    $ cd loco-new && cargo-publish\n    \n    = docs =\n\n    $ cd docs-site\n    $ npm build\n    $ zola build && netlify deploy -p -d public\n    \"\n    );\n    Ok(())\n}\n"
  }
]