Skip to content

Commit 70e5b8b

Browse files
committed
refactor: move parsing logic of epoch/latest[offset] to Epoch type
So it can be re-used accross crates. - `EpochSpecifier` enum to represents the cases between <{uint}, 'latest', latest-{uint}> - add `Epoch::parse_specifier` - impl `FromStr` to epoch to avoid having downstream code specifying the inner type of the epoch when parsing from str - update aggregator `ExpandedEpoch` logic
1 parent 6b0f330 commit 70e5b8b

File tree

4 files changed

+192
-24
lines changed

4 files changed

+192
-24
lines changed

mithril-aggregator/src/http_server/parameters.rs

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
//! Helpers when working with parameters from either the query string or the path of an HTTP request
22
3-
use anyhow::Context;
43
use std::ops::Deref;
54

65
use mithril_common::StdResult;
7-
use mithril_common::entities::Epoch;
6+
use mithril_common::entities::{Epoch, EpochSpecifier};
87

98
use crate::dependency_injection::EpochServiceWrapper;
109

@@ -61,26 +60,18 @@ pub async fn expand_epoch(
6160
epoch_service: EpochServiceWrapper,
6261
) -> StdResult<ExpandedEpoch> {
6362
let epoch_str = epoch_str.to_lowercase();
64-
if epoch_str == "latest" {
65-
epoch_service
63+
match Epoch::parse_specifier(&epoch_str)? {
64+
EpochSpecifier::Number(epoch) => Ok(ExpandedEpoch::Parsed(epoch)),
65+
EpochSpecifier::Latest => epoch_service
6666
.read()
6767
.await
6868
.epoch_of_current_data()
69-
.map(ExpandedEpoch::Latest)
70-
} else if epoch_str.starts_with("latest-") {
71-
let (_, offset_str) = epoch_str.split_at("latest-".len());
72-
let offset = offset_str
73-
.parse::<u64>()
74-
.with_context(|| "Invalid epoch offset: must be a number")?;
75-
76-
epoch_service.read().await.epoch_of_current_data().map(|epoch| {
77-
ExpandedEpoch::LatestMinusOffset(Epoch(epoch.saturating_sub(offset)), offset)
78-
})
79-
} else {
80-
epoch_str
81-
.parse::<u64>()
82-
.map(|epoch| ExpandedEpoch::Parsed(Epoch(epoch)))
83-
.with_context(|| "Invalid epoch: must be a number or 'latest'")
69+
.map(ExpandedEpoch::Latest),
70+
EpochSpecifier::LatestMinusOffset(offset) => {
71+
epoch_service.read().await.epoch_of_current_data().map(|epoch| {
72+
ExpandedEpoch::LatestMinusOffset(Epoch(epoch.saturating_sub(offset)), offset)
73+
})
74+
}
8475
}
8576
}
8677

mithril-client/src/type_alias.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,10 @@ pub mod common {
6969
pub use mithril_common::crypto_helper::MKProof;
7070
pub use mithril_common::entities::{
7171
AncillaryLocation, BlockHash, BlockNumber, CardanoDbBeacon, CardanoNetwork, ChainPoint,
72-
CompressionAlgorithm, DigestLocation, Epoch, ImmutableFileNumber, ImmutablesLocation,
73-
MagicId, MultiFilesUri, ProtocolMessage, ProtocolMessagePartKey, ProtocolParameters,
74-
SignedEntityType, SlotNumber, StakeDistribution, SupportedEra, TemplateUri,
75-
TransactionHash,
72+
CompressionAlgorithm, DigestLocation, Epoch, EpochSpecifier, ImmutableFileNumber,
73+
ImmutablesLocation, MagicId, MultiFilesUri, ProtocolMessage, ProtocolMessagePartKey,
74+
ProtocolParameters, SignedEntityType, SlotNumber, StakeDistribution, SupportedEra,
75+
TemplateUri, TransactionHash,
7676
};
7777
pub use mithril_common::messages::{
7878
AncillaryMessagePart, DigestsMessagePart, ImmutablesMessagePart,

mithril-common/src/entities/epoch.rs

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
use std::fmt::{Display, Formatter};
22
use std::num::TryFromIntError;
33
use std::ops::{Deref, DerefMut};
4+
use std::str::FromStr;
45

6+
use anyhow::Context;
57
use serde::{Deserialize, Serialize};
68
use thiserror::Error;
79

10+
use crate::StdResult;
811
use 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

105137
impl 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)]
153195
pub 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)]
160227
mod 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
}

mithril-common/src/entities/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ pub use cardano_transactions_snapshot::CardanoTransactionsSnapshot;
4848
pub use certificate::{Certificate, CertificateSignature};
4949
pub use certificate_metadata::{CertificateMetadata, StakeDistributionParty};
5050
pub use compression_algorithm::*;
51-
pub use epoch::{Epoch, EpochError};
51+
pub use epoch::{Epoch, EpochError, EpochSpecifier};
5252
pub use file_uri::{FileUri, MultiFilesUri, TemplateUri};
5353
pub use http_server_error::{ClientError, ServerError};
5454
pub use mithril_stake_distribution::MithrilStakeDistribution;

0 commit comments

Comments
 (0)