Skip to content

Commit 691e1f5

Browse files
authored
Merge pull request #51 from dev-five-git/add-third
Add third
2 parents 7798b92 + d638dd4 commit 691e1f5

18 files changed

Lines changed: 3050 additions & 90 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"changes":{"crates/vespera_core/Cargo.toml":"Patch","crates/vespera_macro/Cargo.toml":"Patch","crates/vespera/Cargo.toml":"Patch"},"note":"Implement export app","date":"2026-01-25T13:55:16.361701200Z"}

Cargo.lock

Lines changed: 34 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,10 +194,27 @@ let app = vespera!(
194194
servers = [ // OpenAPI servers
195195
{ url = "https://api.example.com", description = "Production" },
196196
{ url = "http://localhost:3000", description = "Development" }
197-
]
197+
],
198+
merge = [crate1::App1, crate2::App2] // Merge child vespera apps
198199
);
199200
```
200201

202+
## `export_app!` Macro Reference
203+
204+
Export a vespera app for merging into other apps:
205+
206+
```rust
207+
// Basic usage (scans "routes" folder by default)
208+
vespera::export_app!(MyApp);
209+
210+
// Custom directory
211+
vespera::export_app!(MyApp, dir = "api");
212+
```
213+
214+
Generates a struct with:
215+
- `MyApp::OPENAPI_SPEC: &'static str` - The OpenAPI JSON spec
216+
- `MyApp::router() -> Router` - Function returning the Axum router
217+
201218
### Environment Variable Fallbacks
202219

203220
All parameters support environment variable fallbacks:
@@ -251,6 +268,40 @@ let app = vespera!("api");
251268
let app = vespera!(dir = "api");
252269
```
253270

271+
### Merging Multiple Vespera Apps
272+
273+
Combine routes and OpenAPI specs from multiple vespera apps at compile time:
274+
275+
**Child app (e.g., `third` crate):**
276+
```rust
277+
// src/lib.rs
278+
mod routes;
279+
280+
// Export app for merging (dir defaults to "routes")
281+
vespera::export_app!(ThirdApp);
282+
283+
// Or with custom directory
284+
// vespera::export_app!(ThirdApp, dir = "api");
285+
```
286+
287+
**Parent app:**
288+
```rust
289+
// src/main.rs
290+
use vespera::vespera;
291+
292+
let app = vespera!(
293+
openapi = "openapi.json",
294+
docs_url = "/docs",
295+
merge = [third::ThirdApp] // Merges router AND OpenAPI spec
296+
)
297+
.with_state(app_state);
298+
```
299+
300+
This automatically:
301+
- Merges all routes from child apps into the parent router
302+
- Combines OpenAPI specs (paths, schemas, tags) into a single spec
303+
- Makes Swagger UI show all routes from all apps
304+
254305
---
255306

256307
## Type Mapping

SKILL.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,3 +159,61 @@ npx @apidevtools/swagger-cli validate openapi.json
159159
| `VESPERA_DOCS_URL` | Swagger UI path | none |
160160
| `VESPERA_REDOC_URL` | ReDoc path | none |
161161
| `VESPERA_SERVER_URL` | Server URL | `http://localhost:3000` |
162+
163+
---
164+
165+
## Merging Multiple Vespera Apps
166+
167+
Combine routes and OpenAPI specs from multiple apps at compile time.
168+
169+
### export_app! Macro
170+
171+
Export an app for merging:
172+
173+
```rust
174+
// Child crate (e.g., third/src/lib.rs)
175+
mod routes;
176+
177+
// Basic - scans "routes" folder by default
178+
vespera::export_app!(ThirdApp);
179+
180+
// Custom directory
181+
vespera::export_app!(ThirdApp, dir = "api");
182+
```
183+
184+
Generates:
185+
- `ThirdApp::OPENAPI_SPEC: &'static str` - OpenAPI JSON
186+
- `ThirdApp::router() -> Router` - Axum router
187+
188+
### merge Parameter
189+
190+
Merge child apps in parent:
191+
192+
```rust
193+
let app = vespera!(
194+
openapi = "openapi.json",
195+
docs_url = "/docs",
196+
merge = [third::ThirdApp, other::OtherApp]
197+
)
198+
.with_state(state);
199+
```
200+
201+
**What happens:**
202+
1. Child routers merged into parent router
203+
2. OpenAPI specs merged (paths, schemas, tags)
204+
3. Swagger UI shows all routes
205+
206+
### How It Works (Compile-Time)
207+
208+
```
209+
Child compilation (export_app!):
210+
1. Scan routes/ folder
211+
2. Generate OpenAPI spec
212+
3. Write to target/vespera/{Name}.openapi.json
213+
214+
Parent compilation (vespera! with merge):
215+
1. Generate parent OpenAPI spec
216+
2. Read child specs from target/vespera/
217+
3. Merge all specs together
218+
4. Write merged openapi.json
219+
```

