Skip to content

Commit da5f3ad

Browse files
committed
Support multi openapi
1 parent efeeac3 commit da5f3ad

File tree

6 files changed

+1602
-14
lines changed

6 files changed

+1602
-14
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"changes":{"crates/vespera/Cargo.toml":"Patch","crates/vespera_core/Cargo.toml":"Patch","crates/vespera_macro/Cargo.toml":"Patch"},"note":"Support multi openapi","date":"2025-12-09T08:36:05.668321900Z"}

Cargo.lock

Lines changed: 3 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: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,11 @@ let app = vespera!(
232232
openapi = "openapi.json"
233233
);
234234

235+
// Generate multiple OpenAPI JSON files at once
236+
let app = vespera!(
237+
openapi = ["openapi.json", "admin-openapi.json"]
238+
);
239+
235240
// With OpenAPI and Swagger UI
236241
let app = vespera!(
237242
openapi = "openapi.json",
@@ -253,10 +258,12 @@ let app = vespera!(
253258
- **`dir`**: Route folder name (default: `"routes"`)
254259
- You can also specify it directly as a string literal: `vespera!("api")`
255260

256-
- **`openapi`**: OpenAPI JSON file path (optional)
257-
- If specified, an OpenAPI 3.1 spec is generated at compile time and **writes an `openapi.json` file to the specified path**
261+
- **`openapi`**: OpenAPI JSON file path(s) (optional)
262+
- Accepts a single string or an array of strings
263+
- If specified, an OpenAPI 3.1 spec is generated at compile time and **writes an `openapi.json` file to the specified path (or paths)**
258264
- Example: `openapi = "openapi.json"` → creates `openapi.json` file in project root
259265
- Example: `openapi = "docs/api.json"` → creates `docs/api.json` file
266+
- Example: `openapi = ["openapi.json", "docs/admin.json"]` → writes both files
260267

261268
- **`title`**: API title (optional, default: `"API"`)
262269
- Used in the `info.title` field of the OpenAPI document

crates/vespera_macro/src/lib.rs

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ use quote::quote;
1313
use std::path::Path;
1414
use std::sync::{LazyLock, Mutex};
1515
use syn::LitStr;
16+
use syn::bracketed;
1617
use syn::parse::{Parse, ParseStream};
18+
use syn::punctuated::Punctuated;
1719

1820
use crate::collector::collect_metadata;
1921
use crate::metadata::{CollectedMetadata, StructMetadata};
@@ -57,7 +59,7 @@ pub fn derive_schema(input: TokenStream) -> TokenStream {
5759

5860
struct AutoRouterInput {
5961
dir: Option<LitStr>,
60-
openapi: Option<LitStr>,
62+
openapi: Option<Vec<LitStr>>,
6163
title: Option<LitStr>,
6264
version: Option<LitStr>,
6365
docs_url: Option<LitStr>,
@@ -86,8 +88,7 @@ impl Parse for AutoRouterInput {
8688
dir = Some(input.parse()?);
8789
}
8890
"openapi" => {
89-
input.parse::<syn::Token![=]>()?;
90-
openapi = Some(input.parse()?);
91+
openapi = Some(parse_openapi_values(input)?);
9192
}
9293
"docs_url" => {
9394
input.parse::<syn::Token![=]>()?;
@@ -137,7 +138,7 @@ impl Parse for AutoRouterInput {
137138
}),
138139
openapi: openapi.or_else(|| {
139140
std::env::var("VESPERA_OPENAPI")
140-
.map(|f| LitStr::new(&f, Span::call_site()))
141+
.map(|f| vec![LitStr::new(&f, Span::call_site())])
141142
.ok()
142143
}),
143144
title: title.or_else(|| {
@@ -164,6 +165,21 @@ impl Parse for AutoRouterInput {
164165
}
165166
}
166167

168+
fn parse_openapi_values(input: ParseStream) -> syn::Result<Vec<LitStr>> {
169+
input.parse::<syn::Token![=]>()?;
170+
171+
if input.peek(syn::token::Bracket) {
172+
let content;
173+
let _ = bracketed!(content in input);
174+
let entries: Punctuated<LitStr, syn::Token![,]> =
175+
content.parse_terminated(|input| input.parse::<LitStr>(), syn::Token![,])?;
176+
Ok(entries.into_iter().collect())
177+
} else {
178+
let single: LitStr = input.parse()?;
179+
Ok(vec![single])
180+
}
181+
}
182+
167183
#[proc_macro]
168184
pub fn vespera(input: TokenStream) -> TokenStream {
169185
let input = syn::parse_macro_input!(input as AutoRouterInput);
@@ -173,7 +189,12 @@ pub fn vespera(input: TokenStream) -> TokenStream {
173189
.map(|f| f.value())
174190
.unwrap_or_else(|| "routes".to_string());
175191

176-
let openapi_file_name = input.openapi.map(|f| f.value());
192+
let openapi_file_names = input
193+
.openapi
194+
.unwrap_or_default()
195+
.into_iter()
196+
.map(|f| f.value())
197+
.collect::<Vec<_>>();
177198

178199
let title = input.title.map(|t| t.value());
179200
let version = input.version.map(|v| v.value());
@@ -209,7 +230,7 @@ pub fn vespera(input: TokenStream) -> TokenStream {
209230
let mut docs_info = None;
210231
let mut redoc_info = None;
211232

212-
if openapi_file_name.is_some() || docs_url.is_some() || redoc_url.is_some() {
233+
if !openapi_file_names.is_empty() || docs_url.is_some() || redoc_url.is_some() {
213234
// Generate OpenAPI document using collected metadata
214235

215236
// Serialize to JSON
@@ -226,8 +247,18 @@ pub fn vespera(input: TokenStream) -> TokenStream {
226247
.into();
227248
}
228249
};
229-
if let Some(openapi_file_name) = openapi_file_name {
230-
std::fs::write(openapi_file_name, &json_str).unwrap();
250+
for openapi_file_name in &openapi_file_names {
251+
if let Err(e) = std::fs::write(openapi_file_name, &json_str) {
252+
return syn::Error::new(
253+
Span::call_site(),
254+
format!(
255+
"Failed to write OpenAPI document to {}: {}",
256+
openapi_file_name, e
257+
),
258+
)
259+
.to_compile_error()
260+
.into();
261+
}
231262
}
232263
if let Some(docs_url) = docs_url {
233264
docs_info = Some((docs_url, json_str.clone()));

examples/axum-example/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ pub struct TestStruct {
1818
/// Create the application router for testing
1919
pub fn create_app() -> axum::Router {
2020
vespera!(
21-
openapi = "examples/axum-example/openapi.json",
21+
openapi = ["examples/axum-example/openapi.json", "openapi.json"],
2222
docs_url = "/docs",
2323
redoc_url = "/redoc"
2424
)

0 commit comments

Comments
 (0)