Skip to content

Commit 8380e27

Browse files
committed
add era reader
1 parent 30f6152 commit 8380e27

File tree

3 files changed

+367
-1
lines changed

3 files changed

+367
-1
lines changed
Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
1+
use std::{error::Error as StdError, str::FromStr};
2+
3+
use crate::entities::Epoch;
4+
use async_trait::async_trait;
5+
use thiserror::Error;
6+
7+
use super::{supported_era::UnsupportedEraError, SupportedEra};
8+
9+
/// Represents a tag of Era change.
10+
#[derive(Debug, Clone, PartialEq, Eq)]
11+
pub struct EraMarker {
12+
name: String,
13+
epoch: Option<Epoch>,
14+
}
15+
16+
/// Adapters are responsible of technically reading the information of
17+
/// [EraMarker]s from a backend.
18+
#[async_trait]
19+
pub trait EraReaderAdapter {
20+
/// Read era markers from the underlying adapter.
21+
async fn read(&self) -> Result<Vec<EraMarker>, Box<dyn StdError>>;
22+
}
23+
24+
/// This is a response from the [EraReader]. It contains [EraMarker]s read from
25+
/// the adapter. It can try to cast the given markers to [SupportedEra]s.
26+
#[derive(Debug, Clone, PartialEq, Eq)]
27+
pub struct EraEpochToken {
28+
current_epoch: Epoch,
29+
current_era: EraMarker,
30+
next_era: Option<EraMarker>,
31+
}
32+
33+
impl EraEpochToken {
34+
/// Instanciate a new [EraMarker].
35+
pub fn new(current_epoch: Epoch, current_era: EraMarker, next_era: Option<EraMarker>) -> Self {
36+
Self {
37+
current_epoch,
38+
current_era,
39+
next_era,
40+
}
41+
}
42+
43+
/// Try to cast the current [EraMarker] to a [SupportedEra]. If it fails,
44+
/// that means the current Era is not supported by this version of the
45+
/// software.
46+
pub fn get_current_supported_era(&self) -> Result<SupportedEra, UnsupportedEraError> {
47+
SupportedEra::from_str(&self.current_era.name)
48+
}
49+
50+
/// Return the [EraMarker] of the current Era.
51+
pub fn get_current_era_marker(&self) -> &EraMarker {
52+
&self.current_era
53+
}
54+
55+
/// Try to cast the next [EraMarker] to a [SupportedEra]. If it fails, that
56+
/// means the coming Era will not be supported by this version of the
57+
/// software. This mechanism is used to issue a warning to the user asking
58+
/// for upgrade.
59+
pub fn get_next_supported_era(&self) -> Result<Option<SupportedEra>, UnsupportedEraError> {
60+
match self.next_era.as_ref() {
61+
Some(marker) => Ok(Some(SupportedEra::from_str(&marker.name)?)),
62+
None => Ok(None),
63+
}
64+
}
65+
66+
/// Return the [EraMarker] for the coming Era if any.
67+
pub fn get_next_era_marker(&self) -> Option<&EraMarker> {
68+
self.next_era.as_ref()
69+
}
70+
}
71+
72+
/// The EraReader is responsible of giving the current Era and the Era to come.
73+
/// Is uses an [EraReaderAdapter] to read data from a backend.
74+
pub struct EraReader {
75+
adapter: Box<dyn EraReaderAdapter>,
76+
}
77+
78+
/// Error type when [EraReader] fails to return a [EraEpochToken].
79+
#[derive(Debug, Error)]
80+
pub enum EraReaderError {
81+
/// Underlying adapter fails to return data.
82+
#[error("Adapter Error message: «{message}» caught error: {error:?}")]
83+
AdapterFailure {
84+
/// context message
85+
message: String,
86+
87+
/// nested underlying adapter error
88+
error: Box<dyn StdError>,
89+
},
90+
91+
/// Data returned from the adapter are inconsistent or incomplete preventing
92+
/// the determined the current [EraMarker].
93+
#[error("Current Era could not be determined with data from the adapter.")]
94+
CurrentEraNotFound,
95+
}
96+
97+
impl EraReader {
98+
/// Instantiate the [EraReader] injecting the adapter.
99+
pub fn new(adapter: Box<dyn EraReaderAdapter>) -> Self {
100+
Self { adapter }
101+
}
102+
103+
/// This methods triggers the adapter to read the markers from the backend.
104+
/// It tries to determine the current Era and the next Era if any from the
105+
/// data returned from the adapter.
106+
pub async fn read_era_epoch_token(
107+
&self,
108+
current_epoch: Epoch,
109+
) -> Result<EraEpochToken, EraReaderError> {
110+
let eras = self
111+
.adapter
112+
.read()
113+
.await
114+
.map_err(|e| EraReaderError::AdapterFailure {
115+
message: format!("Reading from EraReader adapter raised an error: '{}'.", &e),
116+
error: e,
117+
})?;
118+
119+
let get_epoch = |era_marker: Option<&EraMarker>| -> Epoch {
120+
if let Some(marker) = era_marker {
121+
marker.epoch.unwrap_or_default()
122+
} else {
123+
Epoch(0)
124+
}
125+
};
126+
let current_marker = eras.iter().fold(None, |acc, marker| {
127+
if get_epoch(Some(marker)) <= current_epoch && get_epoch(Some(marker)) > get_epoch(acc)
128+
{
129+
Some(marker)
130+
} else {
131+
acc
132+
}
133+
});
134+
let current_era_marker =
135+
current_marker.ok_or_else(|| EraReaderError::CurrentEraNotFound)?;
136+
137+
let next_era_marker = eras.last().filter(|&marker| marker != current_era_marker);
138+
139+
Ok(EraEpochToken::new(
140+
current_epoch,
141+
current_era_marker.to_owned(),
142+
next_era_marker.cloned(),
143+
))
144+
}
145+
}
146+
147+
#[cfg(test)]
148+
mod tests {
149+
use super::*;
150+
151+
#[derive(Default)]
152+
struct DummyAdapter {
153+
markers: Vec<EraMarker>,
154+
}
155+
156+
impl DummyAdapter {
157+
pub fn set_markers(&mut self, markers: Vec<EraMarker>) {
158+
self.markers = markers;
159+
}
160+
}
161+
162+
#[async_trait]
163+
impl EraReaderAdapter for DummyAdapter {
164+
async fn read(&self) -> Result<Vec<EraMarker>, Box<dyn StdError>> {
165+
Ok(self.markers.clone())
166+
}
167+
}
168+
169+
fn get_basic_marker_sample() -> Vec<EraMarker> {
170+
vec![
171+
EraMarker {
172+
name: "one".to_string(),
173+
epoch: Some(Epoch(1)),
174+
},
175+
EraMarker {
176+
name: "thales".to_string(),
177+
epoch: None,
178+
},
179+
EraMarker {
180+
name: "thales".to_string(),
181+
epoch: Some(Epoch(10)),
182+
},
183+
]
184+
}
185+
186+
#[tokio::test]
187+
async fn current_era_is_supported() {
188+
let markers: Vec<EraMarker> = get_basic_marker_sample();
189+
let mut adapter = DummyAdapter::default();
190+
adapter.set_markers(markers);
191+
192+
let reader = EraReader::new(Box::new(adapter));
193+
let token = reader.read_era_epoch_token(Epoch(10)).await.unwrap();
194+
195+
assert_eq!(
196+
EraEpochToken {
197+
current_epoch: Epoch(10),
198+
current_era: EraMarker {
199+
name: "thales".to_string(),
200+
epoch: Some(Epoch(10))
201+
},
202+
next_era: None,
203+
},
204+
token
205+
);
206+
}
207+
208+
#[tokio::test]
209+
async fn era_epoch_token() {
210+
let markers: Vec<EraMarker> = get_basic_marker_sample();
211+
let mut adapter = DummyAdapter::default();
212+
adapter.set_markers(markers);
213+
214+
let reader = EraReader::new(Box::new(adapter));
215+
let token = reader.read_era_epoch_token(Epoch(10)).await.unwrap();
216+
assert_eq!(
217+
SupportedEra::dummy(),
218+
token
219+
.get_current_supported_era()
220+
.expect("the given era is supported")
221+
);
222+
assert!(token.get_next_era_marker().is_none());
223+
assert!(token
224+
.get_next_supported_era()
225+
.expect("None era shall not fail when asked.")
226+
.is_none());
227+
}
228+
229+
#[tokio::test]
230+
async fn previous_era_is_not_supported() {
231+
let markers: Vec<EraMarker> = get_basic_marker_sample();
232+
let mut adapter = DummyAdapter::default();
233+
adapter.set_markers(markers);
234+
235+
let reader = EraReader::new(Box::new(adapter));
236+
let token = reader.read_era_epoch_token(Epoch(9)).await.unwrap();
237+
238+
assert_eq!(
239+
EraEpochToken {
240+
current_epoch: Epoch(9),
241+
current_era: EraMarker {
242+
name: "one".to_string(),
243+
epoch: Some(Epoch(1))
244+
},
245+
next_era: Some(EraMarker {
246+
name: "thales".to_string(),
247+
epoch: Some(Epoch(10))
248+
}),
249+
},
250+
token
251+
);
252+
}
253+
254+
#[tokio::test]
255+
async fn error_when_no_current_era() {
256+
let markers = vec![
257+
EraMarker {
258+
name: "one".to_string(),
259+
epoch: None,
260+
},
261+
EraMarker {
262+
name: "two".to_string(),
263+
epoch: None,
264+
},
265+
EraMarker {
266+
name: "three".to_string(),
267+
epoch: Some(Epoch(100)),
268+
},
269+
];
270+
271+
let mut adapter = DummyAdapter::default();
272+
adapter.set_markers(markers);
273+
274+
let reader = EraReader::new(Box::new(adapter));
275+
let _ = reader
276+
.read_era_epoch_token(Epoch(9))
277+
.await
278+
.expect_err("No current era must make the reader to fail.");
279+
}
280+
281+
#[tokio::test]
282+
async fn error_when_no_era() {
283+
let adapter = DummyAdapter::default();
284+
285+
let reader = EraReader::new(Box::new(adapter));
286+
let _ = reader
287+
.read_era_epoch_token(Epoch(9))
288+
.await
289+
.expect_err("The adapter gave no result hence the reader should fail.");
290+
}
291+
292+
#[tokio::test]
293+
async fn current_era_is_not_supported() {
294+
let markers: Vec<EraMarker> = get_basic_marker_sample();
295+
let mut adapter = DummyAdapter::default();
296+
adapter.set_markers(markers);
297+
298+
let reader = EraReader::new(Box::new(adapter));
299+
let token = reader.read_era_epoch_token(Epoch(9)).await.unwrap();
300+
301+
token
302+
.get_current_supported_era()
303+
.expect_err("The era 'one' is not supported hence the token must issue an error.");
304+
305+
assert_eq!(
306+
&EraMarker {
307+
name: "one".to_string(),
308+
epoch: Some(Epoch(1))
309+
},
310+
token.get_current_era_marker()
311+
);
312+
token
313+
.get_next_supported_era()
314+
.expect("The next era is supported hence this shall not fail.");
315+
}
316+
}

