Skip to content

Commit 2992c1d

Browse files
committed
feat(examples): bare minimum "light switch" example
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 2992c1d

File tree

12 files changed

+384
-0
lines changed

12 files changed

+384
-0
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
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
]

examples/light-switch/Cargo.toml

Lines changed: 15 additions & 0 deletions
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" }

examples/light-switch/README.md

Lines changed: 8 additions & 0 deletions
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.
Lines changed: 23 additions & 0 deletions
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+
}
Lines changed: 33 additions & 0 deletions
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+
}
Lines changed: 3 additions & 0 deletions
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;
Lines changed: 34 additions & 0 deletions
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+
}
Lines changed: 34 additions & 0 deletions
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+
}

examples/light-switch/src/domain.rs

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
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+
impl From<eventually::aggregate::Root<LightSwitch>> for LightSwitchRoot {
113+
fn from(root: eventually::aggregate::Root<LightSwitch>) -> Self {
114+
Self(root)
115+
}
116+
}
117+
impl From<LightSwitchRoot> for eventually::aggregate::Root<LightSwitch> {
118+
fn from(value: LightSwitchRoot) -> Self {
119+
value.0
120+
}
121+
}
122+
impl std::ops::Deref for LightSwitchRoot {
123+
type Target = eventually::aggregate::Root<LightSwitch>;
124+
fn deref(&self) -> &Self::Target {
125+
&self.0
126+
}
127+
}
128+
impl std::ops::DerefMut for LightSwitchRoot {
129+
fn deref_mut(&mut self) -> &mut Self::Target {
130+
&mut self.0
131+
}
132+
}
133+
134+
impl LightSwitchRoot {
135+
pub fn install(id: LightSwitchId) -> Result<Self, LightSwitchError> {
136+
aggregate::Root::<LightSwitch>::record_new(
137+
LightSwitchEvent::Installed(Installed { id }).into(),
138+
)
139+
.map(Self)
140+
}
141+
pub fn turn_on(&mut self, id: LightSwitchId) -> Result<(), LightSwitchError> {
142+
self.record_that(LightSwitchEvent::SwitchedOn(SwitchedOn { id }).into())
143+
}
144+
pub fn turn_off(&mut self, id: LightSwitchId) -> Result<(), LightSwitchError> {
145+
self.record_that(LightSwitchEvent::SwitchedOff(SwitchedOff { id }).into())
146+
}
147+
pub fn get_switch_state(&self) -> Result<LightSwitchState, LightSwitchError> {
148+
Ok(self.state.clone())
149+
}
150+
}

examples/light-switch/src/main.rs

Lines changed: 47 additions & 0 deletions
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+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
use async_trait::async_trait;
2+
use eventually::{aggregate, message, query};
3+
4+
use crate::application::LightSwitchService;
5+
use crate::domain::{LightSwitch, LightSwitchId, LightSwitchRoot, LightSwitchState};
6+
7+
#[derive(Debug, Clone, PartialEq, Eq)]
8+
pub struct GetSwitchState {
9+
pub id: LightSwitchId,
10+
}
11+
12+
impl message::Message for GetSwitchState {
13+
fn name(&self) -> &'static str {
14+
"GetSwitch"
15+
}
16+
}
17+
18+
#[async_trait]
19+
impl<R> query::Handler<GetSwitchState> for LightSwitchService<R>
20+
where
21+
R: aggregate::Repository<LightSwitch>,
22+
{
23+
type Error = anyhow::Error;
24+
type Output = LightSwitchState;
25+
26+
async fn handle(
27+
&self,
28+
query: query::Envelope<GetSwitchState>,
29+
) -> Result<LightSwitchState, Self::Error> {
30+
let query = query.message;
31+
let root: LightSwitchRoot = self.light_switch_repository.get(&query.id).await?.into();
32+
let s = root.get_switch_state()?;
33+
Ok(s)
34+
}
35+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pub mod get_switch_state;

0 commit comments

Comments
 (0)