Skip to content
Open
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
1 change: 1 addition & 0 deletions .changepacks/changepack_log_T_Ks87b0wcAt8G79QLxTv.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"changes":{"Cargo.toml":"Minor","libs/vespera-bridge-gradle-plugin/build.gradle.kts":"Minor","libs/vespera-bridge/build.gradle.kts":"Minor"},"note":"Implement lsp and plugins","date":"2026-05-25T15:18:38.254483600Z"}
2 changes: 1 addition & 1 deletion .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ jobs:
cargo fmt
cargo tarpaulin --out Lcov Stdout --engine llvm
- name: Upload to codecov.io
uses: codecov/codecov-action@v5
uses: codecov/codecov-action@v6
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/deploy-pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ jobs:
bun-${{ runner.os }}-

- name: Install Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: 22

Expand All @@ -60,7 +60,7 @@ jobs:
run: touch apps/landing/out/.nojekyll

- name: Upload artifact
uses: actions/upload-pages-artifact@v3
uses: actions/upload-pages-artifact@v5
with:
path: ./apps/landing/out

Expand All @@ -74,4 +74,4 @@ jobs:
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
uses: actions/deploy-pages@v5
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,8 @@ bin/
*.iml
.idea/
.omc
.omo
node_modules

# Generated OpenAPI artifacts at workspace root
/openapi*.json
3 changes: 0 additions & 3 deletions .oxlintrc.json

This file was deleted.

192 changes: 180 additions & 12 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,29 @@
**Generated:** 2026-03-21
**Branch:** main

> This file is the **single source of truth** for repository conventions.
> `CLAUDE.md` is intentionally a one-line redirect (`@AGENTS.md`) — never duplicate
> guidance into CLAUDE.md.

## OVERVIEW

Vespera is a fully automated OpenAPI 3.1 engine for Axum - delivers FastAPI-like DX to Rust. Zero-config route discovery via compile-time macro scanning.

Also provides in-process dispatch (`vespera_inprocess` crate) and JNI integration (`vespera_jni` crate) for embedding Rust axum apps inside Java/Spring applications without HTTP overhead.

### Headline Capabilities (2026)

| Capability | Where | Notes |
|---|---|---|
| **`#[derive(Schema)]` → OpenAPI 3.1** | `vespera_macro::Schema` | Rust types become JSON Schema at compile time, including serde renames, `Option<T>`, `Vec<T>`, SeaORM relations |
| **`Validated<T>` extractor + auto-`422`** | `vespera::Validated`, `crates/vespera/src/validated.rs` | Wraps `Json`/`Form`/`Query`/`Path` and runs `garde::Validate` before the handler — rejection is **`422 Unprocessable Entity`** with `{"errors":[{"path","message"}]}` JSON envelope |
| **`schema_type! { ... }`** | `vespera_macro::schema_type` | Derive request/response DTOs from existing structs (`pick` / `omit` / `partial` / `add` / `multipart` / `omit_default`) — first-class SeaORM relation support |
| **One-liner `.serve(addr)`** | `vespera::Serve` (`crates/vespera/src/serve.rs`) | Extension trait on `axum::Router` — `create_app().serve("0.0.0.0:3000").await` replaces 3 lines of `TcpListener::bind` + `axum::serve` boilerplate |
| **Binary wire format (JNI)** | `vespera_inprocess` | `[u32 BE len | UTF-8 JSON header | raw body]` — multipart / PDFs / images travel as raw bytes; **`422` validation errors hoisted** into the wire header as `"validation_errors": [...]` so Java decoders never special-case error shapes |
| **Multi-app routing (JNI/FFI)** | `vespera::jni_apps! { "_default" => app, "admin" => admin_app }` | Wire header carries optional `"app"` field; Java side picks per request via `X-Vespera-App` header (configurable via `AppNameResolver`) |
| **Zero-config Spring autoconfigure** | `libs/vespera-bridge/.../VesperaBridgeAutoConfiguration` | `VesperaProxyController` + `AppNameResolver` + `DispatchModeResolver` beans auto-registered; replace any of them via `@ConditionalOnMissingBean` |
| **Cron jobs** | `#[vespera::cron("...")]` | Auto-discovered like routes; runs via `tokio-cron-scheduler` |

