@@ -3,11 +3,13 @@ use crate::{
33 error:: { Error , Result } ,
44 models:: { ApiErrors , CrateData , CrateOwner , OwnerKind , ReleaseData , Search , SearchResponse } ,
55} ;
6- use anyhow:: { Context , anyhow} ;
76use chrono:: { DateTime , Utc } ;
87use docs_rs_types:: { KrateName , Version } ;
98use docs_rs_utils:: { APP_USER_AGENT , retry_async} ;
10- use reqwest:: header:: { ACCEPT , HeaderValue , USER_AGENT } ;
9+ use reqwest:: {
10+ StatusCode ,
11+ header:: { ACCEPT , HeaderValue , USER_AGENT } ,
12+ } ;
1113use serde:: { Deserialize , de:: DeserializeOwned } ;
1214use tracing:: instrument;
1315use url:: Url ;
@@ -111,7 +113,7 @@ impl RegistryApi {
111113 & self ,
112114 name : & KrateName ,
113115 version : & Version ,
114- ) -> Result < ReleaseData > {
116+ ) -> Result < Option < ReleaseData > > {
115117 let url = {
116118 let mut url = self . api_base . clone ( ) ;
117119 url. path_segments_mut ( )
@@ -136,19 +138,25 @@ impl RegistryApi {
136138 downloads : i32 ,
137139 }
138140
139- let response: Response = self . request ( & url) . await ?;
141+ let response: Response = match self . request ( & url) . await {
142+ Ok ( response) => response,
143+ Err ( err) if err. status ( ) == Some ( StatusCode :: NOT_FOUND ) => return Ok ( None ) ,
144+ Err ( err) => return Err ( err) ,
145+ } ;
140146
141- let version = response
147+ let Some ( version) = response
142148 . versions
143149 . into_iter ( )
144150 . find ( |data| data. num == * version)
145- . with_context ( || anyhow ! ( "Could not find version in response" ) ) ?;
151+ else {
152+ return Ok ( None ) ;
153+ } ;
146154
147- Ok ( ReleaseData {
155+ Ok ( Some ( ReleaseData {
148156 release_time : version. created_at ,
149157 yanked : version. yanked ,
150158 downloads : version. downloads ,
151- } )
159+ } ) )
152160 }
153161
154162 /// Fetch owners from the registry's API
@@ -222,6 +230,8 @@ impl RegistryApi {
222230mod tests {
223231 use super :: * ;
224232 use crate :: models:: { ApiError , SearchCrate , SearchMeta } ;
233+ use anyhow:: anyhow;
234+ use docs_rs_types:: testing:: { KRATE , V1 , V2 } ;
225235 use reqwest:: { StatusCode , header:: CONTENT_TYPE } ;
226236 use serde:: Serialize ;
227237 use test_case:: test_case;
@@ -241,6 +251,25 @@ mod tests {
241251 api. search ( "q=foo" ) . await
242252 }
243253
254+ async fn test_get_release (
255+ status : StatusCode ,
256+ body : impl Serialize ,
257+ version : & Version ,
258+ ) -> Result < Option < ReleaseData > > {
259+ let mut crates_io_api = mockito:: Server :: new_async ( ) . await ;
260+
261+ let _m = crates_io_api
262+ . mock ( "GET" , "/api/v1/crates/krate/versions" )
263+ . with_status ( status. as_u16 ( ) . into ( ) )
264+ . with_header ( CONTENT_TYPE , mime:: APPLICATION_JSON . as_ref ( ) )
265+ . with_body ( serde_json:: to_vec ( & body) . unwrap ( ) )
266+ . create_async ( )
267+ . await ;
268+
269+ let api = RegistryApi :: new ( crates_io_api. url ( ) . parse ( ) . unwrap ( ) , 0 ) ?;
270+ api. get_release_data ( & KRATE , version) . await
271+ }
272+
244273 #[ test]
245274 fn test_error_without_status ( ) {
246275 for err in [
@@ -397,4 +426,95 @@ mod tests {
397426
398427 Ok ( ( ) )
399428 }
429+
430+ #[ tokio:: test]
431+ async fn test_get_release_ok ( ) -> Result < ( ) > {
432+ let release_data = test_get_release (
433+ StatusCode :: OK ,
434+ serde_json:: json!( {
435+ "versions" : [
436+ {
437+ "num" : V1 . to_string( ) ,
438+ "created_at" : "2024-01-01T00:00:00Z" ,
439+ "yanked" : false ,
440+ "downloads" : 42
441+ } ,
442+ {
443+ "num" : V2 . to_string( ) ,
444+ "created_at" : "2024-01-02T00:00:00Z" ,
445+ "yanked" : true ,
446+ "downloads" : 100
447+ }
448+ ]
449+ } ) ,
450+ & V1 ,
451+ )
452+ . await ?
453+ . expect ( "found version" ) ;
454+
455+ assert_eq ! (
456+ release_data,
457+ ReleaseData {
458+ release_time: DateTime :: parse_from_rfc3339( "2024-01-01T00:00:00Z" )
459+ . unwrap( )
460+ . with_timezone( & Utc ) ,
461+ yanked: false ,
462+ downloads: 42 ,
463+ }
464+ ) ;
465+
466+ Ok ( ( ) )
467+ }
468+
469+ #[ tokio:: test]
470+ async fn test_get_release_not_found_empty_result ( ) -> Result < ( ) > {
471+ assert ! (
472+ test_get_release(
473+ StatusCode :: OK ,
474+ serde_json:: json!( {
475+ "versions" : [ ]
476+ } ) ,
477+ & V1 ,
478+ )
479+ . await ?
480+ . is_none( )
481+ ) ;
482+
483+ Ok ( ( ) )
484+ }
485+
486+ #[ tokio:: test]
487+ async fn test_get_release_not_found_other_version ( ) -> Result < ( ) > {
488+ assert ! (
489+ test_get_release(
490+ StatusCode :: OK ,
491+ serde_json:: json!( {
492+ "versions" : [
493+ {
494+ "num" : V1 . to_string( ) ,
495+ "created_at" : "2024-01-01T00:00:00Z" ,
496+ "yanked" : false ,
497+ "downloads" : 42
498+ }
499+ ]
500+ } ) ,
501+ & V2 ,
502+ )
503+ . await ?
504+ . is_none( )
505+ ) ;
506+
507+ Ok ( ( ) )
508+ }
509+
510+ #[ tokio:: test]
511+ async fn test_get_release_not_found_404 ( ) -> Result < ( ) > {
512+ assert ! (
513+ test_get_release( StatusCode :: NOT_FOUND , "" , & V1 )
514+ . await ?
515+ . is_none( )
516+ ) ;
517+
518+ Ok ( ( ) )
519+ }
400520}
0 commit comments