You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{"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"}
> This file is the **single source of truth** for repository conventions.
7
+
> `CLAUDE.md` is intentionally a one-line redirect (`@AGENTS.md`) — never duplicate
8
+
> guidance into CLAUDE.md.
9
+
6
10
## OVERVIEW
7
11
8
12
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.
9
13
10
14
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.
11
15
16
+
### Headline Capabilities (2026)
17
+
18
+
| Capability | Where | Notes |
19
+
|---|---|---|
20
+
|**`#[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 |
21
+
|**`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 |
|**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 |
24
+
|**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 |
25
+
|**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`) |
26
+
|**Zero-config Spring autoconfigure**|`libs/vespera-bridge/.../VesperaBridgeAutoConfiguration`|`VesperaProxyController` + `AppNameResolver` + `DispatchModeResolver` beans auto-registered; replace any of them via `@ConditionalOnMissingBean`|
27
+
|**Cron jobs**|`#[vespera::cron("...")]`| Auto-discovered like routes; runs via `tokio-cron-scheduler`|
bytes 4+N.. : raw body bytes (UTF-8 text or binary —
165
+
no encoding applied)
132
166
```
133
167
168
+
- No base64 — multipart uploads / PDFs / images travel as raw bytes.
169
+
-`"v":1` is the protocol version; mismatched versions get a `400` wire response.
170
+
- 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.
171
+
-`validation_errors` is an optional array hoisted from 422 JSON bodies (`{"errors":[...]}`) — original body preserved verbatim alongside.
172
+
173
+
### JNI Dispatch Modes (four symbols)
174
+
175
+
| Symbol | Java native | Mode | Memory |
176
+
|---|---|---|---|
177
+
|`Java_...dispatchBytes`|`byte[] dispatchBytes(byte[])`| sync | full body |
178
+
|`Java_...dispatchAsync`|`void dispatchAsync(CompletableFuture<byte[]>, byte[])`| async | full body |
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.
183
+
184
+
### Rust Public API (vespera_inprocess)
185
+
186
+
| Function | Sig | Use |
187
+
|---|---|---|
188
+
|`register_app(F)`| sync | Register the default app (first-wins, BC) |
189
+
|`register_app_named(&str, F)`| sync | Register a named app for multi-app routing |
|`dispatch_typed(Router, &RequestEnvelope) -> ResponseEnvelope`| async | direct axum API (BC) |
196
+
197
+
### Multi-app routing
198
+
199
+
**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.
200
+
201
+
The wire header carries an optional `"app": "<name>"` field (default
202
+
omitted → `"_default"` app). Dispatch looks the name up in
203
+
`APP_ROUTERS: RwLock<HashMap<String, Router>>` and returns:
204
+
205
+
- 404 wire response if the name is registered but no such app exists
206
+
- 400 wire response if the name fails validation (non-empty, ≤ 64 bytes, `[A-Za-z0-9_-]`)
207
+
- Otherwise the matching `Router` is cloned (Arc-backed) and dispatched
208
+
209
+
Two Rust-side macros assemble the single mandatory `JNI_OnLoad`:
210
+
211
+
```rust
212
+
vespera::jni_app!(create_app); // BC sugar for single default app
213
+
214
+
vespera::jni_apps! { // multi-app primary API
215
+
"_default"=>create_app,
216
+
"admin"=>admin_app,
217
+
"public"=>public_app,
218
+
}
219
+
```
220
+
221
+
### Spring Boot autoconfigure (Java side)
222
+
223
+
`vespera-bridge` ships a Spring Boot autoconfiguration that wires up
224
+
`VesperaProxyController` + two strategy beans, both replaceable via
225
+
`@ConditionalOnMissingBean`:
226
+
227
+
-`AppNameResolver` (default: `HeaderAppNameResolver("X-Vespera-App")`) — picks app per request
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.
231
+
134
232
### Rust side (example app — 2 lines of JNI code):
0 commit comments