Skip to content

Commit 16fbd67

Browse files
authored
Merge pull request #132 from dev-five-git/refactor-1
Refactor
2 parents a6dbbb2 + c65b8a8 commit 16fbd67

105 files changed

Lines changed: 11780 additions & 5386 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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"}

.github/workflows/CI.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ jobs:
5353
cargo fmt
5454
cargo tarpaulin --out Lcov Stdout --engine llvm
5555
- name: Upload to codecov.io
56-
uses: codecov/codecov-action@v5
56+
uses: codecov/codecov-action@v6
5757
with:
5858
token: ${{ secrets.CODECOV_TOKEN }}
5959
fail_ci_if_error: true

.github/workflows/deploy-pages.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ jobs:
3737
bun-${{ runner.os }}-
3838
3939
- name: Install Node.js
40-
uses: actions/setup-node@v4
40+
uses: actions/setup-node@v6
4141
with:
4242
node-version: 22
4343

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

6262
- name: Upload artifact
63-
uses: actions/upload-pages-artifact@v3
63+
uses: actions/upload-pages-artifact@v5
6464
with:
6565
path: ./apps/landing/out
6666

@@ -74,4 +74,4 @@ jobs:
7474
steps:
7575
- name: Deploy to GitHub Pages
7676
id: deployment
77-
uses: actions/deploy-pages@v4
77+
uses: actions/deploy-pages@v5

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,8 @@ bin/
2121
*.iml
2222
.idea/
2323
.omc
24+
.omo
2425
node_modules
26+
27+
# Generated OpenAPI artifacts at workspace root
28+
/openapi*.json

.oxlintrc.json

Lines changed: 0 additions & 3 deletions
This file was deleted.

AGENTS.md

Lines changed: 180 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,29 @@
33
**Generated:** 2026-03-21
44
**Branch:** main
55

6+
> 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+
610
## OVERVIEW
711

812
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.
913

1014
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.
1115

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 |
22+
| **`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 |
23+
| **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` |
28+
1229
## STRUCTURE
1330

