11//! Endpoint for searching and discovery functionality
22
33use crate :: auth:: AuthCheck ;
4+ use axum:: extract:: FromRequestParts ;
5+ use axum_extra:: extract:: Query ;
46use axum_extra:: json;
57use axum_extra:: response:: ErasedJson ;
68use diesel:: dsl:: { exists, sql, InnerJoinQuerySource , LeftJoinQuerySource } ;
@@ -9,8 +11,10 @@ use diesel::sql_types::{Bool, Text};
911use diesel_async:: { AsyncPgConnection , RunQueryDsl } ;
1012use diesel_full_text_search:: * ;
1113use http:: request:: Parts ;
14+ use std:: ops:: Deref ;
1215use std:: sync:: OnceLock ;
1316use tracing:: Instrument ;
17+ use utoipa:: IntoParams ;
1418
1519use crate :: app:: AppState ;
1620use crate :: controllers:: helpers:: Paginate ;
@@ -22,6 +26,7 @@ use crate::views::EncodableCrate;
2226use crate :: controllers:: helpers:: pagination:: { Page , PaginationOptions } ;
2327use crate :: models:: krate:: ALL_COLUMNS ;
2428use crate :: sql:: { array_agg, canon_crate_name, lower} ;
29+ use crate :: util:: string_excl_null:: StringExclNull ;
2530use crate :: util:: RequestUtils ;
2631
2732/// Returns a list of crates.
@@ -33,10 +38,15 @@ use crate::util::RequestUtils;
3338#[ utoipa:: path(
3439 get,
3540 path = "/api/v1/crates" ,
41+ params( ListQPs ) ,
3642 tag = "crates" ,
3743 responses( ( status = 200 , description = "Successful Response" ) ) ,
3844) ]
39- pub async fn list_crates ( app : AppState , req : Parts ) -> AppResult < ErasedJson > {
45+ pub async fn list_crates (
46+ app : AppState ,
47+ params : ListQueryParams ,
48+ req : Parts ,
49+ ) -> AppResult < ErasedJson > {
4050 // Notes:
4151 // The different use cases this function covers is handled through passing
4252 // in parameters in the GET request.
@@ -57,32 +67,8 @@ pub async fn list_crates(app: AppState, req: Parts) -> AppResult<ErasedJson> {
5767 use diesel:: sql_types:: Float ;
5868 use seek:: * ;
5969
60- let params = req. query ( ) ;
61- let option_param = |s| match params. get ( s) . map ( |v| v. as_str ( ) ) {
62- Some ( v) if v. contains ( '\0' ) => Err ( bad_request ( format ! (
63- "parameter {s} cannot contain a null byte"
64- ) ) ) ,
65- Some ( v) => Ok ( Some ( v) ) ,
66- None => Ok ( None ) ,
67- } ;
68- let sort = option_param ( "sort" ) ?;
69- let include_yanked = option_param ( "include_yanked" ) ?
70- . map ( |s| s == "yes" )
71- . unwrap_or ( true ) ;
72-
73- let filter_params = FilterParams {
74- q_string : option_param ( "q" ) ?,
75- include_yanked,
76- category : option_param ( "category" ) ?,
77- all_keywords : option_param ( "all_keywords" ) ?,
78- keyword : option_param ( "keyword" ) ?,
79- letter : option_param ( "letter" ) ?,
80- user_id : option_param ( "user_id" ) ?. and_then ( |s| s. parse :: < i32 > ( ) . ok ( ) ) ,
81- team_id : option_param ( "team_id" ) ?. and_then ( |s| s. parse :: < i32 > ( ) . ok ( ) ) ,
82- following : option_param ( "following" ) ?. is_some ( ) ,
83- has_ids : option_param ( "ids[]" ) ?. is_some ( ) ,
84- ..Default :: default ( )
85- } ;
70+ let filter_params: FilterParams = params. into ( ) ;
71+ let sort = filter_params. sort . as_deref ( ) ;
8672
8773 let selection = (
8874 ALL_COLUMNS ,
@@ -106,6 +92,8 @@ pub async fn list_crates(app: AppState, req: Parts) -> AppResult<ErasedJson> {
10692
10793 if let Some ( q_string) = & filter_params. q_string {
10894 if !q_string. is_empty ( ) {
95+ let q_string = q_string. as_str ( ) ;
96+
10997 let sort = sort. unwrap_or ( "relevance" ) ;
11098
11199 query = query. order ( Crate :: with_name ( q_string) . desc ( ) ) ;
@@ -258,40 +246,93 @@ pub async fn list_crates(app: AppState, req: Parts) -> AppResult<ErasedJson> {
258246 } ) )
259247}
260248
261- #[ derive( Default ) ]
262- struct FilterParams < ' a > {
263- q_string : Option < & ' a str > ,
264- include_yanked : bool ,
265- category : Option < & ' a str > ,
266- all_keywords : Option < & ' a str > ,
267- keyword : Option < & ' a str > ,
268- letter : Option < & ' a str > ,
249+ #[ derive( Debug , Deserialize , FromRequestParts , IntoParams ) ]
250+ #[ from_request( via( Query ) ) ]
251+ #[ into_params( parameter_in = Query ) ]
252+ pub struct ListQueryParams {
253+ /// The sort order of the crates.
254+ ///
255+ /// Valid values: `alphabetical`, `relevance`, `downloads`,
256+ /// `recent-downloads`, `recent-updates`, `new`.
257+ ///
258+ /// Defaults to `relevance` if `q_string` is set, otherwise `alphabetical`.
259+ sort : Option < String > ,
260+
261+ /// A search query string.
262+ #[ serde( rename = "q" ) ]
263+ #[ param( inline) ]
264+ q_string : Option < StringExclNull > ,
265+
266+ /// Includes yanked crates in the results if set to `yes`.
267+ include_yanked : Option < String > ,
268+
269+ /// Only return crates in the specified category.
270+ #[ param( inline) ]
271+ category : Option < StringExclNull > ,
272+
273+ /// Only return crates with all the specified keywords.
274+ ///
275+ /// This parameter expects a space-separated list of keywords.
276+ #[ param( inline) ]
277+ all_keywords : Option < StringExclNull > ,
278+
279+ /// Only return crates with the specified keyword.
280+ #[ param( inline) ]
281+ keyword : Option < StringExclNull > ,
282+
283+ /// Only return crates with names starting with the specified letter.
284+ #[ param( inline) ]
285+ letter : Option < StringExclNull > ,
286+
287+ /// Only return crates owned by the specified user.
269288 user_id : Option < i32 > ,
289+
290+ /// Only return crates owned by the specified team.
270291 team_id : Option < i32 > ,
271- following : bool ,
272- has_ids : bool ,
292+
293+ /// Only return crates followed by the authenticated user.
294+ following : Option < String > ,
295+
296+ /// Only return crates with the specified names.
297+ #[ serde( rename = "ids[]" , default ) ]
298+ #[ param( inline) ]
299+ ids : Vec < StringExclNull > ,
300+ }
301+
302+ impl ListQueryParams {
303+ pub fn include_yanked ( & self ) -> bool {
304+ let include_yanked = self . include_yanked . as_ref ( ) ;
305+ include_yanked. map ( |s| s == "yes" ) . unwrap_or ( true )
306+ }
307+
308+ pub fn following ( & self ) -> bool {
309+ self . following . is_some ( )
310+ }
311+ }
312+
313+ struct FilterParams {
314+ search_params : ListQueryParams ,
273315 _auth_user_id : OnceLock < i32 > ,
274- _ids : OnceLock < Option < Vec < String > > > ,
275316}
276317
277- impl < ' a > FilterParams < ' a > {
278- fn ids ( & self , req : & Parts ) -> Option < & [ String ] > {
279- self . _ids
280- . get_or_init ( || {
281- if self . has_ids {
282- let query_bytes = req. uri . query ( ) . unwrap_or ( "" ) . as_bytes ( ) ;
283- let v = url:: form_urlencoded:: parse ( query_bytes)
284- . filter ( |( key, _) | key == "ids[]" )
285- . map ( |( _, value) | value. to_string ( ) )
286- . collect :: < Vec < _ > > ( ) ;
287- Some ( v)
288- } else {
289- None
290- }
291- } )
292- . as_deref ( )
318+ impl Deref for FilterParams {
319+ type Target = ListQueryParams ;
320+
321+ fn deref ( & self ) -> & Self :: Target {
322+ & self . search_params
293323 }
324+ }
294325
326+ impl From < ListQueryParams > for FilterParams {
327+ fn from ( search_params : ListQueryParams ) -> Self {
328+ Self {
329+ search_params,
330+ _auth_user_id : OnceLock :: new ( ) ,
331+ }
332+ }
333+ }
334+
335+ impl FilterParams {
295336 async fn authed_user_id ( & self , req : & Parts , conn : & mut AsyncPgConnection ) -> AppResult < i32 > {
296337 if let Some ( val) = self . _auth_user_id . get ( ) {
297338 return Ok ( * val) ;
@@ -306,40 +347,40 @@ impl<'a> FilterParams<'a> {
306347 }
307348
308349 async fn make_query (
309- & ' a self ,
350+ & self ,
310351 req : & Parts ,
311352 conn : & mut AsyncPgConnection ,
312- ) -> AppResult < crates:: BoxedQuery < ' a , diesel:: pg:: Pg > > {
353+ ) -> AppResult < crates:: BoxedQuery < ' _ , diesel:: pg:: Pg > > {
313354 let mut query = crates:: table. into_boxed ( ) ;
314355
315- if let Some ( q_string) = self . q_string {
356+ if let Some ( q_string) = & self . q_string {
316357 if !q_string. is_empty ( ) {
317358 let q = sql :: < TsQuery > ( "plainto_tsquery('english', " )
318- . bind :: < Text , _ > ( q_string)
359+ . bind :: < Text , _ > ( q_string. as_str ( ) )
319360 . sql ( ")" ) ;
320361 query = query. filter (
321362 q. matches ( crates:: textsearchable_index_col)
322- . or ( Crate :: loosly_matches_name ( q_string) ) ,
363+ . or ( Crate :: loosly_matches_name ( q_string. as_str ( ) ) ) ,
323364 ) ;
324365 }
325366 }
326367
327- if let Some ( cat) = self . category {
368+ if let Some ( cat) = & self . category {
328369 query = query. filter (
329370 crates:: id. eq_any (
330371 crates_categories:: table
331372 . select ( crates_categories:: crate_id)
332373 . inner_join ( categories:: table)
333374 . filter (
334375 categories:: slug
335- . eq ( cat)
376+ . eq ( cat. as_str ( ) )
336377 . or ( categories:: slug. like ( format ! ( "{cat}::%" ) ) ) ,
337378 ) ,
338379 ) ,
339380 ) ;
340381 }
341382
342- if let Some ( kws) = self . all_keywords {
383+ if let Some ( kws) = & self . all_keywords {
343384 let names: Vec < _ > = kws
344385 . split_whitespace ( )
345386 . map ( |name| name. to_lowercase ( ) )
@@ -353,16 +394,16 @@ impl<'a> FilterParams<'a> {
353394 . single_value ( )
354395 . contains ( names) ,
355396 ) ;
356- } else if let Some ( kw) = self . keyword {
397+ } else if let Some ( kw) = & self . keyword {
357398 query = query. filter (
358399 crates:: id. eq_any (
359400 crates_keywords:: table
360401 . select ( crates_keywords:: crate_id)
361402 . inner_join ( keywords:: table)
362- . filter ( lower ( keywords:: keyword) . eq ( lower ( kw) ) ) ,
403+ . filter ( lower ( keywords:: keyword) . eq ( lower ( kw. as_str ( ) ) ) ) ,
363404 ) ,
364405 ) ;
365- } else if let Some ( letter) = self . letter {
406+ } else if let Some ( letter) = & self . letter {
366407 let pattern = format ! (
367408 "{}%" ,
368409 letter
@@ -389,7 +430,7 @@ impl<'a> FilterParams<'a> {
389430 . filter ( crate_owners:: owner_id. eq ( team_id) ) ,
390431 ) ,
391432 ) ;
392- } else if self . following {
433+ } else if self . following ( ) {
393434 let user_id = self . authed_user_id ( req, conn) . await ?;
394435 query = query. filter (
395436 crates:: id. eq_any (
@@ -398,11 +439,11 @@ impl<'a> FilterParams<'a> {
398439 . filter ( follows:: user_id. eq ( user_id) ) ,
399440 ) ,
400441 ) ;
401- } else if self . ids ( req ) . is_some ( ) {
402- query = query. filter ( crates:: name. eq_any ( self . ids ( req ) . unwrap ( ) ) ) ;
442+ } else if ! self . ids . is_empty ( ) {
443+ query = query. filter ( crates:: name. eq_any ( self . ids . iter ( ) . map ( |s| s . as_str ( ) ) ) ) ;
403444 }
404445
405- if !self . include_yanked {
446+ if !self . include_yanked ( ) {
406447 query = query. filter ( exists (
407448 versions:: table
408449 . filter ( versions:: crate_id. eq ( crates:: id) )
@@ -413,7 +454,7 @@ impl<'a> FilterParams<'a> {
413454 Ok ( query)
414455 }
415456
416- fn seek_after ( & self , seek_payload : & seek:: SeekPayload ) -> BoxedCondition < ' a > {
457+ fn seek_after ( & self , seek_payload : & seek:: SeekPayload ) -> BoxedCondition < ' _ > {
417458 use seek:: * ;
418459
419460 let crates_aliased = alias ! ( crates as crates_aliased) ;
@@ -509,7 +550,7 @@ impl<'a> FilterParams<'a> {
509550 // Equivalent of:
510551 // `WHERE (exact_match = exact_match' AND name < name') OR exact_match <
511552 // exact_match'`
512- let q_string = self . q_string . expect ( "q_string should not be None" ) ;
553+ let q_string = self . q_string . as_ref ( ) . expect ( "q_string should not be None" ) ;
513554 let name_exact_match = Crate :: with_name ( q_string) ;
514555 vec ! [
515556 Box :: new(
@@ -530,12 +571,12 @@ impl<'a> FilterParams<'a> {
530571 // `WHERE (exact_match = exact_match' AND rank = rank' AND name > name')
531572 // OR (exact_match = exact_match' AND rank < rank')
532573 // OR exact_match < exact_match'`
533- let q_string = self . q_string . expect ( "q_string should not be None" ) ;
574+ let q_string = self . q_string . as_ref ( ) . expect ( "q_string should not be None" ) ;
534575 let q = sql :: < TsQuery > ( "plainto_tsquery('english', " )
535- . bind :: < Text , _ > ( q_string)
576+ . bind :: < Text , _ > ( q_string. as_str ( ) )
536577 . sql ( ")" ) ;
537578 let rank = ts_rank_cd ( crates:: textsearchable_index_col, q) ;
538- let name_exact_match = Crate :: with_name ( q_string) ;
579+ let name_exact_match = Crate :: with_name ( q_string. as_str ( ) ) ;
539580 vec ! [
540581 Box :: new(
541582 name_exact_match
0 commit comments