Skip to content

Commit 21f9c58

Browse files
authored
feat(examples): bare minimum "light switch" example (#322)
This example application is a bare minimum example of the core functionality. Persistence is not enabled, nor is logging, nor any kind of user interface. It just starts, runs some commands, queries the final state and prints it. It models a "light switch". You can "install" the switch, then turn it "off" or "on" using commands. You can issue a query to get the current state of a switch.
1 parent 33cec3f commit 21f9c58

12 files changed

+387
-0
lines changed

Diff for: Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ members = [
66
"eventually-postgres",
77
# Examples
88
"examples/bank-accounting",
9+
"examples/light-switch",
910
]

Diff for: examples/light-switch/Cargo.toml

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[package]
2+
name = "light_switch"
3+
version = "0.1.0"
4+
edition = "2021"
5+
readme = "README.md"
6+
publish = false
7+
8+
[dependencies]
9+
serde_json = { version = "1.0.114", optional = true }
10+
serde = { version = "1.0.197", features = ["derive"] }
11+
tokio = { version = "1.36.0", features = ["macros", "rt-multi-thread"] }
12+
thiserror = { version = "2.0.12" }
13+
anyhow = "1.0.97"
14+
async-trait = "0.1.77"
15+
eventually = { features = ["serde-json"], path="../../eventually" }

Diff for: examples/light-switch/README.md

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Example: Light Switch application
2+
3+
This example application is a bare minimum example of the core functionality.
4+
Persistence is not enabled, nor is logging, nor any kind of user interface. It
5+
just starts, runs some commands, queries the final state and prints it.
6+
7+
It models a "light switch". You can "install" the switch, then turn it "off" or
8+
"on" using commands. You can issue a query to get the current state of a switch.

Diff for: examples/light-switch/src/application.rs

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
use eventually::aggregate;
2+
3+
use crate::domain::LightSwitch;
4+
pub type LightSwitchRepo<S> = aggregate::EventSourcedRepository<LightSwitch, S>;
5+
6+
#[derive(Clone)]
7+
pub struct LightSwitchService<R>
8+
where
9+
R: aggregate::Repository<LightSwitch>,
10+
{
11+
pub light_switch_repository: R,
12+
}
13+
14+
impl<R> From<R> for LightSwitchService<R>
15+
where
16+
R: aggregate::Repository<LightSwitch>,
17+
{
18+
fn from(light_switch_repository: R) -> Self {
19+
Self {
20+
light_switch_repository,
21+
}
22+
}
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
use async_trait::async_trait;
2+
use eventually::{aggregate, command, message};
3+
4+
use crate::application::LightSwitchService;
5+
use crate::domain::{LightSwitch, LightSwitchId, LightSwitchRoot};
6+
7+
#[derive(Debug, Clone, PartialEq, Eq)]
8+
pub struct InstallLightSwitch {
9+
pub id: LightSwitchId,
10+
}
11+
12+
impl message::Message for InstallLightSwitch {
13+
fn name(&self) -> &'static str {
14+
"InstallLightSwitch"
15+
}
16+
}
17+
18+
#[async_trait]
19+
impl<R> command::Handler<InstallLightSwitch> for LightSwitchService<R>
20+
where
21+
R: aggregate::Repository<LightSwitch>,
22+
{
23+
type Error = anyhow::Error;
24+
async fn handle(
25+
&self,
26+
command: command::Envelope<InstallLightSwitch>,
27+
) -> Result<(), Self::Error> {
28+
let command = command.message;
29+
let mut light_switch = LightSwitchRoot::install(command.id)?;
30+
self.light_switch_repository.save(&mut light_switch).await?;
31+
Ok(())
32+
}
33+
}

Diff for: examples/light-switch/src/commands/mod.rs

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
pub mod install_light_switch;
2+
pub mod turn_light_switch_off;
3+
pub mod turn_light_switch_on;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
use async_trait::async_trait;
2+
use eventually::{aggregate, command, message};
3+
4+
use crate::application::LightSwitchService;
5+
use crate::domain::{LightSwitch, LightSwitchId, LightSwitchRoot};
6+
7+
#[derive(Debug, Clone, PartialEq, Eq)]
8+
pub struct TurnLightSwitchOff {
9+
pub id: LightSwitchId,
10+
}
11+
12+
impl message::Message for TurnLightSwitchOff {
13+
fn name(&self) -> &'static str {
14+
"TurnLightSwitchOff"
15+
}
16+
}
17+
18+
#[async_trait]
19+
impl<R> command::Handler<TurnLightSwitchOff> for LightSwitchService<R>
20+
where
21+
R: aggregate::Repository<LightSwitch>,
22+
{
23+
type Error = anyhow::Error;
24+
async fn handle(
25+
&self,
26+
command: command::Envelope<TurnLightSwitchOff>,
27+
) -> Result<(), Self::Error> {
28+
let command = command.message;
29+
let mut root: LightSwitchRoot = self.light_switch_repository.get(&command.id).await?.into();
30+
let _ = root.turn_off(command.id)?;
31+
self.light_switch_repository.save(&mut root).await?;
32+
Ok(())
33+
}
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
use async_trait::async_trait;
2+
use eventually::{aggregate, command, message};
3+
4+
use crate::application::LightSwitchService;
5+
use crate::domain::{LightSwitch, LightSwitchId, LightSwitchRoot};
6+
7+
#[derive(Debug, Clone, PartialEq, Eq)]
8+
pub struct TurnLightSwitchOn {
9+
pub id: LightSwitchId,
10+
}
11+
12+
impl message::Message for TurnLightSwitchOn {
13+
fn name(&self) -> &'static str {
14+
"TurnLightSwitchOn"
15+
}
16+
}
17+
18+
#[async_trait]
19+
impl<R> command::Handler<TurnLightSwitchOn> for LightSwitchService<R>
20+
where
21+
R: aggregate::Repository<LightSwitch>,
22+
{
23+
type Error = anyhow::Error;
24+
async fn handle(
25+
&self,
26+
command: command::Envelope<TurnLightSwitchOn>,
27+
) -> Result<(), Self::Error> {
28+
let command = command.message;
29+
let mut root: LightSwitchRoot = self.light_switch_repository.get(&command.id).await?.into();
30+
let _ = root.turn_on(command.id)?;
31+
self.light_switch_repository.save(&mut root).await?;
32+
Ok(())
33+
}
34+
}