## STRUCTURE

```
Expand All @@ -19,7 +36,7 @@ vespera/
│ ├── vespera_core/ # OpenAPI types, route/schema abstractions
│ ├── vespera_macro/ # Proc-macros (main logic lives here)
│ ├── vespera_inprocess/ # In-process dispatch (transport-agnostic)
│ │ └── src/lib.rs # dispatch(), register_app(), dispatch_from_json()
│ │ └── src/lib.rs # dispatch(), register_app(), dispatch_from_bytes()
│ └── vespera_jni/ # JNI bridge (depends on vespera_inprocess)
│ └── src/lib.rs # RUNTIME, jni_app! macro, JNI symbol export
├── libs/
Expand All @@ -46,7 +63,7 @@ vespera/
| Add core types | `crates/vespera_core/src/` | OpenAPI spec types |
| Test new features | `examples/axum-example/` | Add route, run example |
| In-process dispatch | `crates/vespera_inprocess/src/lib.rs` | RequestEnvelope → Router → ResponseEnvelope |
| App factory (FFI pattern) | `crates/vespera_inprocess/src/lib.rs` | register_app(), dispatch_from_json() |
| App factory (FFI pattern) | `crates/vespera_inprocess/src/lib.rs` | register_app(), dispatch_from_bytes() |
| JNI integration | `crates/vespera_jni/src/lib.rs` | RUNTIME, jni_app! macro, JNI symbol export |
| Java bridge library | `libs/vespera-bridge/` | com.devfive.vespera.bridge package |
| JNI demo (Rust) | `examples/rust-jni-demo/src/` | Routes + vespera::jni_app! |
Expand Down Expand Up @@ -76,9 +93,10 @@ vespera (OpenAPI framework)

vespera_inprocess (transport layer — no JNI deps)
├── axum (direct — owns Router re-export)
├── bytes (Bytes for zero-copy body handling)
├── http, http-body-util, tower
├── serde, serde_json
└── tokio (rt only — for dispatch_from_json Runtime param)
└── tokio (rt only — for dispatch_from_bytes Runtime param)

vespera_jni (JNI glue — thin layer)
├── vespera_inprocess (via workspace)
Expand Down Expand Up @@ -120,17 +138,97 @@ Feature flags:
## JNI ARCHITECTURE

```
Java (Spring Boot) Rust (cdylib) vespera crates
───────────────── ────────────── ─────────────────
VesperaBridge.init() → JNI_OnLoad vespera_inprocess::register_app()
↓ ↓
VesperaBridge.dispatch() → JNI symbol vespera_inprocess::dispatch_from_json()
↓ ↓ ↓
VesperaProxyController catch_unwind router.oneshot(request)
↓ ↓ ↓
ResponseEntity JSON envelope axum handlers
Java (Spring Boot) Rust (cdylib) vespera crates
───────────────── ────────────── ─────────────────
VesperaBridge.init() → JNI_OnLoad vespera_inprocess::register_app()
↓ ↓
VesperaBridge.dispatchBytes() → JNI symbol vespera_inprocess::dispatch_from_bytes()
↓ ↓ ↓
VesperaProxyController catch_unwind router.oneshot(request)
↓ ↓ ↓
ResponseEntity binary wire response axum handlers
(String OR byte[]) [u32 BE | JSON | body]
```

### Binary Wire Format

Both request and response use the same layout:

```
bytes 0..4 : u32 BE = header_json byte length N
bytes 4..4+N : UTF-8 JSON
(request) { "v":1, "method", "path",
"query"?, "headers"? }
(response) { "v":1, "status", "headers",
"metadata", "validation_errors"? }
bytes 4+N.. : raw body bytes (UTF-8 text or binary —
no encoding applied)
```

- No base64 — multipart uploads / PDFs / images travel as raw bytes.
- `"v":1` is the protocol version; mismatched versions get a `400` wire response.
- All failure modes (malformed wire, panic in Rust, no app registered) return a valid length-prefixed wire response, so the Java decoder never has to special-case errors.
- `validation_errors` is an optional array hoisted from 422 JSON bodies (`{"errors":[...]}`) — original body preserved verbatim alongside.

### JNI Dispatch Modes (four symbols)

| Symbol | Java native | Mode | Memory |
|---|---|---|---|
| `Java_...dispatchBytes` | `byte[] dispatchBytes(byte[])` | sync | full body |
| `Java_...dispatchAsync` | `void dispatchAsync(CompletableFuture<byte[]>, byte[])` | async | full body |
| `Java_...dispatchStreaming` | `byte[] dispatchStreaming(byte[], OutputStream)` | sync response-streaming | chunk-bounded response |
| `Java_...dispatchFullStreaming` | `byte[] dispatchFullStreaming(byte[], InputStream, OutputStream)` | sync bidirectional streaming | chunk-bounded both directions |

All four share the same wire format, registered router, and panic-safe `catch_unwind` discipline. `dispatchAsync` spawns the dispatch on Rust's shared Tokio runtime via `tokio::spawn` (panic → `JoinError` → `error_wire(500)`) and completes the `CompletableFuture` from a worker thread via `attach_current_thread`. `dispatchStreaming` drains the response body chunk-by-chunk via `http_body::Body::frame()` and writes each chunk to the Java `OutputStream`. `dispatchFullStreaming` adds request-side streaming: a `tokio::task::spawn_blocking` thread pulls 16 KiB chunks from `InputStream.read(byte[])` and feeds them into axum via an `mpsc::channel`-backed `http_body::Body`, giving natural backpressure (bounded 16-slot channel) so 1 GiB uploads run in `O(chunk_size)` RAM.

### Rust Public API (vespera_inprocess)

| Function | Sig | Use |
|---|---|---|
| `register_app(F)` | sync | Register the default app (first-wins, BC) |
| `register_app_named(&str, F)` | sync | Register a named app for multi-app routing |
| `dispatch_from_bytes(Vec<u8>, &Runtime) -> Vec<u8>` | sync | FFI entry, blocks on runtime |
| `dispatch_from_bytes_async(Vec<u8>) -> Vec<u8>` (async) | async | inside an existing runtime |
| `dispatch_streaming_async<F>(Vec<u8>, F) -> Vec<u8>` (async) | response streaming async | `F: FnMut(&[u8])` body chunks |
| `dispatch_bidirectional_streaming<P,F>(Vec<u8>, P, F) -> Vec<u8>` (async) | bidirectional streaming | `P: FnMut() -> Option<Vec<u8>> + Send + 'static`, `F: FnMut(&[u8])` |
| `error_wire(u16, &str) -> Vec<u8>` | sync | wire-format error builder |
| `dispatch_typed(Router, &RequestEnvelope) -> ResponseEnvelope` | async | direct axum API (BC) |

