Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 31 additions & 15 deletions .cursor/rules/git-workflow.mdc
Original file line number Diff line number Diff line change
@@ -1,26 +1,42 @@
---
description: Feature branches and PRs for GitHub issues — never land feature work by committing straight to dev/main
description: Issue workflow — sync dev, branch, implement, test+lint, atomic commits, PR to dev
alwaysApply: true
---

# Git and GitHub workflow (OxyRoute)

When implementing a **feature, fix, or issue** (including backlog / issue numbers in `.github/ISSUE_BACKLOG`):
**Integration branch is `dev`.** Do not land feature or fix work by committing directly to `dev` or `main` (see exceptions at the end).

1. **Branch**
- Do **not** commit or push feature work directly to `dev` or `main`.
- Create a branch from the current base (usually `dev`): `git fetch origin` then `git checkout -b <branch>`.
- Prefer names like: `feat/<short-slug>`, or `issue-<N>-<short-slug>` (e.g. `issue-20-head-options`).
## Order of operations (always)

2. **Pull request**
- Push the branch: `git push -u origin <branch>`.
- Open a **PR** into `dev` (or the repo’s default integration branch).
- In the PR title or description, **link the issue**: `Closes #N` / `Fixes #N` / `Ref #N` as appropriate so GitHub closes or tracks the work.
1. **Sync `dev` first**
- `git fetch origin`
- `git checkout dev`
- `git pull origin dev`

3. **Before coding**
- Confirm whether an issue already exists; if the user only gave a topic, match it to a backlog file or open an issue, then use the same branch+PR flow.
2. **Branch from that `dev` for one GitHub issue** (one topic per PR when possible)
- `git checkout -b issue-<N>-<short-slug>` (e.g. `issue-47-multipart-form`)

4. **Exceptions**
- Tiny doc typo or rule-only changes may still use a small branch+PR, or a single “chore:” PR if the team agrees — default is **always branch + PR** for any code or test change.
3. **Read the requirement**
- Open the GitHub issue and, if present, the body under [`.github/ISSUE_BACKLOG/bodies/`](.github/ISSUE_BACKLOG/bodies/) or linked docs.

This applies to all agents and humans working in this repo.
4. **Implement** in the branch (Rust/Python/docs as needed).

5. **Validate before commit/push** (match CI in [`.github/workflows/ci.yml`](.github/workflows/ci.yml))
- Python: `uv run ruff check oxyroute tests examples` and `uv run ruff format --check oxyroute tests examples` (or format then check).
- Rust: `cargo fmt --all -- --check` and `cargo clippy --all-targets -- -D warnings`
- Tests: `uv run pytest` (or the same isolated pattern as in `docs/development.md` if testing the installed wheel).

6. **Atomic commits** — one logical change per commit (e.g. `feat:`, `fix:`, `test:`, `docs:`). **Do not** mix product code and [`.github/ISSUE_BACKLOG/`](.github/ISSUE_BACKLOG/) in the same commit; backlog updates: separate `docs:` / `chore:` commit if needed.

7. **PR into `dev`**
- `git push -u origin <branch>`
- Open PR: **base = `dev`**, compare = your branch. Use **`Closes #N`** (or `Fixes #N`) when the PR completes issue **N**.

8. **After merge** — delete the local branch, keep `main` and `dev`; next issue: repeat from step 1.

## Exceptions

- Trivial one-line doc fixes may still use a small branch + PR, or a maintainer chore — default is **branch + PR** for any code or test change.