1431
```
@@ -19,7 +36,7 @@ vespera/
1936
│ ├── vespera_core/ # OpenAPI types, route/schema abstractions
2037
│ ├── vespera_macro/ # Proc-macros (main logic lives here)
2138
│ ├── vespera_inprocess/ # In-process dispatch (transport-agnostic)
22-
│ │ └── src/lib.rs # dispatch(), register_app(), dispatch_from_json()
39+
│ │ └── src/lib.rs # dispatch(), register_app(), dispatch_from_bytes()
2340
│ └── vespera_jni/ # JNI bridge (depends on vespera_inprocess)
2441
│ └── src/lib.rs # RUNTIME, jni_app! macro, JNI symbol export
2542
├── libs/
@@ -46,7 +63,7 @@ vespera/
4663
| Add core types | `crates/vespera_core/src/` | OpenAPI spec types |
4764
| Test new features | `examples/axum-example/` | Add route, run example |
4865
| In-process dispatch | `crates/vespera_inprocess/src/lib.rs` | RequestEnvelope → Router → ResponseEnvelope |
49-
| App factory (FFI pattern) | `crates/vespera_inprocess/src/lib.rs` | register_app(), dispatch_from_json() |
66+
| App factory (FFI pattern) | `crates/vespera_inprocess/src/lib.rs` | register_app(), dispatch_from_bytes() |
5067
| JNI integration | `crates/vespera_jni/src/lib.rs` | RUNTIME, jni_app! macro, JNI symbol export |
5168
| Java bridge library | `libs/vespera-bridge/` | com.devfive.vespera.bridge package |
5269
| JNI demo (Rust) | `examples/rust-jni-demo/src/` | Routes + vespera::jni_app! |
@@ -76,9 +93,10 @@ vespera (OpenAPI framework)
7693
7794
vespera_inprocess (transport layer — no JNI deps)
7895
├── axum (direct — owns Router re-export)
96+
├── bytes (Bytes for zero-copy body handling)
7997
├── http, http-body-util, tower
8098
├── serde, serde_json
81-
└── tokio (rt only — for dispatch_from_json Runtime param)
99+
└── tokio (rt only — for dispatch_from_bytes Runtime param)
82100
83101
vespera_jni (JNI glue — thin layer)
84102
├── vespera_inprocess (via workspace)
@@ -120,17 +138,97 @@ Feature flags:
120138
## JNI ARCHITECTURE
121139

122140
```
123-
Java (Spring Boot) Rust (cdylib) vespera crates
124-
───────────────── ────────────── ─────────────────
125-
VesperaBridge.init() → JNI_OnLoad vespera_inprocess::register_app()
126-
↓ ↓
127-
VesperaBridge.dispatch() → JNI symbol vespera_inprocess::dispatch_from_json()
128-
↓ ↓ ↓
129-
VesperaProxyController catch_unwind router.oneshot(request)
130-
↓ ↓ ↓
131-
ResponseEntity JSON envelope axum handlers
141+
Java (Spring Boot) Rust (cdylib) vespera crates
142+
───────────────── ────────────── ─────────────────
143+
VesperaBridge.init() → JNI_OnLoad vespera_inprocess::register_app()
144+
↓ ↓
145+
VesperaBridge.dispatchBytes() → JNI symbol vespera_inprocess::dispatch_from_bytes()
146+
↓ ↓ ↓
147+
VesperaProxyController catch_unwind router.oneshot(request)
148+
↓ ↓ ↓
149+
ResponseEntity binary wire response axum handlers
150+
(String OR byte[]) [u32 BE | JSON | body]
151+
```
152+
153+
### Binary Wire Format
154+
155+
Both request and response use the same layout:
156+
157+
```
158+
bytes 0..4 : u32 BE = header_json byte length N
159+
bytes 4..4+N : UTF-8 JSON
160+
(request) { "v":1, "method", "path",
161+
"query"?, "headers"? }
162+
(response) { "v":1, "status", "headers",
163+
"metadata", "validation_errors"? }
164+
bytes 4+N.. : raw body bytes (UTF-8 text or binary —
165+
no encoding applied)
132166
```
133167

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 |
179+
| `Java_...dispatchStreaming` | `byte[] dispatchStreaming(byte[], OutputStream)` | sync response-streaming | chunk-bounded response |
180+
| `Java_...dispatchFullStreaming` | `byte[] dispatchFullStreaming(byte[], InputStream, OutputStream)` | sync bidirectional streaming | chunk-bounded both directions |
181+
182+
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 |
190+
| `dispatch_from_bytes(Vec<u8>, &Runtime) -> Vec<u8>` | sync | FFI entry, blocks on runtime |
191+
| `dispatch_from_bytes_async(Vec<u8>) -> Vec<u8>` (async) | async | inside an existing runtime |
192+
| `dispatch_streaming_async<F>(Vec<u8>, F) -> Vec<u8>` (async) | response streaming async | `F: FnMut(&[u8])` body chunks |
193+
| `dispatch_bidirectional_streaming<P,F>(Vec<u8>, P, F) -> Vec<u8>` (async) | bidirectional streaming | `P: FnMut() -> Option<Vec<u8>> + Send + 'static`, `F: FnMut(&[u8])` |
194+
| `error_wire(u16, &str) -> Vec<u8>` | sync | wire-format error builder |
195+
| `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
228+
- `DispatchModeResolver` (default: `BidirectionalStreamingDispatchModeResolver`) — picks `DispatchMode`
229+
230+
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+
134232
### Rust side (example app — 2 lines of JNI code):
135233
```rust
136234
pub fn create_app() -> axum::Router { vespera!(...) }
@@ -167,6 +265,76 @@ Generate request/response types from existing structs with powerful transformati
167265
| `rename_all` | Serde rename strategy |
168266
| `ignore` | Skip Schema derive |
169267