### Multi-app routing

**Use case**: multi-app is primarily a feature for **external-dispatcher scenarios** — JNI (Java host picks app per request via header), WebAssembly bridge, C FFI, or any in-process embedding where the host distinguishes between multiple independent vespera API surfaces. For Rust **standalone** servers (`axum::serve(...)`), the native axum patterns (`Router::merge()`, `Router::nest()`) are more idiomatic for modularization — `register_app_named` adds no value when the same binary owns both the router registration and the HTTP entry point.

The wire header carries an optional `"app": "<name>"` field (default
omitted → `"_default"` app). Dispatch looks the name up in
`APP_ROUTERS: RwLock<HashMap<String, Router>>` and returns:

- 404 wire response if the name is registered but no such app exists
- 400 wire response if the name fails validation (non-empty, ≤ 64 bytes, `[A-Za-z0-9_-]`)
- Otherwise the matching `Router` is cloned (Arc-backed) and dispatched

Two Rust-side macros assemble the single mandatory `JNI_OnLoad`:

```rust
vespera::jni_app!(create_app); // BC sugar for single default app

vespera::jni_apps! { // multi-app primary API
"_default" => create_app,
"admin" => admin_app,
"public" => public_app,
}
```

### Spring Boot autoconfigure (Java side)

`vespera-bridge` ships a Spring Boot autoconfiguration that wires up
`VesperaProxyController` + two strategy beans, both replaceable via
`@ConditionalOnMissingBean`:

