66use crate :: app:: AppState ;
77use crate :: controllers:: krate:: CratePath ;
88use crate :: models:: download:: Version ;
9- use crate :: models:: VersionDownload ;
10- use crate :: schema:: { version_downloads, versions} ;
11- use crate :: util:: errors:: AppResult ;
12- use crate :: views:: EncodableVersionDownload ;
9+ use crate :: models:: { User , Version as FullVersion , VersionDownload , VersionOwnerAction } ;
10+ use crate :: schema:: { version_downloads, version_owner_actions, versions} ;
11+ use crate :: util:: errors:: { bad_request, AppResult , BoxedAppError } ;
12+ use crate :: views:: { EncodableVersion , EncodableVersionDownload } ;
13+ use axum:: extract:: FromRequestParts ;
14+ use axum_extra:: extract:: Query ;
1315use axum_extra:: json;
1416use axum_extra:: response:: ErasedJson ;
17+ use crates_io_database:: schema:: users;
1518use crates_io_diesel_helpers:: to_char;
1619use diesel:: prelude:: * ;
17- use diesel_async:: RunQueryDsl ;
20+ use diesel_async:: { AsyncPgConnection , RunQueryDsl } ;
21+ use futures_util:: future:: BoxFuture ;
1822use futures_util:: FutureExt ;
1923use std:: cmp;
24+ use std:: str:: FromStr ;
25+
26+ #[ derive( Debug , Deserialize , FromRequestParts , utoipa:: IntoParams ) ]
27+ #[ from_request( via( Query ) ) ]
28+ #[ into_params( parameter_in = Query ) ]
29+ pub struct DownloadsQueryParams {
30+ /// Additional data to include in the response.
31+ ///
32+ /// Valid values: `versions`.
33+ ///
34+ /// Defaults to no additional data.
35+ ///
36+ /// This parameter expects a comma-separated list of values.
37+ include : Option < String > ,
38+ }
2039
2140/// Get the download counts for a crate.
2241///
@@ -25,12 +44,16 @@ use std::cmp;
2544#[ utoipa:: path(
2645 get,
2746 path = "/api/v1/crates/{name}/downloads" ,
28- params( CratePath ) ,
47+ params( CratePath , DownloadsQueryParams ) ,
2948 tag = "crates" ,
3049 responses( ( status = 200 , description = "Successful Response" ) ) ,
3150) ]
3251
33- pub async fn get_crate_downloads ( state : AppState , path : CratePath ) -> AppResult < ErasedJson > {
52+ pub async fn get_crate_downloads (
53+ state : AppState ,
54+ path : CratePath ,
55+ params : DownloadsQueryParams ,
56+ ) -> AppResult < ErasedJson > {
3457 let mut conn = state. db_read ( ) . await ?;
3558
3659 use diesel:: dsl:: * ;
@@ -47,8 +70,15 @@ pub async fn get_crate_downloads(state: AppState, path: CratePath) -> AppResult<
4770 versions. sort_unstable_by ( |a, b| b. num . cmp ( & a. num ) ) ;
4871 let ( latest_five, rest) = versions. split_at ( cmp:: min ( 5 , versions. len ( ) ) ) ;
4972
73+ let include = params
74+ . include
75+ . as_ref ( )
76+ . map ( |mode| ShowIncludeMode :: from_str ( mode) )
77+ . transpose ( ) ?
78+ . unwrap_or_default ( ) ;
79+
5080 let sum_downloads = sql :: < BigInt > ( "SUM(version_downloads.downloads)" ) ;
51- let ( downloads, extra) = tokio:: try_join!(
81+ let ( downloads, extra, versions_and_publishers , actions ) = tokio:: try_join!(
5282 VersionDownload :: belonging_to( latest_five)
5383 . filter( version_downloads:: date. gt( date( now - 90 . days( ) ) ) )
5484 . order( (
@@ -67,6 +97,8 @@ pub async fn get_crate_downloads(state: AppState, path: CratePath) -> AppResult<
6797 . order( version_downloads:: date. asc( ) )
6898 . load:: <ExtraDownload >( & mut conn)
6999 . boxed( ) ,
100+ load_versions_and_publishers( & mut conn, latest_five, include. versions) ,
101+ load_actions( & mut conn, latest_five, include. versions) ,
70102 ) ?;
71103
72104 let downloads = downloads
@@ -80,10 +112,88 @@ pub async fn get_crate_downloads(state: AppState, path: CratePath) -> AppResult<
80112 downloads : i64 ,
81113 }
82114
115+ if include. versions {
116+ let versions_and_publishers = versions_and_publishers. grouped_by ( latest_five) ;
117+ let actions = actions. grouped_by ( latest_five) ;
118+ let versions = versions_and_publishers
119+ . into_iter ( )
120+ . zip ( actions)
121+ . filter_map ( |( vp, actions) | {
122+ vp. into_iter ( ) . next ( ) . map ( |( version, publisher) | {
123+ EncodableVersion :: from ( version, & path. name , publisher, actions)
124+ } )
125+ } )
126+ . collect :: < Vec < _ > > ( ) ;
127+
128+ return Ok ( json ! ( {
129+ "version_downloads" : downloads,
130+ "versions" : versions,
131+ "meta" : {
132+ "extra_downloads" : extra,
133+ } ,
134+ } ) ) ;
135+ }
136+
83137 Ok ( json ! ( {
84138 "version_downloads" : downloads,
85139 "meta" : {
86140 "extra_downloads" : extra,
87141 } ,
88142 } ) )
89143}
144+
145+ type VersionsAndPublishers = ( FullVersion , Option < User > ) ;
146+ fn load_versions_and_publishers < ' a > (
147+ conn : & mut AsyncPgConnection ,
148+ versions : & ' a [ Version ] ,
149+ includes : bool ,
150+ ) -> BoxFuture < ' a , QueryResult < Vec < VersionsAndPublishers > > > {
151+ if !includes {
152+ return futures_util:: future:: always_ready ( || Ok ( vec ! [ ] ) ) . boxed ( ) ;
153+ }
154+ FullVersion :: belonging_to ( versions)
155+ . left_outer_join ( users:: table)
156+ . select ( VersionsAndPublishers :: as_select ( ) )
157+ . load ( conn)
158+ . boxed ( )
159+ }
160+
161+ fn load_actions < ' a > (
162+ conn : & mut AsyncPgConnection ,
163+ versions : & ' a [ Version ] ,
164+ includes : bool ,
165+ ) -> BoxFuture < ' a , QueryResult < Vec < ( VersionOwnerAction , User ) > > > {
166+ if !includes {
167+ return futures_util:: future:: always_ready ( || Ok ( vec ! [ ] ) ) . boxed ( ) ;
168+ }
169+ VersionOwnerAction :: belonging_to ( versions)
170+ . inner_join ( users:: table)
171+ . order ( version_owner_actions:: id)
172+ . load ( conn)
173+ . boxed ( )
174+ }
175+
176+ #[ derive( Debug , Default ) ]
177+ struct ShowIncludeMode {
178+ versions : bool ,
179+ }
180+
181+ impl ShowIncludeMode {
182+ const INVALID_COMPONENT : & ' static str = "invalid component for ?include= (expected 'versions')" ;
183+ }
184+
185+ impl FromStr for ShowIncludeMode {
186+ type Err = BoxedAppError ;
187+
188+ fn from_str ( s : & str ) -> Result < Self , Self :: Err > {
189+ let mut mode = Self { versions : false } ;
190+ for component in s. split ( ',' ) {
191+ match component {
192+ "" => { }
193+ "versions" => mode. versions = true ,
194+ _ => return Err ( bad_request ( Self :: INVALID_COMPONENT ) ) ,
195+ }
196+ }
197+ Ok ( mode)
198+ }
199+ }
0 commit comments