mithril-common/src/era/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
//! The module used for handling eras
22
33
mod era_checker;
4+
mod era_reader;
45
mod supported_era;
56

67
pub use era_checker::EraChecker;
8+
pub use era_reader::*;
79
pub use supported_era::SupportedEra;

mithril-common/src/era/supported_era.rs

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1+
use std::str::FromStr;
2+
3+
use serde::Deserialize;
4+
use thiserror::Error;
5+
16
/// The era that the software is running or will run
2-
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
38
pub enum SupportedEra {
49
/// Thales era
510
Thales,
@@ -12,3 +17,46 @@ impl SupportedEra {
1217
Self::Thales
1318
}
1419
}
20+
21+
/// Error related to [SupportedEra] String parsing implementation.
22+
#[derive(Error, Debug)]
23+
#[error("Unable to transform era '{0}' into a currently supported era ('thales').")]
24+
pub struct UnsupportedEraError(String);
25+
26+
impl FromStr for SupportedEra {
27+
type Err = UnsupportedEraError;
28+
29+
fn from_str(s: &str) -> Result<Self, Self::Err> {
30+
let s = s.trim().to_lowercase();
31+
32+
if &s == "thales" {
33+
Ok(Self::Thales)
34+
} else {
35+
Err(UnsupportedEraError(s))
36+
}
37+
}
38+
}
39+
40+
#[cfg(test)]
41+
mod tests {
42+
use super::*;
43+
44+
const ERA_NAME: &str = "thales";
45+
46+
#[test]
47+
fn from_str() {
48+
let supported_era =
49+
SupportedEra::from_str(ERA_NAME).expect("This era name should be supported.");
50+
51+
assert_eq!(SupportedEra::dummy(), supported_era);
52+
}
53+
54+
#[test]
55+
fn from_bad_str() {
56+
let era_name = &format!(" {} ", ERA_NAME.to_ascii_uppercase());
57+
let supported_era =
58+
SupportedEra::from_str(era_name).expect("This era name should be supported.");
59+
60+
assert_eq!(SupportedEra::dummy(), supported_era);
61+
}
62+
}

0 commit comments

Comments
 (0)