55
66use crate :: app:: AppState ;
77use crate :: controllers:: krate:: CratePath ;
8- use crate :: models:: { Version , VersionDownload } ;
8+ use crate :: models:: { User , Version , VersionDownload , VersionOwnerAction } ;
99use 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 ;
1214use axum_extra:: json;
1315use axum_extra:: response:: ErasedJson ;
16+ use crates_io_database:: schema:: users;
1417use crates_io_diesel_helpers:: to_char;
1518use diesel:: prelude:: * ;
1619use diesel_async:: RunQueryDsl ;
20+ use futures_util:: FutureExt ;
1721use 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+ }
0 commit comments