Diff for: examples/light-switch/src/domain.rs

+153
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
use eventually::{aggregate, message};
2+
3+
pub type LightSwitchId = String;
4+
5+
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
6+
pub enum LightSwitchError {
7+
#[error("Light switch has not yet been installed")]
8+
NotYetInstalled,
9+
#[error("Light switch has already been installed")]
10+
AlreadyInstalled,
11+
#[error("Light switch is already on")]
12+
AlreadyOn,
13+
#[error("Light switch is already off")]
14+
AlreadyOff,
15+
}
16+
17+
// events
18+
#[derive(Debug, Clone, Eq, PartialEq)]
19+
pub struct Installed {
20+
id: LightSwitchId,
21+
}
22+
23+
#[derive(Debug, Clone, Eq, PartialEq)]
24+
pub struct SwitchedOn {
25+
id: LightSwitchId,
26+
}
27+
28+
#[derive(Debug, Clone, Eq, PartialEq)]
29+
pub struct SwitchedOff {
30+
id: LightSwitchId,
31+
}
32+
33+
#[derive(Debug, Clone, Eq, PartialEq)]
34+
pub enum LightSwitchEvent {
35+
Installed(Installed),
36+
SwitchedOn(SwitchedOn),
37+
SwitchedOff(SwitchedOff),
38+
}
39+
40+
impl message::Message for LightSwitchEvent {
41+
fn name(&self) -> &'static str {
42+
match self {
43+
LightSwitchEvent::SwitchedOn(_) => "SwitchedOn",
44+
LightSwitchEvent::SwitchedOff(_) => "SwitchedOff",
45+
LightSwitchEvent::Installed(_) => "Installed",
46+
}
47+
}
48+
}
49+
50+
// aggregate
51+
#[derive(Debug, Clone, Eq, PartialEq)]
52+
pub enum LightSwitchState {
53+
On,
54+
Off,
55+
}
56+
57+
#[derive(Debug, Clone)]
58+
pub struct LightSwitch {
59+
id: LightSwitchId,
60+
state: LightSwitchState,
61+
}
62+
63+
impl aggregate::Aggregate for LightSwitch {
64+
type Id = LightSwitchId;
65+
type Event = LightSwitchEvent;
66+
type Error = LightSwitchError;
67+
68+
fn type_name() -> &'static str {
69+
"LightSwitch"
70+
}
71+
72+
fn aggregate_id(&self) -> &Self::Id {
73+
&self.id
74+
}
75+
76+
fn apply(state: Option<Self>, event: Self::Event) -> Result<Self, Self::Error> {
77+
match state {
78+
None => match event {
79+
LightSwitchEvent::Installed(installed) => Ok(LightSwitch {
80+
id: installed.id,
81+
state: LightSwitchState::Off,
82+
}),
83+
LightSwitchEvent::SwitchedOn(_) | LightSwitchEvent::SwitchedOff(_) => {
84+
Err(LightSwitchError::NotYetInstalled)
85+
},
86+
},
87+
Some(mut light_switch) => match event {
88+
LightSwitchEvent::Installed(_) => Err(LightSwitchError::AlreadyInstalled),
89+
LightSwitchEvent::SwitchedOn(_) => match light_switch.state {
90+
LightSwitchState::On => Err(LightSwitchError::AlreadyOn),
91+
LightSwitchState::Off => {
92+
light_switch.state = LightSwitchState::On;
93+
Ok(light_switch)
94+
},
95+
},
96+
LightSwitchEvent::SwitchedOff(_) => match light_switch.state {
97+
LightSwitchState::On => {
98+
light_switch.state = LightSwitchState::Off;
99+
Ok(light_switch)
100+
},
101+
LightSwitchState::Off => Err(LightSwitchError::AlreadyOff),
102+
},
103+
},
104+
}
105+
}
106+
}
107+
108+
// root
109+
#[derive(Debug, Clone)]
110+
pub struct LightSwitchRoot(aggregate::Root<LightSwitch>);
111+
112+
// NOTE: The trait implementations for From, Deref and DerefMut below are
113+
// implemented manually for demonstration purposes, but most would prefer to have them
114+
// auto-generated at compile time by using the [`eventually_macros::aggregate_root`] macro
115+
impl From<eventually::aggregate::Root<LightSwitch>> for LightSwitchRoot {
116+
fn from(root: eventually::aggregate::Root<LightSwitch>) -> Self {
117+
Self(root)
118+
}
119+
}
120+
impl From<LightSwitchRoot> for eventually::aggregate::Root<LightSwitch> {
121+
fn from(value: LightSwitchRoot) -> Self {
122+
value.0
123+
}
124+
}
125+
impl std::ops::Deref for LightSwitchRoot {
126+
type Target = eventually::aggregate::Root<LightSwitch>;
127+
fn deref(&self) -> &Self::Target {
128+
&self.0
129+
}
130+
}
131+
impl std::ops::DerefMut for LightSwitchRoot {
132+
fn deref_mut(&mut self) -> &mut Self::Target {
133+
&mut self.0
134+
}
135+
}
136+
137+
impl LightSwitchRoot {
138+
pub fn install(id: LightSwitchId) -> Result<Self, LightSwitchError> {
139+
aggregate::Root::<LightSwitch>::record_new(
140+
LightSwitchEvent::Installed(Installed { id }).into(),
141+
)
142+
.map(Self)
143+
}
144+
pub fn turn_on(&mut self, id: LightSwitchId) -> Result<(), LightSwitchError> {
145+
self.record_that(LightSwitchEvent::SwitchedOn(SwitchedOn { id }).into())
146+
}
147+
pub fn turn_off(&mut self, id: LightSwitchId) -> Result<(), LightSwitchError> {
148+
self.record_that(LightSwitchEvent::SwitchedOff(SwitchedOff { id }).into())
149+
}
150+
pub fn get_switch_state(&self) -> Result<LightSwitchState, LightSwitchError> {
151+
Ok(self.state.clone())
152+
}
153+
}

