Skip to content

feat(examples): bare minimum "light switch" example #322

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ members = [
"eventually-postgres",
# Examples
"examples/bank-accounting",
"examples/light-switch",
]
15 changes: 15 additions & 0 deletions examples/light-switch/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[package]
name = "light_switch"
version = "0.1.0"
edition = "2021"
readme = "README.md"
publish = false

[dependencies]
serde_json = { version = "1.0.114", optional = true }
serde = { version = "1.0.197", features = ["derive"] }
tokio = { version = "1.36.0", features = ["macros", "rt-multi-thread"] }
thiserror = { version = "2.0.12" }
anyhow = "1.0.97"
async-trait = "0.1.77"
eventually = { features = ["serde-json"], path="../../eventually" }
8 changes: 8 additions & 0 deletions examples/light-switch/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Example: Light Switch application

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.
23 changes: 23 additions & 0 deletions examples/light-switch/src/application.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
use eventually::aggregate;

use crate::domain::LightSwitch;
pub type LightSwitchRepo<S> = aggregate::EventSourcedRepository<LightSwitch, S>;

#[derive(Clone)]
pub struct LightSwitchService<R>
where
R: aggregate::Repository<LightSwitch>,
{
pub light_switch_repository: R,
}

impl<R> From<R> for LightSwitchService<R>
where
R: aggregate::Repository<LightSwitch>,
{
fn from(light_switch_repository: R) -> Self {
Self {
light_switch_repository,
}
}
}
33 changes: 33 additions & 0 deletions examples/light-switch/src/commands/install_light_switch.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
use async_trait::async_trait;
use eventually::{aggregate, command, message};

use crate::application::LightSwitchService;
use crate::domain::{LightSwitch, LightSwitchId, LightSwitchRoot};

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InstallLightSwitch {
pub id: LightSwitchId,
}

impl message::Message for InstallLightSwitch {
fn name(&self) -> &'static str {
"InstallLightSwitch"
}
}

#[async_trait]
impl<R> command::Handler<InstallLightSwitch> for LightSwitchService<R>
where
R: aggregate::Repository<LightSwitch>,
{
type Error = anyhow::Error;
async fn handle(
&self,
command: command::Envelope<InstallLightSwitch>,
) -> Result<(), Self::Error> {
let command = command.message;
let mut light_switch = LightSwitchRoot::install(command.id)?;
self.light_switch_repository.save(&mut light_switch).await?;
Ok(())
}
}
3 changes: 3 additions & 0 deletions examples/light-switch/src/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub mod install_light_switch;
pub mod turn_light_switch_off;
pub mod turn_light_switch_on;
34 changes: 34 additions & 0 deletions examples/light-switch/src/commands/turn_light_switch_off.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
use async_trait::async_trait;
use eventually::{aggregate, command, message};

use crate::application::LightSwitchService;
use crate::domain::{LightSwitch, LightSwitchId, LightSwitchRoot};

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TurnLightSwitchOff {
pub id: LightSwitchId,
}

impl message::Message for TurnLightSwitchOff {
fn name(&self) -> &'static str {
"TurnLightSwitchOff"
}
}

#[async_trait]
impl<R> command::Handler<TurnLightSwitchOff> for LightSwitchService<R>
where
R: aggregate::Repository<LightSwitch>,
{
type Error = anyhow::Error;
async fn handle(
&self,
command: command::Envelope<TurnLightSwitchOff>,
) -> Result<(), Self::Error> {
let command = command.message;
let mut root: LightSwitchRoot = self.light_switch_repository.get(&command.id).await?.into();
let _ = root.turn_off(command.id)?;
self.light_switch_repository.save(&mut root).await?;
Ok(())
}
}
34 changes: 34 additions & 0 deletions examples/light-switch/src/commands/turn_light_switch_on.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
use async_trait::async_trait;
use eventually::{aggregate, command, message};

use crate::application::LightSwitchService;
use crate::domain::{LightSwitch, LightSwitchId, LightSwitchRoot};

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TurnLightSwitchOn {
pub id: LightSwitchId,
}

impl message::Message for TurnLightSwitchOn {
fn name(&self) -> &'static str {
"TurnLightSwitchOn"
}
}

#[async_trait]
impl<R> command::Handler<TurnLightSwitchOn> for LightSwitchService<R>
where
R: aggregate::Repository<LightSwitch>,
{
type Error = anyhow::Error;
async fn handle(
&self,
command: command::Envelope<TurnLightSwitchOn>,
) -> Result<(), Self::Error> {
let command = command.message;
let mut root: LightSwitchRoot = self.light_switch_repository.get(&command.id).await?.into();
let _ = root.turn_on(command.id)?;
self.light_switch_repository.save(&mut root).await?;
Ok(())
}
}
153 changes: 153 additions & 0 deletions examples/light-switch/src/domain.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
use eventually::{aggregate, message};

pub type LightSwitchId = String;

#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum LightSwitchError {
#[error("Light switch has not yet been installed")]
NotYetInstalled,
#[error("Light switch has already been installed")]
AlreadyInstalled,
#[error("Light switch is already on")]
AlreadyOn,
#[error("Light switch is already off")]
AlreadyOff,
}

// events
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct Installed {
id: LightSwitchId,
}

#[derive(Debug, Clone, Eq, PartialEq)]
pub struct SwitchedOn {
id: LightSwitchId,
}