- `AppNameResolver` (default: `HeaderAppNameResolver("X-Vespera-App")`) — picks app per request
- `DispatchModeResolver` (default: `BidirectionalStreamingDispatchModeResolver`) — picks `DispatchMode`

Property `vespera.bridge.controller-enabled=false` disables the whole controller for BYO scenarios. See [`libs/vespera-bridge/README.md`](libs/vespera-bridge/README.md#customization) for the customization recipes.

### Rust side (example app — 2 lines of JNI code):
```rust
pub fn create_app() -> axum::Router { vespera!(...) }
Expand Down Expand Up @@ -167,6 +265,76 @@ Generate request/response types from existing structs with powerful transformati
| `rename_all` | Serde rename strategy |
| `ignore` | Skip Schema derive |

## REPOSITORY SHAPE

Vespera is a **hybrid monorepo** with two workspaces co-located at the repo root:

| Workspace | Manager | Members | Purpose |
|---|---|---|---|
| Cargo (`Cargo.toml`) | cargo | `crates/*`, `examples/*` (excluding `examples/java-jni-demo`) | OpenAPI engine, proc-macros, JNI bridge |
| Bun (`package.json`) | bun | `apps/*` | Marketing/docs site + admin panel (Next.js) |

`bun run ...` operates on the Node side; `cargo ...` on the Rust side. Many root
scripts deliberately cross the boundary — e.g., `prelint` runs `cargo
clippy/fmt/check` **before** oxlint touches JS.

### Common Commands

```bash
# --- Rust side ---
cargo build # Build all crates
cargo test --workspace # All Rust tests
cargo test -p vespera_macro # One crate
cargo test --test <name> -- <filter> # Single integration test
cargo tarpaulin --out stdout # Coverage (via `bun run posttest`)

# --- Lint / format (order matters — `prelint` runs Rust FIRST) ---
bun run lint # oxlint (after `cargo clippy + fmt --check + check`)
bun run lint:fix # oxlint --fix (after `cargo clippy --fix && cargo fmt`)

# --- Front-end workspace ---
bun run dev # `dev` in every apps/*
bun run build # apps/front + apps/admin
cd apps/front && bun dev # Single-app dev (preferred per devfive-frontend)

# --- Tests (Bun side) ---
bun test # Root runs bun test + tarpaulin (posttest hook)

# --- Release tooling ---
bun run changepacks # @changepacks/cli version bumps
```

> **`prelint` gotcha:** any Rust warning fails the JS lint. Run `bun run
> lint:fix` to auto-resolve both sides.

### Frontend (`apps/front`)

Next.js 16 App Router + React 19 + `@devup-ui/react` (build-time CSS-in-JS).
Theme tokens live in `apps/front/devup.json` and use `$token` syntax in JSX
props only.

- `apps/front/src/app/` contains **only** `layout.tsx` + `page.tsx` — all other
components live in `src/components/` (per devfive-frontend conventions).
- Styling uses devup-ui shorthand props (`bg`, `p`, `w`, `_hover`,
`[mobile,null,pc]` responsive arrays). Never `style={{...}}` or Tailwind.

### Where Tests Live

| Concern | Location |
|---|---|
| Macro integration tests | `crates/vespera_macro/tests/` (+ `insta` snapshots) |
| Validated/422 contract | `crates/vespera/tests/validated_extractor.rs`, `crates/vespera/tests/jni_validation.rs` |
| Core unit tests | `crates/vespera_core/src/**` inline `#[cfg(test)]` |
| JNI end-to-end | `examples/rust-jni-demo` (Rust + Java + Gradle) |
| Front tests | `apps/front/src/__tests__/` (`bun test` + `bun-test-env-dom`) |

`insta` snapshots — run `cargo insta review` to accept drifts.

### Pre-Commit (Husky)

`bun run prepare` installs husky; commits trigger `.husky/` hooks (typically
`lint`). Never bypass with `--no-verify`; fix the underlying finding.

## CONVENTIONS

- **Rust 2024 edition** across all crates
Expand Down
Loading
Loading