Skip to content

Commit 78a8423

Browse files
chore: fix openapi spec generation
Signed-off-by: Henry <mail@henrygressmann.de>
1 parent 157cae8 commit 78a8423

File tree

12 files changed

+181
-137
lines changed

12 files changed

+181
-137
lines changed

Cargo.lock

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

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ tower-http={version="0.6", default-features=false, features=[
5858
"set-header",
5959
]}
6060
tower_governor={version="0.8", default-features=false, features=["axum"]}
61-
aide={version="0.16.0-alpha.1", default-features=false, features=[
61+
aide={version="0.16.0-alpha.2", default-features=false, features=[
6262
"axum",
6363
"axum-json",
6464
"axum-query",
@@ -72,7 +72,7 @@ aide={version="0.16.0-alpha.1", default-features=false, features=[
7272
schemars={version="1.2", features=["derive", "chrono04"]}
7373

7474
ua-parser="0.2"
75-
rust-embed={version="8.9", features=["mime-guess"]}
75+
rust-embed={version="8.11", features=["mime-guess"]}
7676
reqwest={version="0.13", default-features=false, features=["json", "stream", "charset", "rustls"]}
7777

7878
# database

src/web/mod.rs

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ impl Deref for RouterState {
4646
}
4747
}
4848

49-
pub fn router(app: Arc<Liwan>, events: Sender<Event>) -> Result<axum::Router<()>> {
49+
pub fn router(app: Arc<Liwan>, events: Sender<Event>) -> Result<(axum::Router<()>, openapi::OpenApi)> {
5050
let mut api = openapi::OpenApi {
5151
info: openapi::Info { title: "Liwan API".to_string(), ..Default::default() },
5252
..openapi::OpenApi::default()
@@ -89,7 +89,30 @@ pub fn router(app: Arc<Liwan>, events: Sender<Event>) -> Result<axum::Router<()>
8989
.with_state(RouterState { app: app.clone(), events })
9090
.finish_api(&mut api);
9191

92-
Ok(router)
92+
Ok((router, api))
93+
}
94+
95+
#[cfg(debug_assertions)]
96+
fn save_spec(spec: openapi::OpenApi) -> Result<()> {
97+
use std::path::Path;
98+
99+
let path = Path::new("./web/src/api/dashboard.ts");
100+
if path.exists() {
101+
let spec = serde_json::to_string(&spec)?
102+
.replace(r#""servers":[],"#, "") // fets doesn't work with an empty servers array
103+
.replace("; charset=utf-8", "") // fets doesn't detect the json content type correctly
104+
.replace(r#""format":"int64","#, ""); // fets uses bigint for int64
105+
106+
// check if the spec has changed
107+
let old_spec = std::fs::read_to_string(path)?;
108+
if old_spec == format!("export default {spec} as const;\n") {
109+
return Ok(());
110+
}
111+
112+
tracing::info!("API has changed, updating the openapi spec...");
113+
std::fs::write(path, format!("export default {spec} as const;\n"))?;
114+
}
115+
Ok(())
93116
}
94117

95118
pub async fn start_webserver(app: Arc<Liwan>, events: Sender<Event>) -> Result<()> {
@@ -106,7 +129,11 @@ pub async fn start_webserver(app: Arc<Liwan>, events: Sender<Event>) -> Result<(
106129
}
107130

108131
let router = router(app.clone(), events)?;
132+
133+
#[cfg(debug_assertions)]
134+
save_spec(router.1)?;
135+
109136
let listener = tokio::net::TcpListener::bind(("0.0.0.0", app.config.port)).await.unwrap();
110-
let service = router.into_make_service_with_connect_info::<SocketAddr>();
137+
let service = router.0.into_make_service_with_connect_info::<SocketAddr>();
111138
axum::serve(listener, service).await.context("server exited unexpectedly")
112139
}

src/web/routes/admin.rs

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
use aide::axum::{ApiRouter, IntoApiResponse, routing::*};
1+
use aide::{
2+
UseApi,
3+
axum::{ApiRouter, IntoApiResponse, routing::*},
4+
};
25
use axum::{
36
Json,
47
extract::{Path, State},
@@ -147,7 +150,10 @@ struct EntitiesResponse {
147150
entities: Vec<EntityResponse>,
148151
}
149152

150-
async fn get_users(app: State<RouterState>, SessionUser(user): SessionUser) -> ApiResult<impl IntoApiResponse> {
153+
async fn get_users(
154+
app: State<RouterState>,
155+
SessionUser(user): SessionUser,
156+
) -> ApiResult<UseApi<impl IntoApiResponse, Json<UsersResponse>>> {
151157
if user.role != UserRole::Admin {
152158
http_bail!(StatusCode::FORBIDDEN, "Forbidden")
153159
}
@@ -160,7 +166,7 @@ async fn get_users(app: State<RouterState>, SessionUser(user): SessionUser) -> A
160166
.map(|u| UserResponse { username: u.username.clone(), role: u.role, projects: u.projects.clone() })
161167
.collect();
162168

163-
Ok(([(http::header::CACHE_CONTROL, "private")], Json(UsersResponse { users })))
169+
Ok(([(http::header::CACHE_CONTROL, "private")], Json(UsersResponse { users })).into())
164170
}
165171

166172
async fn update_user(
@@ -295,7 +301,7 @@ async fn project_update_handler(
295301
async fn projects_handler(
296302
app: State<RouterState>,
297303
MaybeExtract(user): MaybeExtract<SessionUser>,
298-
) -> ApiResult<impl IntoApiResponse> {
304+
) -> ApiResult<UseApi<impl IntoApiResponse, Json<ProjectsResponse>>> {
299305
let projects = app.projects.all().http_err("Failed to get projects", StatusCode::INTERNAL_SERVER_ERROR)?;
300306
let projects: Vec<Project> = projects.into_iter().filter(|p| can_access_project(p, user.as_ref())).collect();
301307

@@ -315,14 +321,14 @@ async fn projects_handler(
315321
});
316322
}
317323

318-
Ok(([(http::header::CACHE_CONTROL, "private")], Json(ProjectsResponse { projects: resp })))
324+
Ok(([(http::header::CACHE_CONTROL, "private")], Json(ProjectsResponse { projects: resp })).into())
319325
}
320326

321327
async fn project_handler(
322328
app: State<RouterState>,
323329
MaybeExtract(user): MaybeExtract<SessionUser>,
324330
Path(project_id): Path<String>,
325-
) -> ApiResult<impl IntoApiResponse> {
331+
) -> ApiResult<UseApi<impl IntoApiResponse, Json<ProjectResponse>>> {
326332
let project = app.projects.get(&project_id).http_status(StatusCode::NOT_FOUND)?;
327333
if !can_access_project(&project, user.as_ref()) {
328334
return Err(StatusCode::NOT_FOUND.into());
@@ -341,7 +347,7 @@ async fn project_handler(
341347
public: project.public,
342348
});
343349

344-
Ok(([(http::header::CACHE_CONTROL, "private")], resp))
350+
Ok(([(http::header::CACHE_CONTROL, "private")], resp).into())
345351
}
346352

347353
async fn project_delete_handler(
@@ -358,7 +364,10 @@ async fn project_delete_handler(
358364
Ok(empty_response())
359365
}
360366

361-
async fn entities_handler(app: State<RouterState>, SessionUser(user): SessionUser) -> ApiResult<impl IntoApiResponse> {
367+
async fn entities_handler(
368+
app: State<RouterState>,
369+
SessionUser(user): SessionUser,
370+
) -> ApiResult<UseApi<impl IntoApiResponse, Json<EntitiesResponse>>> {
362371
if user.role != UserRole::Admin {
363372
http_bail!(StatusCode::FORBIDDEN, "Forbidden")
364373
}
@@ -384,7 +393,7 @@ async fn entities_handler(app: State<RouterState>, SessionUser(user): SessionUse
384393
});
385394
}
386395

387-
Ok(([(http::header::CACHE_CONTROL, "private")], Json(EntitiesResponse { entities: resp })))
396+
Ok(([(http::header::CACHE_CONTROL, "private")], Json(EntitiesResponse { entities: resp })).into())
388397
}
389398

390399
async fn entity_create_handler(

src/web/routes/auth.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
use aide::axum::{ApiRouter, IntoApiResponse, routing::*};
1+
use aide::{
2+
UseApi,
3+
axum::{ApiRouter, IntoApiResponse, routing::*},
4+
};
25
use anyhow::Context;
36
use axum::{Json, extract::State};
47
use axum_extra::extract::CookieJar;
@@ -58,8 +61,8 @@ pub struct MeResponse {
5861
pub role: UserRole,
5962
}
6063

61-
async fn me(SessionUser(user): SessionUser) -> impl IntoApiResponse {
62-
([(header::CACHE_CONTROL, "private")], Json(MeResponse { username: user.username, role: user.role }))
64+
async fn me(SessionUser(user): SessionUser) -> UseApi<impl IntoApiResponse, Json<MeResponse>> {
65+
([(header::CACHE_CONTROL, "private")], Json(MeResponse { username: user.username, role: user.role })).into()
6366
}
6467

6568
async fn setup(app: State<RouterState>, Json(params): Json<SetupRequest>) -> ApiResult<impl IntoApiResponse> {

src/web/webext.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use std::task::{Context, Poll};
99

1010
use crate::web::Files;
1111
use aide::axum::IntoApiResponse;
12-
use aide::{OperationIo, OperationOutput};
12+
use aide::{OperationInput, OperationOutput};
1313
use axum::body::Body;
1414
use axum::extract::{ConnectInfo, FromRequestParts, Request};
1515
use axum::response::IntoResponse;
@@ -209,8 +209,9 @@ macro_rules! http_bail {
209209
}
210210
pub(crate) use http_bail;
211211

212-
#[derive(Debug, Copy, Clone, OperationIo)]
212+
#[derive(Debug, Copy, Clone)]
213213
pub struct ClientIp(pub Option<IpAddr>);
214+
impl OperationInput for ClientIp {}
214215

215216
impl<S: Send + Sync> FromRequestParts<S> for ClientIp {
216217
type Rejection = Infallible;

tests/common/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ pub struct TestClient {
2323

2424
impl TestClient {
2525
pub fn new(app: Arc<Liwan>, events: std::sync::mpsc::Sender<Event>) -> Self {
26-
let router = liwan::web::router(app, events).unwrap();
26+
let (router, _) = liwan::web::router(app, events).unwrap();
2727
let server = TestServer::new(router).unwrap();
2828
Self { server }
2929
}

web/bun.lock

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

web/package.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"@radix-ui/react-accordion": "^1.2.12",
1818
"@radix-ui/react-dialog": "^1.1.15",
1919
"@radix-ui/react-tabs": "^1.1.13",
20-
"@tanstack/react-query": "^5.90.16",
20+
"@tanstack/react-query": "^5.90.18",
2121
"d3-array": "^3.2.4",
2222
"d3-axis": "^3.0.0",
2323
"d3-ease": "^3.0.1",
@@ -41,25 +41,25 @@
4141
"devDependencies": {
4242
"@astrojs/check": "^0.9.6",
4343
"@biomejs/biome": "2.3.11",
44-
"@types/bun": "^1.3.5",
44+
"@types/bun": "^1.3.6",
4545
"@types/d3-array": "^3.2.2",
4646
"@types/d3-axis": "^3.0.6",
4747
"@types/d3-ease": "^3.0.2",
4848
"@types/d3-geo": "^3.1.0",
4949
"@types/d3-scale": "^4.0.9",
5050
"@types/d3-selection": "^3.0.11",
51-
"@types/d3-shape": "^3.1.7",
51+
"@types/d3-shape": "^3.1.8",
5252
"@types/d3-transition": "^3.0.9",
5353
"@types/d3-zoom": "^3.0.8",
5454
"@types/react": "^19.2.8",
5555
"@types/react-dom": "^19.2.3",
5656
"@types/topojson-client": "^3.1.5",
5757
"@types/topojson-specification": "^1.0.5",
58-
"astro": "5.16.8",
58+
"astro": "5.16.11",
5959
"rollup-plugin-license": "^3.6.0",
6060
"typescript": "^5.9.3"
6161
},
62-
"packageManager": "bun@1.3.5",
62+
"packageManager": "bun@1.3.6",
6363
"trustedDependencies": [
6464
"@biomejs/biome",
6565
"esbuild"

0 commit comments

Comments
 (0)