#[derive(Debug, Clone, Eq, PartialEq)]
pub struct SwitchedOff {
id: LightSwitchId,
}

#[derive(Debug, Clone, Eq, PartialEq)]
pub enum LightSwitchEvent {
Installed(Installed),
SwitchedOn(SwitchedOn),
SwitchedOff(SwitchedOff),
}

impl message::Message for LightSwitchEvent {
fn name(&self) -> &'static str {
match self {
LightSwitchEvent::SwitchedOn(_) => "SwitchedOn",
LightSwitchEvent::SwitchedOff(_) => "SwitchedOff",
LightSwitchEvent::Installed(_) => "Installed",
}
}
}

// aggregate
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum LightSwitchState {
On,
Off,
}

#[derive(Debug, Clone)]
pub struct LightSwitch {
id: LightSwitchId,
state: LightSwitchState,
}

impl aggregate::Aggregate for LightSwitch {
type Id = LightSwitchId;
type Event = LightSwitchEvent;
type Error = LightSwitchError;

fn type_name() -> &'static str {
"LightSwitch"
}

fn aggregate_id(&self) -> &Self::Id {
&self.id
}

fn apply(state: Option<Self>, event: Self::Event) -> Result<Self, Self::Error> {
match state {
None => match event {
LightSwitchEvent::Installed(installed) => Ok(LightSwitch {
id: installed.id,
state: LightSwitchState::Off,
}),
LightSwitchEvent::SwitchedOn(_) | LightSwitchEvent::SwitchedOff(_) => {
Err(LightSwitchError::NotYetInstalled)
},
},
Some(mut light_switch) => match event {
LightSwitchEvent::Installed(_) => Err(LightSwitchError::AlreadyInstalled),
LightSwitchEvent::SwitchedOn(_) => match light_switch.state {
LightSwitchState::On => Err(LightSwitchError::AlreadyOn),
LightSwitchState::Off => {
light_switch.state = LightSwitchState::On;
Ok(light_switch)
},
},
LightSwitchEvent::SwitchedOff(_) => match light_switch.state {
LightSwitchState::On => {
light_switch.state = LightSwitchState::Off;
Ok(light_switch)
},
LightSwitchState::Off => Err(LightSwitchError::AlreadyOff),
},
},
}
}
}

// root
#[derive(Debug, Clone)]
pub struct LightSwitchRoot(aggregate::Root<LightSwitch>);

// NOTE: The trait implementations for From, Deref and DerefMut below are
// implemented manually for demonstration purposes, but most would prefer to have them
// auto-generated at compile time by using the [`eventually_macros::aggregate_root`] macro
impl From<eventually::aggregate::Root<LightSwitch>> for LightSwitchRoot {
fn from(root: eventually::aggregate::Root<LightSwitch>) -> Self {
Self(root)
}
}
impl From<LightSwitchRoot> for eventually::aggregate::Root<LightSwitch> {
fn from(value: LightSwitchRoot) -> Self {
value.0
}
}
impl std::ops::Deref for LightSwitchRoot {
type Target = eventually::aggregate::Root<LightSwitch>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl std::ops::DerefMut for LightSwitchRoot {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}

impl LightSwitchRoot {
pub fn install(id: LightSwitchId) -> Result<Self, LightSwitchError> {
aggregate::Root::<LightSwitch>::record_new(
LightSwitchEvent::Installed(Installed { id }).into(),
)
.map(Self)
}
pub fn turn_on(&mut self, id: LightSwitchId) -> Result<(), LightSwitchError> {
self.record_that(LightSwitchEvent::SwitchedOn(SwitchedOn { id }).into())
}
pub fn turn_off(&mut self, id: LightSwitchId) -> Result<(), LightSwitchError> {
self.record_that(LightSwitchEvent::SwitchedOff(SwitchedOff { id }).into())
}
pub fn get_switch_state(&self) -> Result<LightSwitchState, LightSwitchError> {
Ok(self.state.clone())
}
}
47 changes: 47 additions & 0 deletions examples/light-switch/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
mod application;
mod commands;
mod domain;
mod queries;
use application::{LightSwitchRepo, LightSwitchService};
use commands::install_light_switch::InstallLightSwitch;
use commands::turn_light_switch_off::TurnLightSwitchOff;
use commands::turn_light_switch_on::TurnLightSwitchOn;
use domain::{LightSwitchEvent, LightSwitchId};
use eventually::{command, event, query};
use queries::get_switch_state::GetSwitchState;

#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
let store = event::store::InMemory::<LightSwitchId, LightSwitchEvent>::default();
let repo = LightSwitchRepo::from(store.clone());
let svc = LightSwitchService::from(repo);

let cmd = InstallLightSwitch {
id: "Switch1".to_string(),
}
.into();
command::Handler::handle(&svc, cmd).await?;
println!("Installed Switch1");

let cmd = TurnLightSwitchOn {
id: "Switch1".to_string(),
}
.into();
command::Handler::handle(&svc, cmd).await?;
println!("Turned Switch1 On");

let cmd = TurnLightSwitchOff {
id: "Switch1".to_string(),
}
.into();
command::Handler::handle(&svc, cmd).await?;
println!("Turned Switch1 Off");

let query = GetSwitchState {
id: "Switch1".to_string(),
}
.into();
let state = query::Handler::handle(&svc, query).await?;
println!("Switch1 is currently: {:?}", state);
Ok(())
}
Loading
Loading