Diff for: examples/light-switch/src/main.rs

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
mod application;
2+
mod commands;
3+
mod domain;
4+
mod queries;
5+
use application::{LightSwitchRepo, LightSwitchService};
6+
use commands::install_light_switch::InstallLightSwitch;
7+
use commands::turn_light_switch_off::TurnLightSwitchOff;
8+
use commands::turn_light_switch_on::TurnLightSwitchOn;
9+
use domain::{LightSwitchEvent, LightSwitchId};
10+
use eventually::{command, event, query};
11+
use queries::get_switch_state::GetSwitchState;
12+
13+
#[tokio::main]
14+
async fn main() -> Result<(), anyhow::Error> {
15+
let store = event::store::InMemory::<LightSwitchId, LightSwitchEvent>::default();
16+
let repo = LightSwitchRepo::from(store.clone());
17+
let svc = LightSwitchService::from(repo);
18+
19+
let cmd = InstallLightSwitch {
20+
id: "Switch1".to_string(),
21+
}
22+
.into();
23+
command::Handler::handle(&svc, cmd).await?;
24+
println!("Installed Switch1");
25+
26+
let cmd = TurnLightSwitchOn {
27+
id: "Switch1".to_string(),
28+
}
29+
.into();
30+
command::Handler::handle(&svc, cmd).await?;
31+
println!("Turned Switch1 On");
32+
33+
let cmd = TurnLightSwitchOff {
34+
id: "Switch1".to_string(),
35+
}
36+
.into();
37+
command::Handler::handle(&svc, cmd).await?;
38+
println!("Turned Switch1 Off");
39+
40+
let query = GetSwitchState {
41+
id: "Switch1".to_string(),
42+
}
43+
.into();
44+
let state = query::Handler::handle(&svc, query).await?;
45+
println!("Switch1 is currently: {:?}", state);
46+
Ok(())
47+
}

0 commit comments

Comments
 (0)