[Full narrative + `gh` examples: `docs/development-workflow.md`]
6 changes: 3 additions & 3 deletions .github/ISSUE_BACKLOG/PRIORITIES.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ This file tracks **priority tiers** for items in [bodies/](bodies/). The **next
|---|------|-----------|
| 4 | [04.md](bodies/04.md) | **Route hot path** — reduce lock contention / route snapshot after `freeze()`. |
| 17 | [17.md](bodies/17.md) | **ASGI bridge** — `run_coroutine_threadsafe` / thread safety under concurrent load. |
| 18 | [18.md](bodies/18.md) | **App state / lifespan** — shared resources across workers. |
| 22 | [22.md](bodies/22.md) | **Multipart / urlencoded body** — [GitHub #47](https://github.com/QueryaHub/OxyRoute/issues/47). |

## P1 (API ergonomics, security helpers, protocol features)

Expand All @@ -36,10 +34,12 @@ This file tracks **priority tiers** for items in [bodies/](bodies/). The **next
- **Query, errors, E2E:** 2, 3, 12
- **API surface:** 1, 5, 6, 7, 10, 11, 19, 20
- **CI / release / docs / PyO3:** 13, 14, 15, 16
- **Lifespan / `app.state`:** 18 (see `App.state`, `examples/rsgi_lifespan_app.py`, `docs/rsgi.md`)
- **Form bodies:** 22 / [#47](https://github.com/QueryaHub/OxyRoute/issues/47) — `read_form_body`, `form` / `files` kwargs, `docs/handlers.md`

## Roadmap phasing (summary)

1. **P0:** 4, 17, 18, 47 (order flexible).
1. **P0:** 4, 17; **18** / **#47 (form)** done (order flexible).
2. **P1:** 8, 46, 48, 49, 53, 54; 9 as polish.
3. **Research:** 50, 51, 52 — as capacity allows.

Expand Down
7 changes: 4 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,12 @@ cargo clippy

## Branches, PRs, and atomic commits

- **Integration branch is `dev`:** create your branch from `dev`, open PRs with **base = `dev`**. The maintainer merges to `dev` and later promotes to `main` for releases as needed.
- **One issue, one PR (when possible):** name the branch with the issue number, e.g. `issue-2-query-decode` or `feat/2-query-decode`.
- **Integration branch is `dev`:** always `git fetch origin`, `git checkout dev`, `git pull origin dev` **before** creating a branch. Open PRs with **base = `dev`**; maintainers promote to `main` for releases as needed.
- **One issue, one PR (when possible):** name the branch with the issue number, e.g. `issue-2-query-decode` or `feat/2-query-decode`. Read the GitHub issue (and backlog body if linked) before coding.
- After implementation, run **the same tests and linters as CI** (Ruff, `cargo fmt`/`clippy`, pytest) — see [`.github/workflows/ci.yml`](.github/workflows/ci.yml).
- Use **atomic commits** (one logical change per commit). **Do not** put `.github/ISSUE_BACKLOG/` changes in the same commit as product code—backlog/templating updates should be a separate `docs:` (or `chore:`) commit, or a separate PR.
- Prefer **conventional commits**: `feat:`, `fix:`, `docs:`, `ci:`, `chore:`, `test:`.
- Full step-by-step: [docs/development-workflow.md](docs/development-workflow.md).
- Full checklist: [docs/development-workflow.md](docs/development-workflow.md).

## Creating GitHub issues from the template backlog

Expand Down
69 changes: 69 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ base64 = "0.22"
once_cell = "1.19"
# RFC 1738 / WHATWG: percent-decode and + as space in application/x-www-form-urlencoded query
form_urlencoded = "1.2"
bytes = "1"
futures-util = { version = "0.3", default-features = false, features = ["std"] }
multer = "3.1"
log = "0.4"
parking_lot = "0.12"

Expand Down
8 changes: 5 additions & 3 deletions docs/asgi.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ The module `oxyroute.asgi` builds a minimal RSGI-shaped `scope` / `protocol` and

## How responses get back to ASGI

The RSGI response helpers on the protocol object are **synchronous** from the Rust side. The ASGI implementation schedules `send` coroutines on the **current asyncio event loop** that was running when the request was accepted (`asyncio.run_coroutine_threadsafe` + `result()`), so the event loop in the process must be free to make progress when native code issues responses.
The RSGI response helpers on the protocol object are **synchronous** from the Rust side. The ASGI implementation schedules `send` coroutines on the **ASGI process’s** event loop: that loop is captured in the protocol and native code uses `asyncio.run_coroutine_threadsafe` + `result()` to run each `send` on it.

**Implications:** This bridge is a **practical adapter**, not a full reimplementation of RSGI under every ASGI host. Prefer **RSGI/Granian** for production with OxyRoute if you can.
**Why the bridge does not `await` on that loop directly:** Awaiting the native `handle_rsgi` coroutine on the same loop that must drain `run_coroutine_threadsafe` can **deadlock** (the loop is blocked in `await` and never runs the scheduled `send` tasks). The bridge therefore runs `handle_rsgi` in a **thread-pool** worker using `asyncio.run()` on a **separate** event loop, while the protocol still targets the main ASGI loop for `send` scheduling.

**Implications:** This bridge is a **practical adapter**, not a full reimplementation of RSGI under every ASGI host. Prefer **RSGI/Granian** for production with OxyRoute if you can. With **Uvicorn**, use a **single worker** process unless you are sure the combination is safe in your app (the usual `workers=N` + in-process `asyncio` footguns can still apply to third-party `asyncio` use).

## Limitations (current)

Expand All @@ -20,7 +22,7 @@ The RSGI response helpers on the protocol object are **synchronous** from the Ru

## Local testing

The **httpx** project’s `ASGITransport` is used in `tests/test_asgi.py` to exercise the bridge without a real TCP server. That test is a good template for a smoke check.
The **httpx** project’s `ASGITransport` is used in `tests/test_asgi.py` to exercise the bridge without a real TCP server. `tests/test_asgi_stress.py` runs **50** concurrent `GET` requests to guard against cross-thread/loop deadlocks in the bridge.

## See also

Expand Down
2 changes: 1 addition & 1 deletion docs/dependencies.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ The underlying callables are unwrapped in Python and passed to the native `add_r

## Freezing route registration

Calling **`app.freeze()`** (forwarded to the native `App`) sets the app to **no longer accept** new routes—use this when you want a final route table before serving (future-proofing for DI graphs and similar).
Calling **`app.freeze()`** (forwarded to the native `App`) sets the app to **no longer accept** new routes—use this when you want a final route table before serving (future-proofing for DI graphs and similar). The native layer also **clones the per-method `matchit` routers** into a read-only snapshot so that **path matching no longer takes per-router mutexes** on the hot request path (the mutable copies remain for introspection alignment with the snapshot).

## See also

Expand Down
42 changes: 22 additions & 20 deletions docs/development-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,43 @@

OxyRoute uses **`dev` as the integration branch**. `main` (or the default production branch) is updated from `dev` when maintainers cut a release or merge a stable snapshot—follow what your team documents for release tagging.

## Branching model

1. **Always branch from the latest `dev`:**
## Checklist (order matters)

1. **Sync `dev` first (always `fetch` + `pull` before branching):**
```bash
git fetch origin
git checkout dev
git pull origin dev
```
2. **Create a branch** for one GitHub issue: `git checkout -b issue-<N>-<short-slug>`.
3. **Read the issue** (and [`.github/ISSUE_BACKLOG/bodies/`](.github/ISSUE_BACKLOG/bodies/) or linked spec if you use it).
4. **Implement** on that branch.
5. **Run tests and linters** (same as CI, see [`.github/workflows/ci.yml`](../.github/workflows/ci.yml)):
- `uv run ruff check oxyroute tests examples` and `uv run ruff format --check oxyroute tests examples`
- `cargo fmt --all -- --check` and `cargo clippy --all-targets -- -D warnings`
- `uv run pytest` (or the isolated pattern in [development.md](development.md))
6. **Commit atomically** (one logical change per commit: `feat:`, `fix:`, `test:`, `docs:`, …).
7. **Push and open a PR to `dev`** (base = `dev`), with **`Closes #N`** when the issue is done.

2. **Create a branch for one GitHub issue** (one topic per PR when possible):

```bash
git checkout -b issue-12-rsgi-e2e
# or: feat/12-rsgi-e2e — use a short, kebab-case slug
```
## Branch naming

Prefer including the **issue number** in the name so the PR and issue stay linked (e.g. #12).
- Prefer `issue-<N>-<kebab-slug>` or `feat/<N>-<kebab>` so the PR links to the issue (e.g. #12).

3. **Implement the issue** with **small, atomic commits** (one logical change per commit: `fix: …`, `test: …`, `docs: …`).
## Atomic commits and backlog

4. **Do not mix** code changes and **`.github/ISSUE_BACKLOG/`** (issue template bodies) in the same commit. The backlog is reference material for maintainers; if you need to update it, use a **separate** commit, e.g. `docs: update issue backlog description`.
- **Do not mix** product code and **`.github/ISSUE_BACKLOG/`** in the same commit. If you update templates, use a **separate** `docs:` or `chore:` commit (or a separate PR).

5. **Push and open a pull request** with **base = `dev`** and **compare = your branch**.
## Push and open a pull request

```bash
git push -u origin issue-12-rsgi-e2e
```
```bash
git push -u origin issue-12-rsgi-e2e
```

On GitHub: **New pull request** → base repository `QueryaHub/OxyRoute`, **base: `dev`**, **compare: `issue-12-rsgi-e2e`**.
On GitHub: **New pull request** → **base: `dev`**, **compare: your branch**. Use **`Closes #N`** (or `Fixes #N`) to auto-close the issue on merge.

6. In the PR description, use **`Closes #N`** (or `Fixes #N`) if the work fully finishes issue **N**—GitHub will close the issue when the PR is merged.
## After merge

7. After merge into `dev`, delete the remote branch (GitHub can do this on merge) and locally:
`git branch -d issue-12-rsgi-e2e` , then continue with the next issue from a fresh `dev`.
Delete the remote branch (GitHub can do this on merge) and locally: `git branch -d issue-12-rsgi-e2e`. For the **next** issue, repeat from **fetch + pull `dev`**.

## What not to do

Expand Down
6 changes: 3 additions & 3 deletions docs/feature.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@

| Тема | Зазор | Комментарий |
|------|--------|-------------|
| **`multipart/form-data`** | Нет | Загрузка файлов, поля формы — нужен парсер и API (`upload`, `form()`). |
| **`application/x-www-form-urlencoded` (body)** | Нет | Сейчас разбор query есть; тело формы не инжектится как отдельный тип. |
| **`multipart/form-data`** | Частично | `read_form_body=True`, инжект `form` / `files` (в памяти); см. [handlers](handlers.md). [#47](https://github.com/QueryaHub/OxyRoute/issues/47). |
| **`application/x-www-form-urlencoded` (body)** | Частично | То же — `form`; нет streaming для очень больших тел. |
| **Streaming request body** | Ограничено | Тело читается в буфер для JSON/сырых байт; большие upload без полного чтения — отдельная работа. |
| **Валидация на уровне фреймворка** | Частично | Pydantic удобнее вручную; нет единого `Body()` как в FastAPI для всех типов контента. |

Expand Down Expand Up @@ -68,7 +68,7 @@

| Тема | Зазор | Комментарий |
|------|--------|-------------|
| **Встроенный `app.state` / lifespan** | Слабо | `__rsgi_init__` и общий state — [#18](https://github.com/QueryaHub/OxyRoute/issues/18). |
| **Встроенный `app.state` / lifespan** | Частично | `App.state` (`SimpleNamespace`), хуки `__rsgi_init__` / `__rsgi_del__`, пример `examples/rsgi_lifespan_app.py` — закрыто в [#18](https://github.com/QueryaHub/OxyRoute/issues/18). |
| **Сессии (signed cookie, server-side)** | Нет | |
| **Кэш глобальных настроек** | Нет | Нет первого класса для config. |

Expand Down
Loading
Loading