5
5
6
6
use crate :: app:: AppState ;
7
7
use crate :: controllers:: krate:: CratePath ;
8
- use crate :: models:: { Version , VersionDownload } ;
8
+ use crate :: models:: { User , Version , VersionDownload , VersionOwnerAction } ;
9
9
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 ;
12
14
use axum_extra:: json;
13
15
use axum_extra:: response:: ErasedJson ;
16
+ use crates_io_database:: schema:: users;
14
17
use crates_io_diesel_helpers:: to_char;
15
18
use diesel:: prelude:: * ;
16
19
use diesel_async:: RunQueryDsl ;
20
+ use futures_util:: FutureExt ;
17
21
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
+ }
18
37
19
38
/// Get the download counts for a crate.
20
39
///
@@ -23,12 +42,16 @@ use std::cmp;
23
42
#[ utoipa:: path(
24
43
get,
25
44
path = "/api/v1/crates/{name}/downloads" ,
26
- params( CratePath ) ,
45
+ params( CratePath , DownloadsQueryParams ) ,
27
46
tag = "crates" ,
28
47
responses( ( status = 200 , description = "Successful Response" ) ) ,
29
48
) ]
30
49
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 > {
32
55
let mut conn = state. db_read ( ) . await ?;
33
56
34
57
use diesel:: dsl:: * ;
@@ -43,9 +66,10 @@ pub async fn get_crate_downloads(state: AppState, path: CratePath) -> AppResult<
43
66
. await ?;
44
67
45
68
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) ) ;
47
71
48
- let downloads = VersionDownload :: belonging_to ( latest_five)
72
+ let downloads = VersionDownload :: belonging_to ( & latest_five[ .. ] )
49
73
. filter ( version_downloads:: date. gt ( date ( now - 90 . days ( ) ) ) )
50
74
. order ( (
51
75
version_downloads:: date. asc ( ) ,
@@ -58,7 +82,7 @@ pub async fn get_crate_downloads(state: AppState, path: CratePath) -> AppResult<
58
82
. collect :: < Vec < EncodableVersionDownload > > ( ) ;
59
83
60
84
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[ .. ] )
62
86
. select ( (
63
87
to_char ( version_downloads:: date, "YYYY-MM-DD" ) ,
64
88
sum_downloads,
@@ -75,10 +99,74 @@ pub async fn get_crate_downloads(state: AppState, path: CratePath) -> AppResult<
75
99
downloads : i64 ,
76
100
}
77
101
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
+
78
141
Ok ( json ! ( {
79
142
"version_downloads" : downloads,
80
143
"meta" : {
81
144
"extra_downloads" : extra,
82
145
} ,
83
146
} ) )
84
147
}
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