From 8ae539534953b16e613a554d19b6d21ea382d66c Mon Sep 17 00:00:00 2001 From: Zach Waite Date: Mon, 7 Apr 2025 06:21:24 -0400 Subject: [PATCH] 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. --- Cargo.toml | 1 + examples/light-switch/Cargo.toml | 15 ++ examples/light-switch/README.md | 8 + examples/light-switch/src/application.rs | 23 +++ .../src/commands/install_light_switch.rs | 33 ++++ examples/light-switch/src/commands/mod.rs | 3 + .../src/commands/turn_light_switch_off.rs | 34 ++++ .../src/commands/turn_light_switch_on.rs | 34 ++++ examples/light-switch/src/domain.rs | 153 ++++++++++++++++++ examples/light-switch/src/main.rs | 47 ++++++ .../src/queries/get_switch_state.rs | 35 ++++ examples/light-switch/src/queries/mod.rs | 1 + 12 files changed, 387 insertions(+) create mode 100644 examples/light-switch/Cargo.toml create mode 100644 examples/light-switch/README.md create mode 100644 examples/light-switch/src/application.rs create mode 100644 examples/light-switch/src/commands/install_light_switch.rs create mode 100644 examples/light-switch/src/commands/mod.rs create mode 100644 examples/light-switch/src/commands/turn_light_switch_off.rs create mode 100644 examples/light-switch/src/commands/turn_light_switch_on.rs create mode 100644 examples/light-switch/src/domain.rs create mode 100644 examples/light-switch/src/main.rs create mode 100644 examples/light-switch/src/queries/get_switch_state.rs create mode 100644 examples/light-switch/src/queries/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 8e39f678..7e0b3d76 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,4 +6,5 @@ members = [ "eventually-postgres", # Examples "examples/bank-accounting", + "examples/light-switch", ] diff --git a/examples/light-switch/Cargo.toml b/examples/light-switch/Cargo.toml new file mode 100644 index 00000000..d5391164 --- /dev/null +++ b/examples/light-switch/Cargo.toml @@ -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" } diff --git a/examples/light-switch/README.md b/examples/light-switch/README.md new file mode 100644 index 00000000..5a14c5ec --- /dev/null +++ b/examples/light-switch/README.md @@ -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. diff --git a/examples/light-switch/src/application.rs b/examples/light-switch/src/application.rs new file mode 100644 index 00000000..2713580a --- /dev/null +++ b/examples/light-switch/src/application.rs @@ -0,0 +1,23 @@ +use eventually::aggregate; + +use crate::domain::LightSwitch; +pub type LightSwitchRepo = aggregate::EventSourcedRepository; + +#[derive(Clone)] +pub struct LightSwitchService +where + R: aggregate::Repository, +{ + pub light_switch_repository: R, +} + +impl From for LightSwitchService +where + R: aggregate::Repository, +{ + fn from(light_switch_repository: R) -> Self { + Self { + light_switch_repository, + } + } +} diff --git a/examples/light-switch/src/commands/install_light_switch.rs b/examples/light-switch/src/commands/install_light_switch.rs new file mode 100644 index 00000000..ca8f21c7 --- /dev/null +++ b/examples/light-switch/src/commands/install_light_switch.rs @@ -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 command::Handler for LightSwitchService +where + R: aggregate::Repository, +{ + type Error = anyhow::Error; + async fn handle( + &self, + command: command::Envelope, + ) -> 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(()) + } +} diff --git a/examples/light-switch/src/commands/mod.rs b/examples/light-switch/src/commands/mod.rs new file mode 100644 index 00000000..b9d315d8 --- /dev/null +++ b/examples/light-switch/src/commands/mod.rs @@ -0,0 +1,3 @@ +pub mod install_light_switch; +pub mod turn_light_switch_off; +pub mod turn_light_switch_on; diff --git a/examples/light-switch/src/commands/turn_light_switch_off.rs b/examples/light-switch/src/commands/turn_light_switch_off.rs new file mode 100644 index 00000000..17ea92b4 --- /dev/null +++ b/examples/light-switch/src/commands/turn_light_switch_off.rs @@ -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 command::Handler for LightSwitchService +where + R: aggregate::Repository, +{ + type Error = anyhow::Error; + async fn handle( + &self, + command: command::Envelope, + ) -> 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(()) + } +} diff --git a/examples/light-switch/src/commands/turn_light_switch_on.rs b/examples/light-switch/src/commands/turn_light_switch_on.rs new file mode 100644 index 00000000..4947df8d --- /dev/null +++ b/examples/light-switch/src/commands/turn_light_switch_on.rs @@ -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 command::Handler for LightSwitchService +where + R: aggregate::Repository, +{ + type Error = anyhow::Error; + async fn handle( + &self, + command: command::Envelope, + ) -> 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(()) + } +} diff --git a/examples/light-switch/src/domain.rs b/examples/light-switch/src/domain.rs new file mode 100644 index 00000000..2b4d3470 --- /dev/null +++ b/examples/light-switch/src/domain.rs @@ -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, event: Self::Event) -> Result { + 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); + +// 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> for LightSwitchRoot { + fn from(root: eventually::aggregate::Root) -> Self { + Self(root) + } +} +impl From for eventually::aggregate::Root { + fn from(value: LightSwitchRoot) -> Self { + value.0 + } +} +impl std::ops::Deref for LightSwitchRoot { + type Target = eventually::aggregate::Root; + 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 { + aggregate::Root::::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 { + Ok(self.state.clone()) + } +} diff --git a/examples/light-switch/src/main.rs b/examples/light-switch/src/main.rs new file mode 100644 index 00000000..5e576c1c --- /dev/null +++ b/examples/light-switch/src/main.rs @@ -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::::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(()) +} diff --git a/examples/light-switch/src/queries/get_switch_state.rs b/examples/light-switch/src/queries/get_switch_state.rs new file mode 100644 index 00000000..b931890a --- /dev/null +++ b/examples/light-switch/src/queries/get_switch_state.rs @@ -0,0 +1,35 @@ +use async_trait::async_trait; +use eventually::{aggregate, message, query}; + +use crate::application::LightSwitchService; +use crate::domain::{LightSwitch, LightSwitchId, LightSwitchRoot, LightSwitchState}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct GetSwitchState { + pub id: LightSwitchId, +} + +impl message::Message for GetSwitchState { + fn name(&self) -> &'static str { + "GetSwitch" + } +} + +#[async_trait] +impl query::Handler for LightSwitchService +where + R: aggregate::Repository, +{ + type Error = anyhow::Error; + type Output = LightSwitchState; + + async fn handle( + &self, + query: query::Envelope, + ) -> Result { + let query = query.message; + let root: LightSwitchRoot = self.light_switch_repository.get(&query.id).await?.into(); + let s = root.get_switch_state()?; + Ok(s) + } +} diff --git a/examples/light-switch/src/queries/mod.rs b/examples/light-switch/src/queries/mod.rs new file mode 100644 index 00000000..0a887d4a --- /dev/null +++ b/examples/light-switch/src/queries/mod.rs @@ -0,0 +1 @@ +pub mod get_switch_state;