Skip to content

Commit 466ec44

Browse files
committed
improve openapi
1 parent f46f606 commit 466ec44

File tree

7 files changed

+160
-40
lines changed

7 files changed

+160
-40
lines changed

src/app/cache.rs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
use std::sync::Arc;
22

3-
use ohkami::{IntoResponse, Json, fang::Context};
3+
use ohkami::{Json, fang::Context};
44
use tracing::debug;
55

6-
use crate::{app::AppState, error::Error};
6+
use crate::{
7+
app::{AppState, skin::SkinQuery},
8+
error::Error,
9+
};
710

811
#[inline(always)]
912
/// Represent GET method to return list of skins
10-
pub async fn cache_handler<'a>(
11-
Context(state): Context<'a, Arc<AppState>>
12-
) -> Result<impl IntoResponse + 'a, Error> {
13+
pub async fn cache_handler(
14+
Context(state): Context<'_, Arc<AppState>>
15+
) -> Result<Json<Vec<SkinQuery>>, Error> {
1316
Ok(Json(
1417
state
1518
.cache

src/app/lock.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
use std::sync::Arc;
22

3-
use ohkami::{IntoResponse, Json, fang::Context};
3+
use ohkami::{Json, fang::Context};
44

55
use crate::{app::AppState, error::Error};
66

77
#[inline(always)]
88
/// Represent GET method to return list of skins
9-
pub async fn lock_handler<'a>(
10-
Context(state): Context<'a, Arc<AppState>>
11-
) -> Result<impl IntoResponse + 'a, Error> {
9+
pub async fn lock_handler(
10+
Context(state): Context<'_, Arc<AppState>>
11+
) -> Result<Json<Vec<String>>, Error> {
1212
Ok(Json(
1313
state
1414
.lock

src/app/logger.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
use ohkami::{FangAction, Request, Response};
2+
use tracing::{info, instrument};
23

34
#[derive(Clone)]
45
pub struct LogRequest;
56
impl FangAction for LogRequest {
67
#[inline(always)]
8+
#[instrument(skip_all, level="info", fields(ip=%req.ip, type=%req.method, user_agent=?req.headers.user_agent(), path=%req.path))]
79
async fn fore<'a>(
810
&'a self,
911
req: &'a mut Request,
1012
) -> Result<(), Response> {
11-
tracing::debug!("\nGot request: {req:#?}");
13+
info!("got req");
1214
Ok(())
1315
}
1416
}

src/app/png.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,25 @@
11
use std::borrow::Cow;
22

3-
use ohkami::{claw::content::IntoContent, openapi};
3+
use ohkami::{
4+
claw::content::IntoContent,
5+
openapi::{self, Schema},
6+
};
47

58
/// Represents a PNG image content.
9+
#[derive(Debug, Schema)]
10+
#[openapi(component)]
611
pub struct Png(pub Vec<u8>);
712

813
impl IntoContent for Png {
914
const CONTENT_TYPE: &'static str = "image/png";
15+
1016
#[inline(always)]
1117
fn into_content(self) -> Result<std::borrow::Cow<'static, [u8]>, impl std::fmt::Display> {
1218
Result::<_, std::convert::Infallible>::Ok(Cow::Owned(self.0))
1319
}
20+
1421
#[inline(always)]
1522
fn openapi_responsebody() -> impl Into<openapi::schema::SchemaRef> {
16-
openapi::schema::SchemaRef::Reference("png")
23+
openapi::string().format("binary")
1724
}
1825
}

src/app/skin.rs

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
use std::{fmt::Display, sync::Arc};
22

33
use ohkami::{
4-
IntoResponse, Ohkami, Query, Route,
5-
claw::status::Created,
4+
Ohkami, Query, Route,
5+
claw::status::OK,
66
fang::Context,
7-
openapi::{self, Schema},
7+
openapi::{self, Schema, SchemaRef, operation},
88
serde::Deserialize,
99
};
1010
use serde::Serialize;
@@ -26,17 +26,47 @@ pub fn skin_router() -> Ohkami {
2626
))
2727
}
2828

29-
#[derive(Debug, Clone, Deserialize, Schema, Hash, PartialEq, Eq, Serialize)]
29+
#[derive(Debug, Clone, Deserialize, Hash, PartialEq, Eq, Serialize)]
3030
/// Base/Default/Main skin query
3131
pub struct SkinQuery {
32-
/// Replace all whitespaces to `_`
32+
/// **note**: Replace all whitespaces to `_`
3333
pub name: String,
3434
/// DDNet value
3535
pub body: Option<u32>,
3636
/// DDNet value
3737
pub feet: Option<u32>,
3838
}
3939

40+
impl Schema for SkinQuery {
41+
fn schema() -> impl Into<SchemaRef> {
42+
openapi::component(
43+
"SkinQuery",
44+
openapi::object()
45+
.property(
46+
"name",
47+
openapi::string()
48+
.format("a-zA-Z0-9_")
49+
.description("Skin name")
50+
.example("zzz"),
51+
)
52+
.optional(
53+
"body",
54+
openapi::string()
55+
.description("DDNet value")
56+
.example("32132114")
57+
.nullable(),
58+
)
59+
.optional(
60+
"feet",
61+
openapi::string()
62+
.description("DDNet value")
63+
.example("32132114")
64+
.nullable(),
65+
),
66+
)
67+
}
68+
}
69+
4070
impl Display for SkinQuery {
4171
fn fmt(
4272
&self,
@@ -51,13 +81,16 @@ impl Display for SkinQuery {
5181
}
5282

5383
#[inline(always)]
54-
#[instrument(skip(state))]
84+
#[instrument(skip_all, level="info", fields(name=%query.name, body=?query.body, feet=?query.feet))]
85+
#[operation({
86+
summary: "Get rendered skin image",
87+
})]
5588
/// Represent GET method to return a builded skin by query
56-
async fn skin_handler<'a>(
57-
Context(state): Context<'a, Arc<AppState>>,
89+
async fn skin_handler(
90+
Context(state): Context<'_, Arc<AppState>>,
5891
Query(query): Query<SkinQuery>,
59-
) -> Result<impl IntoResponse, Error> {
60-
Ok(Created(Png(match state.cache.get(&query).await {
92+
) -> Result<OK<Png>, Error> {
93+
Ok(OK(Png(match state.cache.get(&query).await {
6194
Ok(Some(e)) => e.to_vec(),
6295
_ => state.lock.get(state.cache.clone(), query).await?,
6396
})))

src/cache.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ impl CacheStore {
6969
match self.store.get(&query) {
7070
Some(x) => {
7171
if x.value().is_acutal() {
72-
info!(query=%query, "Take from cache");
72+
info!("Take from cache");
7373
Ok(Some(x.value().data.clone()))
7474
} else {
7575
Ok(None)

src/error.rs

Lines changed: 92 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,22 @@
11
use std::path::PathBuf;
22

3-
use ohkami::{IntoResponse, Response};
3+
use ohkami::{
4+
IntoResponse, Response,
5+
openapi::{self, Schema},
6+
serde::Serialize,
7+
};
48
use reqwest::header::ToStrError;
59
use thiserror::Error;
610
use tokio::{io, task::JoinError};
711
use tracing::{error, instrument};
812

13+
#[derive(Serialize, Schema)]
14+
#[openapi(component)]
15+
struct ErrorResponse {
16+
code: u16,
17+
message: String,
18+
}
19+
920
#[derive(Debug, Error)]
1021
pub enum Error {
1122
#[error("I/O error")]
@@ -36,24 +47,88 @@ impl IntoResponse for Error {
3647
#[instrument]
3748
fn into_response(self) -> Response {
3849
match self {
39-
Error::Io(_) => Response::InternalServerError().with_text("Skin not found"),
40-
Error::Tee(_) => Response::InternalServerError().with_text("Fail to render uv"),
41-
Error::QueryNameNotFound => {
42-
Response::BadRequest().with_text("Query expected name, but got none")
50+
Error::QueryNameNotFound => Response::BadRequest().with_json(ErrorResponse {
51+
code: 400,
52+
message: "Query expected name, but got none".to_string(),
53+
}),
54+
Error::Io(e) => {
55+
tracing::error!("I/O error: {}", e);
56+
Response::InternalServerError().with_json(ErrorResponse {
57+
code: 500,
58+
message: "Skin not found".to_string(),
59+
})
60+
}
61+
Error::Tee(e) => {
62+
tracing::error!("Tee error: {}", e);
63+
Response::InternalServerError().with_json(ErrorResponse {
64+
code: 500,
65+
message: "Failed to render UV".to_string(),
66+
})
67+
}
68+
Error::Reqwest(e) => {
69+
tracing::error!("Reqwest error: {}", e);
70+
Response::InternalServerError().with_json(ErrorResponse {
71+
code: 500,
72+
message: "External request failed".to_string(),
73+
})
74+
}
75+
Error::ToStrError(e) => {
76+
tracing::error!("Header conversion error: {}", e);
77+
Response::InternalServerError().with_json(ErrorResponse {
78+
code: 500,
79+
message: "Invalid header value".to_string(),
80+
})
81+
}
82+
Error::TaskJoin(e) => {
83+
tracing::error!("Task join error: {}", e);
84+
Response::InternalServerError().with_json(ErrorResponse {
85+
code: 500,
86+
message: "Background task failed".to_string(),
87+
})
88+
}
89+
Error::Json(e) => {
90+
tracing::error!("JSON error: {}", e);
91+
Response::InternalServerError().with_json(ErrorResponse {
92+
code: 500,
93+
message: "JSON processing failed".to_string(),
94+
})
4395
}
44-
Error::Reqwest(_) => Response::InternalServerError(),
45-
Error::ToStrError(_) => Response::InternalServerError(),
46-
Error::DownloadFailed {
47-
name: _,
48-
error: _,
49-
} => todo!(),
50-
Error::TaskJoin(_join_error) => Response::InternalServerError(),
51-
Error::Json(_error) => Response::InternalServerError(),
5296
Error::SaveFailed {
53-
path: _,
54-
name: _,
55-
error: _,
56-
} => Response::InternalServerError(),
97+
path,
98+
name,
99+
error,
100+
} => {
101+
tracing::error!("Save failed: {} at {:?}: {}", name, path, error);
102+
Response::InternalServerError().with_json(ErrorResponse {
103+
code: 500,
104+
message: format!("Failed to save {}", name),
105+
})
106+
}
107+
Error::DownloadFailed {
108+
name,
109+
error,
110+
} => {
111+
tracing::error!("Download failed: {}: {}", name, error);
112+
Response::InternalServerError().with_json(ErrorResponse {
113+
code: 500,
114+
message: format!("Failed to download {}", name),
115+
})
116+
}
57117
}
58118
}
119+
120+
fn openapi_responses() -> openapi::Responses {
121+
openapi::Responses::new([
122+
(
123+
400,
124+
openapi::Response::when("Bad request - invalid query parameters")
125+
.content("application/json", <ErrorResponse as Schema>::schema()),
126+
),
127+
(
128+
500,
129+
openapi::Response::when("Internal server error")
130+
.content("application/json", <ErrorResponse as Schema>::schema()),
131+
),
132+
])
133+
}
59134
}

0 commit comments

Comments
 (0)