crates/vespera/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,6 @@ vespera_core = { workspace = true }
1515
vespera_macro = { workspace = true }
1616
axum = "0.8"
1717
axum-extra = { version = "0.12", optional = true }
18+
serde_json = "1"
19+
tower-layer = "0.3"
20+
tower-service = "0.3"

crates/vespera/src/lib.rs

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,14 @@ pub mod openapi {
1616
pub use vespera_core::openapi::*;
1717
}
1818

19+
// Re-export OpenApi directly for convenience (used by merge feature)
20+
pub use vespera_core::openapi::OpenApi;
21+
1922
// Re-export macros from vespera_macro
20-
pub use vespera_macro::{Schema, route, vespera};
23+
pub use vespera_macro::{Schema, export_app, route, vespera};
24+
25+
// Re-export serde_json for merge feature (runtime spec merging)
26+
pub use serde_json;
2127

2228
// Re-export axum for convenience
2329
pub mod axum {
@@ -27,3 +33,71 @@ pub mod axum {
2733
pub mod axum_extra {
2834
pub use axum_extra::*;
2935
}
36+
37+
/// A router wrapper that defers merging until `with_state()` is called.
38+
///
39+
/// This is necessary because in Axum, routers can only be merged when they have
40+
/// the same state type. By deferring the merge, we ensure that:
41+
/// 1. The base router's `.with_state()` is called first, converting it to `Router<()>`
42+
/// 2. Then the child routers (also `Router<()>`) are merged
43+
///
44+
/// This wrapper is returned by `vespera!()` when the `merge` parameter is used.
45+
pub struct VesperaRouter<S>
46+
where
47+
S: Clone + Send + Sync + 'static,
48+
{
49+
base: axum::Router<S>,
50+
/// Routers to merge after `with_state()` is called
51+
merge_fns: Vec<fn() -> axum::Router<()>>,
52+
}
53+
54+
impl<S> VesperaRouter<S>
55+
where
56+
S: Clone + Send + Sync + 'static,
57+
{
58+
/// Create a new VesperaRouter with a base router and routers to merge
59+
pub fn new(base: axum::Router<S>, merge_fns: Vec<fn() -> axum::Router<()>>) -> Self {
60+
Self { base, merge_fns }
61+
}
62+
63+
/// Provide the state for the router and merge all child routers.
64+
///
65+
/// This is equivalent to calling `Router::with_state()` and then merging
66+
/// all the child routers.
67+
///
68+
/// After calling `with_state()`, the router's state type becomes `()` because
69+
/// the state has been provided. Child routers (also `Router<()>`) can then be merged.
70+
pub fn with_state(self, state: S) -> axum::Router<()> {
71+
// First, apply the state to convert Router<S> to Router<()>
72+
let mut router: axum::Router<()> = self.base.with_state(state);
73+
74+
// Then merge all child routers (they are Router<()> which can be merged
75+
// into Router<()> without issues)
76+
for merge_fn in self.merge_fns {
77+
router = router.merge(merge_fn());
78+
}
79+
80+
router
81+
}
82+
83+
/// Add a layer to the router.
84+
pub fn layer<L>(self, layer: L) -> Self
85+
where
86+
L: tower_layer::Layer<axum::routing::Route> + Clone + Send + Sync + 'static,
87+
L::Service: tower_service::Service<axum::extract::Request> + Clone + Send + Sync + 'static,
88+
<L::Service as tower_service::Service<axum::extract::Request>>::Response:
89+
axum::response::IntoResponse + 'static,
90+
<L::Service as tower_service::Service<axum::extract::Request>>::Error:
91+
Into<std::convert::Infallible> + 'static,
92+
<L::Service as tower_service::Service<axum::extract::Request>>::Future: Send + 'static,
93+
{
94+
Self {
95+
base: self.base.layer(layer),
96+
merge_fns: self.merge_fns,
97+
}
98+
}
99+
}
100+
101+
// Re-export tower_layer and tower_service for the layer method
102+
pub use tower_layer;
103+
pub use tower_service;

0 commit comments

Comments
 (0)