Skip to content

Commit e690e92

Browse files
committed
controllers/krate/downloads: Add versions include mode
1 parent 1164440 commit e690e92

File tree

2 files changed

+105
-8
lines changed

2 files changed

+105
-8
lines changed

src/controllers/krate/downloads.rs

Lines changed: 96 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,35 @@
55
66
use crate::app::AppState;
77
use crate::controllers::krate::CratePath;
8-
use crate::models::{Version, VersionDownload};
8+
use crate::models::{User, Version, VersionDownload, VersionOwnerAction};
99
use crate::schema::{version_downloads, versions};
10-
use crate::util::errors::AppResult;
11-
use crate::views::EncodableVersionDownload;
10+
use crate::util::errors::{bad_request, AppResult, BoxedAppError};
11+
use crate::views::{EncodableVersion, EncodableVersionDownload};
12+
use axum::extract::FromRequestParts;
13+
use axum_extra::extract::Query;
1214
use axum_extra::json;
1315
use axum_extra::response::ErasedJson;
16+
use crates_io_database::schema::users;
1417
use crates_io_diesel_helpers::to_char;
1518
use diesel::prelude::*;
1619
use diesel_async::RunQueryDsl;
20+
use futures_util::FutureExt;
1721
use std::cmp;
22+
use std::str::FromStr;
23+
24+
#[derive(Debug, Deserialize, FromRequestParts, utoipa::IntoParams)]
25+
#[from_request(via(Query))]
26+
#[into_params(parameter_in = Query)]
27+
pub struct DownloadsQueryParams {
28+
/// Additional data to include in the response.
29+
///
30+
/// Valid values: `versions`.
31+
///
32+
/// Defaults to no additional data.
33+
///
34+
/// This parameter expects a comma-separated list of values.
35+
include: Option<String>,
36+
}
1837

1938
/// Get the download counts for a crate.
2039
///
@@ -23,12 +42,16 @@ use std::cmp;
2342
#[utoipa::path(
2443
get,
2544
path = "/api/v1/crates/{name}/downloads",
26-
params(CratePath),
45+
params(CratePath, DownloadsQueryParams),
2746
tag = "crates",
2847
responses((status = 200, description = "Successful Response")),
2948
)]
3049

31-
pub async fn get_crate_downloads(state: AppState, path: CratePath) -> AppResult<ErasedJson> {
50+
pub async fn get_crate_downloads(
51+
state: AppState,
52+
path: CratePath,
53+
params: DownloadsQueryParams,
54+
) -> AppResult<ErasedJson> {
3255
let mut conn = state.db_read().await?;
3356

3457
use diesel::dsl::*;
@@ -43,9 +66,10 @@ pub async fn get_crate_downloads(state: AppState, path: CratePath) -> AppResult<
4366
.await?;
4467

4568
versions.sort_by_cached_key(|version| cmp::Reverse(semver::Version::parse(&version.num).ok()));
46-
let (latest_five, rest) = versions.split_at(cmp::min(5, versions.len()));
69+
let total = versions.len();
70+
let (latest_five, rest) = versions.split_at_mut(cmp::min(5, total));
4771

48-
let downloads = VersionDownload::belonging_to(latest_five)
72+
let downloads = VersionDownload::belonging_to(&latest_five[..])
4973
.filter(version_downloads::date.gt(date(now - 90.days())))
5074
.order((
5175
version_downloads::date.asc(),
@@ -58,7 +82,7 @@ pub async fn get_crate_downloads(state: AppState, path: CratePath) -> AppResult<
5882
.collect::<Vec<EncodableVersionDownload>>();
5983

6084
let sum_downloads = sql::<BigInt>("SUM(version_downloads.downloads)");
61-
let extra: Vec<ExtraDownload> = VersionDownload::belonging_to(rest)
85+
let extra: Vec<ExtraDownload> = VersionDownload::belonging_to(&rest[..])
6286
.select((
6387
to_char(version_downloads::date, "YYYY-MM-DD"),
6488
sum_downloads,
@@ -75,10 +99,74 @@ pub async fn get_crate_downloads(state: AppState, path: CratePath) -> AppResult<
7599
downloads: i64,
76100
}
77101

102+
let include = params
103+
.include
104+
.as_ref()
105+
.map(|mode| ShowIncludeMode::from_str(mode))
106+
.transpose()?
107+
.unwrap_or_default();
108+
109+
if include.versions {
110+
latest_five.sort_unstable_by_key(|it| cmp::Reverse(it.id));
111+
let ids = latest_five.iter().map(|it| it.id);
112+
let versions = latest_five.iter().collect::<Vec<_>>();
113+
let (id_and_publishers, actions) = tokio::try_join!(
114+
versions::table
115+
.left_join(users::table)
116+
.select((versions::id, Option::<User>::as_select()))
117+
.filter(versions::id.eq_any(ids))
118+
.order_by(versions::id.desc())
119+
.load::<(i32, Option<User>)>(&mut conn)
120+
.boxed(),
121+
VersionOwnerAction::for_versions(&mut conn, &versions).boxed()
122+
)?;
123+
let publishers = id_and_publishers.into_iter().map(|(_, pb)| pb);
124+
let versions = versions
125+
.into_iter()
126+
.cloned()
127+
.zip(publishers)
128+
.zip(actions)
129+
.map(|((v, pb), actions)| EncodableVersion::from(v, &path.name, pb, actions))
130+
.collect::<Vec<_>>();
131+
132+
return Ok(json!({
133+
"version_downloads": downloads,
134+
"versions": versions,
135+
"meta": {
136+
"extra_downloads": extra,
137+
},
138+
}));
139+
}
140+
78141
Ok(json!({
79142
"version_downloads": downloads,
80143
"meta": {
81144
"extra_downloads": extra,
82145
},
83146
}))
84147
}
148+
149+
#[derive(Debug, Default)]
150+
struct ShowIncludeMode {
151+
versions: bool,
152+
}
153+
154+
impl ShowIncludeMode {
155+
const INVALID_COMPONENT: &'static str = "invalid component for ?include= (expected 'versions')";
156+
}
157+
158+
impl FromStr for ShowIncludeMode {
159+
type Err = BoxedAppError;
160+
161+
fn from_str(s: &str) -> Result<Self, Self::Err> {
162+
let mut mode = Self { versions: false };
163+
for component in s.split(',') {
164+
match component {
165+
"" => {}
166+
"versions" => mode.versions = true,
167+
_ => return Err(bad_request(Self::INVALID_COMPONENT)),
168+
}
169+
}
170+
Ok(mode)
171+
}
172+
}

src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,15 @@ expression: response.json()
564564
"schema": {
565565
"type": "string"
566566
}
567+
},
568+
{
569+
"description": "Additional data to include in the response.\n\nValid values: `versions`.\n\nDefaults to no additional data.\n\nThis parameter expects a comma-separated list of values.",
570+
"in": "query",
571+
"name": "include",
572+
"required": false,
573+
"schema": {
574+
"type": "string"
575+
}
567576
}
568577
],
569578
"responses": {

0 commit comments

Comments
 (0)