11use std:: fmt:: { Display , Formatter } ;
22use std:: num:: TryFromIntError ;
33use std:: ops:: { Deref , DerefMut } ;
4+ use std:: str:: FromStr ;
45
6+ use anyhow:: Context ;
57use serde:: { Deserialize , Serialize } ;
68use thiserror:: Error ;
79
10+ use crate :: StdResult ;
811use crate :: entities:: arithmetic_operation_wrapper:: {
912 impl_add_to_wrapper, impl_partial_eq_to_wrapper, impl_sub_to_wrapper,
1013} ;
1114
15+ const INVALID_EPOCH_SPECIFIER_ERROR : & str =
16+ "Invalid epoch: expected 'X', 'latest' or 'latest-X' where X is a positive 64-bit integer" ;
17+
1218/// Epoch represents a Cardano epoch
1319#[ derive(
1420 Debug , Copy , Clone , Default , PartialEq , Serialize , Deserialize , Hash , Eq , PartialOrd , Ord ,
@@ -100,6 +106,32 @@ impl Epoch {
100106 pub fn has_gap_with ( & self , other : & Epoch ) -> bool {
101107 self . 0 . abs_diff ( other. 0 ) > 1
102108 }
109+
110+ /// Parses the given epoch string into an `EpochSpecifier`.
111+ ///
112+ /// Accepted values are:
113+ /// - a `u64` number
114+ /// - `latest`
115+ /// - `latest-{offset}` where `{offset}` is a `u64` number
116+ pub fn parse_specifier ( epoch_str : & str ) -> StdResult < EpochSpecifier > {
117+ if epoch_str == "latest" {
118+ Ok ( EpochSpecifier :: Latest )
119+ } else if let Some ( offset_str) = epoch_str. strip_prefix ( "latest-" ) {
120+ if offset_str. is_empty ( ) {
121+ anyhow:: bail!( "Invalid epoch '{epoch_str}': offset cannot be empty" ) ;
122+ }
123+ let offset = offset_str. parse :: < u64 > ( ) . with_context ( || {
124+ format ! ( "Invalid epoch '{epoch_str}': offset must be a positive 64-bit integer" )
125+ } ) ?;
126+
127+ Ok ( EpochSpecifier :: LatestMinusOffset ( offset) )
128+ } else {
129+ epoch_str
130+ . parse :: < Epoch > ( )
131+ . map ( EpochSpecifier :: Number )
132+ . with_context ( || INVALID_EPOCH_SPECIFIER_ERROR )
133+ }
134+ }
103135}
104136
105137impl Deref for Epoch {
@@ -148,6 +180,16 @@ impl From<Epoch> for f64 {
148180 }
149181}
150182
183+ impl FromStr for Epoch {
184+ type Err = anyhow:: Error ;
185+
186+ fn from_str ( epoch_str : & str ) -> Result < Self , Self :: Err > {
187+ epoch_str. parse :: < u64 > ( ) . map ( Epoch ) . with_context ( || {
188+ format ! ( "Invalid epoch '{epoch_str}': must be a positive 64-bit integer" )
189+ } )
190+ }
191+ }
192+
151193/// EpochError is an error triggered by an [Epoch]
152194#[ derive( Error , Debug ) ]
153195pub enum EpochError {
@@ -156,6 +198,31 @@ pub enum EpochError {
156198 EpochOffset ( u64 , i64 ) ,
157199}
158200
201+ /// Represents the different ways to specify an epoch when querying the API.
202+ #[ derive( Debug , Clone , Copy , PartialEq , Eq ) ]
203+ pub enum EpochSpecifier {
204+ /// Epoch was explicitly provided as a number (e.g., "123")
205+ Number ( Epoch ) ,
206+ /// Epoch was provided as "latest" (e.g., "latest")
207+ Latest ,
208+ /// Epoch was provided as "latest-{offset}" (e.g., "latest-100")
209+ LatestMinusOffset ( u64 ) ,
210+ }
211+
212+ impl Display for EpochSpecifier {
213+ fn fmt ( & self , f : & mut Formatter < ' _ > ) -> std:: fmt:: Result {
214+ match self {
215+ EpochSpecifier :: Number ( epoch) => write ! ( f, "{}" , epoch) ,
216+ EpochSpecifier :: Latest => {
217+ write ! ( f, "latest" )
218+ }
219+ EpochSpecifier :: LatestMinusOffset ( offset) => {
220+ write ! ( f, "latest-{}" , offset)
221+ }
222+ }
223+ }
224+ }
225+
159226#[ cfg( test) ]
160227mod tests {
161228 use crate :: entities:: arithmetic_operation_wrapper:: tests:: test_op_assign;
@@ -262,4 +329,114 @@ mod tests {
262329 assert ! ( !Epoch ( 3 ) . has_gap_with( & Epoch ( 2 ) ) ) ;
263330 assert ! ( Epoch ( 3 ) . has_gap_with( & Epoch ( 0 ) ) ) ;
264331 }
332+
333+ #[ test]
334+ fn from_str ( ) {
335+ let expected_epoch = Epoch ( 123 ) ;
336+ let from_str = Epoch :: from_str ( "123" ) . unwrap ( ) ;
337+ assert_eq ! ( from_str, expected_epoch) ;
338+
339+ let from_string = String :: from ( "123" ) . parse :: < Epoch > ( ) . unwrap ( ) ;
340+ assert_eq ! ( from_string, expected_epoch) ;
341+
342+ let alternate_notation: Epoch = "123" . parse ( ) . unwrap ( ) ;
343+ assert_eq ! ( alternate_notation, expected_epoch) ;
344+
345+ let invalid_epoch_err = Epoch :: from_str ( "123.456" ) . unwrap_err ( ) ;
346+ assert ! (
347+ invalid_epoch_err
348+ . to_string( )
349+ . contains( "Invalid epoch '123.456': must be a positive 64-bit integer" )
350+ ) ;
351+
352+ let overflow_err = format ! ( "1{}" , u64 :: MAX ) . parse :: < Epoch > ( ) . unwrap_err ( ) ;
353+ assert ! (
354+ overflow_err. to_string( ) . contains(
355+ "Invalid epoch '118446744073709551615': must be a positive 64-bit integer"
356+ )
357+ ) ;
358+ }
359+
360+ #[ test]
361+ fn display_epoch_specifier ( ) {
362+ assert_eq ! ( format!( "{}" , EpochSpecifier :: Number ( Epoch ( 123 ) ) ) , "123" ) ;
363+ assert_eq ! ( format!( "{}" , EpochSpecifier :: Latest ) , "latest" ) ;
364+ assert_eq ! (
365+ format!( "{}" , EpochSpecifier :: LatestMinusOffset ( 123 ) ) ,
366+ "latest-123"
367+ ) ;
368+ }
369+
370+ mod parse_specifier {
371+ use super :: * ;
372+
373+ #[ test]
374+ fn parse_epoch_number ( ) {
375+ let parsed_value = Epoch :: parse_specifier ( "5" ) . unwrap ( ) ;
376+ assert_eq ! ( EpochSpecifier :: Number ( Epoch ( 5 ) ) , parsed_value) ;
377+ }
378+
379+ #[ test]
380+ fn parse_latest_epoch ( ) {
381+ let parsed_value = Epoch :: parse_specifier ( "latest" ) . unwrap ( ) ;
382+ assert_eq ! ( EpochSpecifier :: Latest , parsed_value) ;
383+ }
384+
385+ #[ test]
386+ fn parse_latest_epoch_with_offset ( ) {
387+ let parsed_value = Epoch :: parse_specifier ( "latest-43" ) . unwrap ( ) ;
388+ assert_eq ! ( EpochSpecifier :: LatestMinusOffset ( 43 ) , parsed_value) ;
389+ }
390+
391+ #[ test]
392+ fn parse_invalid_str_yield_error ( ) {
393+ let error = Epoch :: parse_specifier ( "invalid_string" ) . unwrap_err ( ) ;
394+ assert ! ( error. to_string( ) . contains( INVALID_EPOCH_SPECIFIER_ERROR ) ) ;
395+ }
396+
397+ #[ test]
398+ fn parse_too_big_epoch_number_yield_error ( ) {
399+ let error = Epoch :: parse_specifier ( & format ! ( "9{}" , u64 :: MAX ) ) . unwrap_err ( ) ;
400+ assert ! ( error. to_string( ) . contains( INVALID_EPOCH_SPECIFIER_ERROR ) ) ;
401+ println ! ( "{:?}" , error) ;
402+ }
403+
404+ #[ test]
405+ fn parse_latest_epoch_with_invalid_offset_yield_error ( ) {
406+ let error = Epoch :: parse_specifier ( "latest-invalid" ) . unwrap_err ( ) ;
407+ assert ! ( error. to_string( ) . contains(
408+ "Invalid epoch 'latest-invalid': offset must be a positive 64-bit integer"
409+ ) ) ;
410+ }
411+
412+ #[ test]
413+ fn parse_latest_epoch_with_empty_offset_yield_error ( ) {
414+ let error = Epoch :: parse_specifier ( "latest-" ) . unwrap_err ( ) ;
415+ assert ! (
416+ error
417+ . to_string( )
418+ . contains( "Invalid epoch 'latest-': offset cannot be empty" )
419+ ) ;
420+ }
421+
422+ #[ test]
423+ fn parse_latest_epoch_with_too_big_offset_yield_error ( ) {
424+ let error = Epoch :: parse_specifier ( & format ! ( "latest-9{}" , u64 :: MAX ) ) . unwrap_err ( ) ;
425+ assert ! ( error. to_string( ) . contains(
426+ "Invalid epoch 'latest-918446744073709551615': offset must be a positive 64-bit integer"
427+ ) )
428+ }
429+
430+ #[ test]
431+ fn specifier_to_string_can_be_parsed_back ( ) {
432+ for specifier in [
433+ EpochSpecifier :: Number ( Epoch ( 121 ) ) ,
434+ EpochSpecifier :: Latest ,
435+ EpochSpecifier :: LatestMinusOffset ( 121 ) ,
436+ ] {
437+ let value = Epoch :: parse_specifier ( & specifier. to_string ( ) ) . unwrap ( ) ;
438+ assert_eq ! ( value, specifier) ;
439+ }
440+ }
441+ }
265442}
0 commit comments