Skip to content

Commit ec98198

Browse files
authored
Merge pull request #10720 from Turbo87/openapi-crates
Improve OpenAPI documentation for crates API endpoints
2 parents 57a418f + e99762b commit ec98198

File tree

5 files changed

+578
-49
lines changed

5 files changed

+578
-49
lines changed

src/controllers/krate/downloads.rs

Lines changed: 45 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,9 @@ use crate::models::{User, Version as FullVersion, VersionDownload, VersionOwnerA
1010
use crate::schema::{version_downloads, version_owner_actions, versions};
1111
use crate::util::errors::{AppResult, BoxedAppError, bad_request};
1212
use crate::views::{EncodableVersion, EncodableVersionDownload};
13+
use axum::Json;
1314
use axum::extract::FromRequestParts;
1415
use axum_extra::extract::Query;
15-
use axum_extra::json;
16-
use axum_extra::response::ErasedJson;
1716
use crates_io_database::schema::users;
1817
use crates_io_diesel_helpers::to_char;
1918
use diesel::prelude::*;
@@ -37,6 +36,37 @@ pub struct DownloadsQueryParams {
3736
include: Option<String>,
3837
}
3938

39+
#[derive(Debug, Serialize, utoipa::ToSchema)]
40+
pub struct DownloadsResponse {
41+
/// The per-day download counts for the last 90 days.
42+
pub version_downloads: Vec<EncodableVersionDownload>,
43+
44+
/// The versions referenced in the download counts, if `?include=versions`
45+
/// was requested.
46+
#[serde(skip_serializing_if = "Option::is_none")]
47+
pub versions: Option<Vec<EncodableVersion>>,
48+
49+
#[schema(inline)]
50+
pub meta: DownloadsMeta,
51+
}
52+
53+
#[derive(Debug, Serialize, utoipa::ToSchema)]
54+
pub struct DownloadsMeta {
55+
#[schema(inline)]
56+
pub extra_downloads: Vec<ExtraDownload>,
57+
}
58+
59+
#[derive(Debug, Serialize, Queryable, utoipa::ToSchema)]
60+
pub struct ExtraDownload {
61+
/// The date this download count is for.
62+
#[schema(example = "2019-12-13")]
63+
date: String,
64+
65+
/// The number of downloads on the given date.
66+
#[schema(example = 123)]
67+
downloads: i64,
68+
}
69+
4070
/// Get the download counts for a crate.
4171
///
4272
/// This includes the per-day downloads for the last 90 days and for the
@@ -46,14 +76,13 @@ pub struct DownloadsQueryParams {
4676
path = "/api/v1/crates/{name}/downloads",
4777
params(CratePath, DownloadsQueryParams),
4878
tag = "crates",
49-
responses((status = 200, description = "Successful Response")),
79+
responses((status = 200, description = "Successful Response", body = inline(DownloadsResponse))),
5080
)]
51-
5281
pub async fn get_crate_downloads(
5382
state: AppState,
5483
path: CratePath,
5584
params: DownloadsQueryParams,
56-
) -> AppResult<ErasedJson> {
85+
) -> AppResult<Json<DownloadsResponse>> {
5786
let mut conn = state.db_read().await?;
5887

5988
use diesel::dsl::*;
@@ -78,7 +107,7 @@ pub async fn get_crate_downloads(
78107
.unwrap_or_default();
79108

80109
let sum_downloads = sql::<BigInt>("SUM(version_downloads.downloads)");
81-
let (downloads, extra, versions_and_publishers, actions) = tokio::try_join!(
110+
let (downloads, extra_downloads, versions_and_publishers, actions) = tokio::try_join!(
82111
VersionDownload::belonging_to(latest_five)
83112
.filter(version_downloads::date.gt(date(now - 90.days())))
84113
.order((
@@ -101,18 +130,12 @@ pub async fn get_crate_downloads(
101130
load_actions(&mut conn, latest_five, include.versions),
102131
)?;
103132

104-
let downloads = downloads
133+
let version_downloads = downloads
105134
.into_iter()
106135
.map(VersionDownload::into)
107136
.collect::<Vec<EncodableVersionDownload>>();
108137

109-
#[derive(Serialize, Queryable)]
110-
struct ExtraDownload {
111-
date: String,
112-
downloads: i64,
113-
}
114-
115-
if include.versions {
138+
let versions = if include.versions {
116139
let versions_and_publishers = versions_and_publishers.grouped_by(latest_five);
117140
let actions = actions.grouped_by(latest_five);
118141
let versions = versions_and_publishers
@@ -125,20 +148,15 @@ pub async fn get_crate_downloads(
125148
})
126149
.collect::<Vec<_>>();
127150

128-
return Ok(json!({
129-
"version_downloads": downloads,
130-
"versions": versions,
131-
"meta": {
132-
"extra_downloads": extra,
133-
},
134-
}));
135-
}
151+
Some(versions)
152+
} else {
153+
None
154+
};
136155

137-
Ok(json!({
138-
"version_downloads": downloads,
139-
"meta": {
140-
"extra_downloads": extra,
141-
},
156+
Ok(Json(DownloadsResponse {
157+
version_downloads,
158+
versions,
159+
meta: DownloadsMeta { extra_downloads },
142160
}))
143161
}
144162

src/controllers/krate/follow.rs

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@ use crate::controllers::krate::CratePath;
77
use crate::models::{Crate, Follow};
88
use crate::schema::*;
99
use crate::util::errors::{AppResult, crate_not_found};
10-
use axum_extra::json;
11-
use axum_extra::response::ErasedJson;
10+
use axum::Json;
1211
use diesel::prelude::*;
1312
use diesel_async::{AsyncPgConnection, RunQueryDsl};
1413
use http::request::Parts;
@@ -74,20 +73,26 @@ pub async fn unfollow_crate(app: AppState, path: CratePath, req: Parts) -> AppRe
7473
Ok(OkResponse::new())
7574
}
7675

76+
#[derive(Debug, Serialize, utoipa::ToSchema)]
77+
pub struct FollowingResponse {
78+
/// Whether the authenticated user is following the crate.
79+
pub following: bool,
80+
}
81+
7782
/// Check if a crate is followed.
7883
#[utoipa::path(
7984
get,
8085
path = "/api/v1/crates/{name}/following",
8186
params(CratePath),
8287
security(("cookie" = [])),
8388
tag = "crates",
84-
responses((status = 200, description = "Successful Response")),
89+
responses((status = 200, description = "Successful Response", body = inline(FollowingResponse))),
8590
)]
8691
pub async fn get_following_crate(
8792
app: AppState,
8893
path: CratePath,
8994
req: Parts,
90-
) -> AppResult<ErasedJson> {
95+
) -> AppResult<Json<FollowingResponse>> {
9196
use diesel::dsl::exists;
9297

9398
let mut conn = app.db_read_prefer_primary().await?;
@@ -98,8 +103,8 @@ pub async fn get_following_crate(
98103

99104
let follow = follow_target(&path.name, &mut conn, user_id).await?;
100105
let following = diesel::select(exists(follows::table.find(follow.id())))
101-
.get_result::<bool>(&mut conn)
106+
.get_result(&mut conn)
102107
.await?;
103108

104-
Ok(json!({ "following": following }))
109+
Ok(Json(FollowingResponse { following }))
105110
}

src/controllers/krate/metadata.rs

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,8 @@ use crate::util::errors::{
1515
AppResult, BoxedAppError, bad_request, crate_not_found, version_not_found,
1616
};
1717
use crate::views::{EncodableCategory, EncodableCrate, EncodableKeyword, EncodableVersion};
18+
use axum::Json;
1819
use axum::extract::{FromRequestParts, Query};
19-
use axum_extra::json;
20-
use axum_extra::response::ErasedJson;
2120
use diesel::prelude::*;
2221
use diesel_async::{AsyncPgConnection, RunQueryDsl};
2322
use futures_util::FutureExt;
@@ -41,6 +40,25 @@ pub struct FindQueryParams {
4140
include: Option<String>,
4241
}
4342

43+
#[derive(Debug, Serialize, utoipa::ToSchema)]
44+
pub struct GetResponse {
45+
/// The crate metadata.
46+
#[serde(rename = "crate")]
47+
krate: EncodableCrate,
48+
49+
/// The versions of the crate.
50+
#[schema(example = json!(null))]
51+
versions: Option<Vec<EncodableVersion>>,
52+
53+
/// The keywords of the crate.
54+
#[schema(example = json!(null))]
55+
keywords: Option<Vec<EncodableKeyword>>,
56+
57+
/// The categories of the crate.
58+
#[schema(example = json!(null))]
59+
categories: Option<Vec<EncodableCategory>>,
60+
}
61+
4462
/// Get crate metadata (for the `new` crate).
4563
///
4664
/// This endpoint works around a small limitation in `axum` and is delegating
@@ -49,9 +67,12 @@ pub struct FindQueryParams {
4967
get,
5068
path = "/api/v1/crates/new",
5169
tag = "crates",
52-
responses((status = 200, description = "Successful Response")),
70+
responses((status = 200, description = "Successful Response", body = inline(GetResponse))),
5371
)]
54-
pub async fn find_new_crate(app: AppState, params: FindQueryParams) -> AppResult<ErasedJson> {
72+
pub async fn find_new_crate(
73+
app: AppState,
74+
params: FindQueryParams,
75+
) -> AppResult<Json<GetResponse>> {
5576
let name = "new".to_string();
5677
find_crate(app, CratePath { name }, params).await
5778
}
@@ -62,13 +83,13 @@ pub async fn find_new_crate(app: AppState, params: FindQueryParams) -> AppResult
6283
path = "/api/v1/crates/{name}",
6384
params(CratePath, FindQueryParams),
6485
tag = "crates",
65-
responses((status = 200, description = "Successful Response")),
86+
responses((status = 200, description = "Successful Response", body = inline(GetResponse))),
6687
)]
6788
pub async fn find_crate(
6889
app: AppState,
6990
path: CratePath,
7091
params: FindQueryParams,
71-
) -> AppResult<ErasedJson> {
92+
) -> AppResult<Json<GetResponse>> {
7293
let mut conn = app.db_read().await?;
7394

7495
let include = params
@@ -186,11 +207,11 @@ pub async fn find_crate(
186207
.collect::<Vec<EncodableCategory>>()
187208
});
188209

189-
Ok(json!({
190-
"crate": encodable_crate,
191-
"versions": encodable_versions,
192-
"keywords": encodable_keywords,
193-
"categories": encodable_cats,
210+
Ok(Json(GetResponse {
211+
krate: encodable_crate,
212+
versions: encodable_versions,
213+
keywords: encodable_keywords,
214+
categories: encodable_cats,
194215
}))
195216
}
196217

0 commit comments

Comments
 (0)