use serde::{Deserialize, Serialize};
use serde_json::json;
use crate::{
Event, EventSchema, Events, State, UpcastEvent, replay, replay_stored, step, step_stored,
};
#[derive(Serialize, Deserialize, Event)]
#[event(type = "io.thenativeweb.test.incremented")]
struct Incremented {
by: i64,
}
#[derive(Serialize, Deserialize, Event)]
#[event(type = "io.thenativeweb.test.reset")]
struct Reset {}
#[derive(Serialize, Deserialize, Event)]
#[event(type = "io.thenativeweb.test.annotated")]
struct Annotated {
note: String,
}
#[derive(Events)]
enum CounterEvent {
Incremented(Incremented),
Reset(Reset),
Annotated(Annotated),
}
#[derive(Clone, Debug, Default, PartialEq)]
struct Counter {
total: i64,
}
impl State for Counter {
type Event = CounterEvent;
fn evolve(self, event: &CounterEvent) -> Self {
match event {
CounterEvent::Incremented(incremented) => Counter {
total: self.total + incremented.by,
},
CounterEvent::Reset(_) => Counter { total: 0 },
CounterEvent::Annotated(_) => self,
}
}
}
#[derive(Clone, Debug, Default, PartialEq)]
struct CounterFromLatest(Counter);
impl State for CounterFromLatest {
type Event = CounterEvent;
fn evolve(self, event: &CounterEvent) -> Self {
CounterFromLatest(self.0.evolve(event))
}
fn from_latest() -> Option<&'static str> {
Some(Reset::EVENT_TYPE)
}
}
#[derive(Clone, Debug, Default, PartialEq)]
struct CounterFromUnheardOf(Counter);
impl State for CounterFromUnheardOf {
type Event = CounterEvent;
fn evolve(self, event: &CounterEvent) -> Self {
CounterFromUnheardOf(self.0.evolve(event))
}
fn from_latest() -> Option<&'static str> {
Some("io.thenativeweb.test.unheard-of")
}
}
#[derive(Serialize, Deserialize, Event)]
#[event(type = "io.thenativeweb.test.measured")]
struct Measured {
value: f64,
}
#[derive(Events)]
enum GaugeEvent {
Measured(Measured),
}
#[derive(Clone, Debug, Default, PartialEq)]
struct Gauge {
last: f64,
}
impl State for Gauge {
type Event = GaugeEvent;
fn evolve(self, event: &GaugeEvent) -> Self {
match event {
GaugeEvent::Measured(measured) => Gauge {
last: measured.value,
},
}
}
}
fn incremented(by: i64) -> CounterEvent {
CounterEvent::Incremented(Incremented { by })
}
#[test]
fn returns_the_initial_state_for_an_empty_history() {
let counter: Counter = replay([]).expect("an empty history replays");
assert_eq!(counter, Counter::default());
}
#[test]
fn folds_events_in_order() {
let counter: Counter = replay([
incremented(1),
incremented(2),
CounterEvent::Reset(Reset {}),
incremented(4),
])
.expect("the history replays");
assert_eq!(counter.total, 4);
}
#[test]
fn fails_on_an_event_that_does_not_survive_being_written_and_read_back() {
let failure = replay::<Gauge>([GaugeEvent::Measured(Measured { value: f64::NAN })])
.expect_err("NaN does not survive JSON");
assert!(failure.to_string().contains("decoding"), "{failure}");
}
#[test]
fn fails_on_data_that_does_not_match_the_rule() {
let failure = replay_stored::<Counter>([UpcastEvent::new(
Incremented::EVENT_TYPE,
json!({ "by": "three" }),
)])
.expect_err("a string is no number");
assert!(failure.to_string().contains("decoding"), "{failure}");
}
#[test]
fn fails_on_an_event_without_rule() {
let failure = replay_stored::<Counter>([UpcastEvent::new(
"io.thenativeweb.test.unheard-of",
json!({}),
)])
.expect_err("the state has no rule for it");
assert_eq!(
failure.to_string(),
r#"permanent failure: no rule for event type "io.thenativeweb.test.unheard-of""#,
);
}
#[test]
fn collects_the_schema_of_every_event() {
assert_eq!(
CounterEvent::schemas(),
vec![
EventSchema::of::<Incremented>(),
EventSchema::of::<Reset>(),
EventSchema::of::<Annotated>(),
],
);
assert_eq!(
CounterEvent::EVENT_TYPES,
[
"io.thenativeweb.test.incremented",
"io.thenativeweb.test.reset",
"io.thenativeweb.test.annotated",
],
);
}
#[test]
fn has_a_rule_for_every_variant_and_none_for_other_types() {
assert!(CounterEvent::has_rule(Incremented::EVENT_TYPE));
assert!(
CounterEvent::has_rule(Annotated::EVENT_TYPE),
"an ignored event is a variant too"
);
assert!(!CounterEvent::has_rule("io.thenativeweb.test.unheard-of"));
}
#[test]
fn names_the_event_type_of_an_event() {
assert_eq!(incremented(1).event_type(), Incremented::EVENT_TYPE);
assert_eq!(
CounterEvent::Reset(Reset {}).event_type(),
Reset::EVENT_TYPE
);
}
#[test]
fn takes_an_ignored_event_without_changing() {
let counter: Counter = replay([
incremented(3),
CounterEvent::Annotated(Annotated {
note: "a remark".to_owned(),
}),
])
.expect("the history replays");
assert_eq!(counter.total, 3);
}
#[test]
fn walks_the_same_states_as_replay() {
let history = || {
[
incremented(1),
incremented(2),
CounterEvent::Reset(Reset {}),
incremented(4),
]
};
let mut current = Counter::default();
for event in history() {
current = step(¤t, &event).expect("the event steps");
}
assert_eq!(current, replay(history()).expect("the history replays"));
}
#[test]
fn leaves_the_given_state_unchanged() {
let before = Counter { total: 5 };
let after = step(&before, &incremented(2)).expect("the event steps");
assert_eq!(before.total, 5);
assert_eq!(after.total, 7);
}
#[test]
fn fails_on_an_event_that_does_not_survive_a_step() {
let before = Gauge { last: 1.0 };
let failure = step(&before, &GaugeEvent::Measured(Measured { value: f64::NAN }))
.expect_err("NaN does not survive JSON");
assert!(failure.to_string().contains("decoding"), "{failure}");
assert_eq!(
before.last.to_bits(),
1.0_f64.to_bits(),
"the given state is unchanged"
);
}
#[test]
fn starts_from_the_latest_event_of_that_type() {
let counter: CounterFromLatest = replay([
incremented(3),
CounterEvent::Reset(Reset {}),
incremented(5),
CounterEvent::Reset(Reset {}),
incremented(2),
])
.expect("the history replays");
assert_eq!(counter.0.total, 2);
}
#[test]
fn replays_everything_if_the_type_is_missing() {
let counter: CounterFromLatest =
replay([incremented(3), incremented(4)]).expect("the history replays");
assert_eq!(counter.0.total, 7);
}
#[test]
fn starts_stored_events_from_the_latest_event_of_that_type() {
let counter: CounterFromLatest = replay_stored([
UpcastEvent::new("io.thenativeweb.test.unheard-of", json!({})),
UpcastEvent::new(Reset::EVENT_TYPE, json!({})),
UpcastEvent::new(Incremented::EVENT_TYPE, json!({ "by": 2 })),
UpcastEvent::new(Reset::EVENT_TYPE, json!({})),
UpcastEvent::new(Incremented::EVENT_TYPE, json!({ "by": 5 })),
])
.expect("the history replays");
assert_eq!(counter.0.total, 5);
}
#[test]
fn starts_from_the_latest_event_of_that_type_not_from_the_first() {
let counter: CounterFromLatest = replay_stored([
UpcastEvent::new(Reset::EVENT_TYPE, json!({})),
UpcastEvent::new("io.thenativeweb.test.unheard-of", json!({})),
UpcastEvent::new(Reset::EVENT_TYPE, json!({})),
UpcastEvent::new(Incremented::EVENT_TYPE, json!({ "by": 5 })),
])
.expect("the history replays from the latest reset");
assert_eq!(counter.0.total, 5);
}
#[test]
#[should_panic(
expected = r#"event type "io.thenativeweb.test.unheard-of" has no rule on the state"#
)]
fn panics_for_a_state_that_starts_from_an_event_type_without_rule() {
let _ = replay::<CounterFromUnheardOf>([incremented(1)]);
}
#[test]
fn steps_a_stored_event() {
let after = step_stored(
&Counter { total: 1 },
UpcastEvent::new(Incremented::EVENT_TYPE, json!({ "by": 2 })),
)
.expect("the event steps");
assert_eq!(after.total, 3);
}