268+
## REPOSITORY SHAPE
269+
270+
Vespera is a **hybrid monorepo** with two workspaces co-located at the repo root:
271+
272+
| Workspace | Manager | Members | Purpose |
273+
|---|---|---|---|
274+
| Cargo (`Cargo.toml`) | cargo | `crates/*`, `examples/*` (excluding `examples/java-jni-demo`) | OpenAPI engine, proc-macros, JNI bridge |
275+
| Bun (`package.json`) | bun | `apps/*` | Marketing/docs site + admin panel (Next.js) |
276+
277+
`bun run ...` operates on the Node side; `cargo ...` on the Rust side. Many root
278+
scripts deliberately cross the boundary — e.g., `prelint` runs `cargo
279+
clippy/fmt/check` **before** oxlint touches JS.
280+
281+
### Common Commands
282+
283+
```bash
284+
# --- Rust side ---
285+
cargo build # Build all crates
286+
cargo test --workspace # All Rust tests
287+
cargo test -p vespera_macro # One crate
288+
cargo test --test <name> -- <filter> # Single integration test
289+
cargo tarpaulin --out stdout # Coverage (via `bun run posttest`)
290+
291+
# --- Lint / format (order matters — `prelint` runs Rust FIRST) ---
292+
bun run lint # oxlint (after `cargo clippy + fmt --check + check`)
293+
bun run lint:fix # oxlint --fix (after `cargo clippy --fix && cargo fmt`)
294+
295+
# --- Front-end workspace ---
296+
bun run dev # `dev` in every apps/*
297+
bun run build # apps/front + apps/admin
298+
cd apps/front && bun dev # Single-app dev (preferred per devfive-frontend)
299+
300+
# --- Tests (Bun side) ---
301+
bun test # Root runs bun test + tarpaulin (posttest hook)
302+
303+
# --- Release tooling ---
304+
bun run changepacks # @changepacks/cli version bumps
305+
```
306+
307+
> **`prelint` gotcha:** any Rust warning fails the JS lint. Run `bun run
308+
> lint:fix` to auto-resolve both sides.
309+
310+
### Frontend (`apps/front`)
311+
312+
Next.js 16 App Router + React 19 + `@devup-ui/react` (build-time CSS-in-JS).
313+
Theme tokens live in `apps/front/devup.json` and use `$token` syntax in JSX
314+
props only.
315+
316+
- `apps/front/src/app/` contains **only** `layout.tsx` + `page.tsx` — all other
317+
components live in `src/components/` (per devfive-frontend conventions).
318+
- Styling uses devup-ui shorthand props (`bg`, `p`, `w`, `_hover`,
319+
`[mobile,null,pc]` responsive arrays). Never `style={{...}}` or Tailwind.
320+
321+
### Where Tests Live
322+
323+
| Concern | Location |
324+
|---|---|
325+
| Macro integration tests | `crates/vespera_macro/tests/` (+ `insta` snapshots) |
326+
| Validated/422 contract | `crates/vespera/tests/validated_extractor.rs`, `crates/vespera/tests/jni_validation.rs` |
327+
| Core unit tests | `crates/vespera_core/src/**` inline `#[cfg(test)]` |
328+
| JNI end-to-end | `examples/rust-jni-demo` (Rust + Java + Gradle) |
329+
| Front tests | `apps/front/src/__tests__/` (`bun test` + `bun-test-env-dom`) |
330+
331+
`insta` snapshots — run `cargo insta review` to accept drifts.
332+
333+
### Pre-Commit (Husky)
334+
335+
`bun run prepare` installs husky; commits trigger `.husky/` hooks (typically
336+
`lint`). Never bypass with `--no-verify`; fix the underlying finding.
337+
170338
## CONVENTIONS
171339

172340
- **Rust 2024 edition** across all crates

0 commit comments

Comments
 (0)