6
6
use crate :: app:: AppState ;
7
7
use crate :: controllers:: krate:: CratePath ;
8
8
use 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 ;
13
15
use axum_extra:: json;
14
16
use axum_extra:: response:: ErasedJson ;
17
+ use crates_io_database:: schema:: users;
15
18
use crates_io_diesel_helpers:: to_char;
16
19
use diesel:: prelude:: * ;
17
- use diesel_async:: RunQueryDsl ;
20
+ use diesel_async:: { AsyncPgConnection , RunQueryDsl } ;
21
+ use futures_util:: future:: BoxFuture ;
18
22
use futures_util:: FutureExt ;
19
23
use 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
+ }
20
39
21
40
/// Get the download counts for a crate.
22
41
///
@@ -25,12 +44,16 @@ use std::cmp;
25
44
#[ utoipa:: path(
26
45
get,
27
46
path = "/api/v1/crates/{name}/downloads" ,
28
- params( CratePath ) ,
47
+ params( CratePath , DownloadsQueryParams ) ,
29
48
tag = "crates" ,
30
49
responses( ( status = 200 , description = "Successful Response" ) ) ,
31
50
) ]
32
51
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 > {
34
57
let mut conn = state. db_read ( ) . await ?;
35
58
36
59
use diesel:: dsl:: * ;
@@ -47,8 +70,15 @@ pub async fn get_crate_downloads(state: AppState, path: CratePath) -> AppResult<
47
70
versions. sort_unstable_by ( |a, b| b. num . cmp ( & a. num ) ) ;
48
71
let ( latest_five, rest) = versions. split_at ( cmp:: min ( 5 , versions. len ( ) ) ) ;
49
72
73
+ let include = params
74
+ . include
75
+ . as_ref ( )
76
+ . map ( |mode| ShowIncludeMode :: from_str ( mode) )
77
+ . transpose ( ) ?
78
+ . unwrap_or_default ( ) ;
79
+
50
80
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!(
52
82
VersionDownload :: belonging_to( latest_five)
53
83
. filter( version_downloads:: date. gt( date( now - 90 . days( ) ) ) )
54
84
. order( (
@@ -67,6 +97,8 @@ pub async fn get_crate_downloads(state: AppState, path: CratePath) -> AppResult<
67
97
. order( version_downloads:: date. asc( ) )
68
98
. load:: <ExtraDownload >( & mut conn)
69
99
. boxed( ) ,
100
+ load_versions_and_publishers( & mut conn, latest_five, include. versions) ,
101
+ load_actions( & mut conn, latest_five, include. versions) ,
70
102
) ?;
71
103
72
104
let downloads = downloads
@@ -80,10 +112,88 @@ pub async fn get_crate_downloads(state: AppState, path: CratePath) -> AppResult<
80
112
downloads : i64 ,
81
113
}
82
114
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
+
83
137
Ok ( json ! ( {
84
138
"version_downloads" : downloads,
85
139
"meta" : {
86
140
"extra_downloads" : extra,
87
141
} ,
88
142
} ) )
89
143
}
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