├── .github └── workflows │ ├── build.yml │ └── publish.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE.md ├── README.md ├── renovate.json ├── src ├── aggregate.rs ├── decider.rs ├── lib.rs ├── materialized_view.rs ├── saga.rs ├── saga_manager.rs ├── specification.rs └── view.rs └── tests ├── aggregate_combined_test.rs ├── aggregate_test.rs ├── api └── mod.rs ├── application └── mod.rs ├── decider_test.rs ├── materialized_view_merged_test.rs ├── materialized_view_test.rs ├── saga_manager_merged_test.rs ├── saga_manager_test.rs ├── saga_test.rs └── view_test.rs /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ${{ matrix.os }} 16 | strategy: 17 | matrix: 18 | os: [ ubuntu-latest, windows-latest, macOS-latest ] 19 | rust: [ stable ] 20 | 21 | steps: 22 | - uses: hecrj/setup-rust-action@v2 23 | with: 24 | rust-version: ${{ matrix.rust }} 25 | 26 | - uses: actions/checkout@v4 27 | 28 | - name: Build 29 | run: cargo build --verbose 30 | 31 | - name: Run tests 32 | run: cargo test --verbose 33 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | release: 5 | types: [ created ] 6 | 7 | env: 8 | CARGO_TERM_COLOR: always 9 | 10 | jobs: 11 | build: 12 | 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | matrix: 16 | os: [ ubuntu-latest ] 17 | rust: [ stable ] 18 | 19 | steps: 20 | - uses: hecrj/setup-rust-action@v2 21 | with: 22 | rust-version: ${{ matrix.rust }} 23 | 24 | - uses: actions/checkout@v4 25 | 26 | - name: Publish 27 | run: cargo publish --token ${CRATES_TOKEN} 28 | env: 29 | CRATES_TOKEN: ${{ secrets.CRATES_TOKEN }} 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | ### IntelliJ IDEA ### 3 | .idea 4 | *.iws 5 | *.iml 6 | *.ipr 7 | 8 | ### NetBeans ### 9 | /nbproject/private/ 10 | /nbbuild/ 11 | /dist/ 12 | /nbdist/ 13 | /.nb-gradle/ 14 | build/ 15 | 16 | ### VS Code ### 17 | .vscode/ 18 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.21.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler" 16 | version = "1.0.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 19 | 20 | [[package]] 21 | name = "backtrace" 22 | version = "0.3.69" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" 25 | dependencies = [ 26 | "addr2line", 27 | "cc", 28 | "cfg-if", 29 | "libc", 30 | "miniz_oxide", 31 | "object", 32 | "rustc-demangle", 33 | ] 34 | 35 | [[package]] 36 | name = "cc" 37 | version = "1.0.83" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" 40 | dependencies = [ 41 | "libc", 42 | ] 43 | 44 | [[package]] 45 | name = "cfg-if" 46 | version = "1.0.0" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 49 | 50 | [[package]] 51 | name = "derive_more" 52 | version = "2.0.1" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" 55 | dependencies = [ 56 | "derive_more-impl", 57 | ] 58 | 59 | [[package]] 60 | name = "derive_more-impl" 61 | version = "2.0.1" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" 64 | dependencies = [ 65 | "proc-macro2", 66 | "quote", 67 | "syn", 68 | "unicode-xid", 69 | ] 70 | 71 | [[package]] 72 | name = "fmodel-rust" 73 | version = "0.8.1" 74 | dependencies = [ 75 | "derive_more", 76 | "serde", 77 | "tokio", 78 | ] 79 | 80 | [[package]] 81 | name = "gimli" 82 | version = "0.28.0" 83 | source = "registry+https://github.com/rust-lang/crates.io-index" 84 | checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" 85 | 86 | [[package]] 87 | name = "libc" 88 | version = "0.2.147" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" 91 | 92 | [[package]] 93 | name = "memchr" 94 | version = "2.6.3" 95 | source = "registry+https://github.com/rust-lang/crates.io-index" 96 | checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" 97 | 98 | [[package]] 99 | name = "miniz_oxide" 100 | version = "0.7.1" 101 | source = "registry+https://github.com/rust-lang/crates.io-index" 102 | checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" 103 | dependencies = [ 104 | "adler", 105 | ] 106 | 107 | [[package]] 108 | name = "object" 109 | version = "0.32.1" 110 | source = "registry+https://github.com/rust-lang/crates.io-index" 111 | checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" 112 | dependencies = [ 113 | "memchr", 114 | ] 115 | 116 | [[package]] 117 | name = "pin-project-lite" 118 | version = "0.2.13" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" 121 | 122 | [[package]] 123 | name = "proc-macro2" 124 | version = "1.0.89" 125 | source = "registry+https://github.com/rust-lang/crates.io-index" 126 | checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" 127 | dependencies = [ 128 | "unicode-ident", 129 | ] 130 | 131 | [[package]] 132 | name = "quote" 133 | version = "1.0.35" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" 136 | dependencies = [ 137 | "proc-macro2", 138 | ] 139 | 140 | [[package]] 141 | name = "rustc-demangle" 142 | version = "0.1.23" 143 | source = "registry+https://github.com/rust-lang/crates.io-index" 144 | checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" 145 | 146 | [[package]] 147 | name = "serde" 148 | version = "1.0.219" 149 | source = "registry+https://github.com/rust-lang/crates.io-index" 150 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 151 | dependencies = [ 152 | "serde_derive", 153 | ] 154 | 155 | [[package]] 156 | name = "serde_derive" 157 | version = "1.0.219" 158 | source = "registry+https://github.com/rust-lang/crates.io-index" 159 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 160 | dependencies = [ 161 | "proc-macro2", 162 | "quote", 163 | "syn", 164 | ] 165 | 166 | [[package]] 167 | name = "syn" 168 | version = "2.0.82" 169 | source = "registry+https://github.com/rust-lang/crates.io-index" 170 | checksum = "83540f837a8afc019423a8edb95b52a8effe46957ee402287f4292fae35be021" 171 | dependencies = [ 172 | "proc-macro2", 173 | "quote", 174 | "unicode-ident", 175 | ] 176 | 177 | [[package]] 178 | name = "tokio" 179 | version = "1.45.1" 180 | source = "registry+https://github.com/rust-lang/crates.io-index" 181 | checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" 182 | dependencies = [ 183 | "backtrace", 184 | "pin-project-lite", 185 | "tokio-macros", 186 | ] 187 | 188 | [[package]] 189 | name = "tokio-macros" 190 | version = "2.5.0" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" 193 | dependencies = [ 194 | "proc-macro2", 195 | "quote", 196 | "syn", 197 | ] 198 | 199 | [[package]] 200 | name = "unicode-ident" 201 | version = "1.0.11" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" 204 | 205 | [[package]] 206 | name = "unicode-xid" 207 | version = "0.2.4" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" 210 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fmodel-rust" 3 | version = "0.8.1" 4 | edition = "2021" 5 | description = "Accelerate development of compositional, safe, and ergonomic applications/information systems by effectively implementing Event Sourcing and CQRS patterns in Rust." 6 | license = "Apache-2.0" 7 | 8 | [dependencies] 9 | serde = {version = "1.0.200", features = ["derive"]} 10 | 11 | 12 | [dev-dependencies] 13 | derive_more = { version = "2", features = ["display"] } 14 | 15 | tokio = { version = "1.43.1", features = ["rt", "rt-multi-thread", "macros"] } 16 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2023 Fraktalio D.O.O. All rights reserved. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the 4 | License. You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an " 9 | AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific 10 | language governing permissions and limitations under the License. 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # **f`(`model`)`** - Functional Domain Modeling with Rust 2 | 3 | > Publicly available at [crates.io](https://crates.io/crates/fmodel-rust) and [docs.rs](https://docs.rs/fmodel-rust/latest/fmodel_rust/) 4 | 5 | > From version 0.7.0+, the library is using [`async fn` in Traits](https://blog.rust-lang.org/2023/12/21/async-fn-rpit-in-traits.html) feature, which is currently available only in stable Rust 1.75.0+. 6 | 7 | > If you are using older version of Rust, please use version 0.6.0 of the library. It depends on `async-trait` crate. Version 0.6.0 is not maintained anymore, only patched for security issues and bugs. 8 | 9 | 10 | * [**f`(`model`)`** - Functional Domain Modeling with Rust](#fmodel---functional-domain-modeling-with-rust) 11 | * [`IOR`](#iorlibrary-inspiration) 12 | * [Abstraction and generalization](#abstraction-and-generalization) 13 | * [`Box Result, Error>`](#boxdyn-fnc-s---vece) 14 | * [`Box S>`](#boxdyn-fns-e---s) 15 | * [Decider](#decider) 16 | * [Event-sourcing aggregate](#event-sourcing-aggregate) 17 | * [State-stored aggregate](#state-stored-aggregate) 18 | * [View](#view) 19 | * [Materialized View](#materialized-view) 20 | * [Algebraic Data Types](#algebraic-data-types) 21 | * [`C` / Command / Intent to change the state of the system](#c--command--intent-to-change-the-state-of-the-system) 22 | * [`E` / Event / Fact](#e--event--fact) 23 | * [`S` / State / Current state of the system/aggregate/entity](#s--state--current-state-of-the-systemaggregateentity) 24 | * [Modeling the Behaviour of our domain](#modeling-the-behaviour-of-our-domain) 25 | * [The Application layer](#the-application-layer) 26 | * [Fearless Concurrency](#fearless-concurrency) 27 | * [Install the crate as a dependency of your project](#install-the-crate-as-a-dependency-of-your-project) 28 | * [Examples](#examples) 29 | * [FModel in other languages](#fmodel-in-other-languages) 30 | * [Further reading](#further-reading) 31 | * [Credits](#credits) 32 | 33 | 34 | When you’re developing an information system to automate the activities of the business, you are modeling the business. 35 | The abstractions that you design, the behaviors that you implement, and the UI interactions that you build all reflect 36 | the business — together, they constitute the model of the domain. 37 | 38 | ![event-modeling](https://github.com/fraktalio/fmodel-ts/raw/main/.assets/event-modeling.png) 39 | 40 | ## `IOR` 41 | 42 | This project can be used as a library, or as an inspiration, or both. It provides just enough tactical Domain-Driven 43 | Design patterns, optimised for Event Sourcing and CQRS. 44 | 45 | ## Abstraction and generalization 46 | 47 | Abstractions can hide irrelevant details and use names to reference objects. It emphasizes what an object is or does 48 | rather than how it is represented or how it works. 49 | 50 | Generalization reduces complexity by replacing multiple entities which perform similar functions with a single 51 | construct. 52 | 53 | Abstraction and generalization are often used together. Abstracts are generalized through parameterization to provide 54 | more excellent utility. 55 | 56 | ## `Box Result, Error>` 57 | 58 | `type DecideFunction<'a, C, S, E> = Box Result, Error> + 'a + Send + Sync>` 59 | 60 | On a higher level of abstraction, any information system is responsible for handling the intent (`Command`) and based on 61 | the current `State`, produce new facts (`Events`): 62 | 63 | - given the current `State/S` *on the input*, 64 | - when `Command/C` is handled *on the input*, 65 | - expect `Vec` of new `Events/E` to be published/emitted *on the output* 66 | 67 | ## `Box S>` 68 | 69 | `type EvolveFunction<'a, S, E> = Box S + 'a + Send + Sync>` 70 | 71 | The new state is always evolved out of the current state `S` and the current event `E`: 72 | 73 | - given the current `State/S` *on the input*, 74 | - when `Event/E` is handled *on the input*, 75 | - expect new `State/S` to be published *on the output* 76 | 77 | Two functions are wrapped in a datatype class (algebraic data structure), which is generalized with three generic 78 | parameters: 79 | 80 | ```rust 81 | pub struct Decider<'a, C: 'a, S: 'a, E: 'a> { 82 | pub decide: DecideFunction<'a, C, S, E>, 83 | pub evolve: EvolveFunction<'a, S, E>, 84 | pub initial_state: InitialStateFunction<'a, S>, 85 | } 86 | ``` 87 | 88 | `Decider` is the most important datatype, but it is not the only one. There are others: 89 | 90 | ![onion architecture image](https://github.com/fraktalio/fmodel/blob/d8643a7d0de30b79f0b904f7a40233419c463fc8/.assets/onion.png?raw=true) 91 | 92 | ## Decider 93 | 94 | `Decider` is a datatype/struct that represents the main decision-making algorithm. It belongs to the Domain layer. It 95 | has three 96 | generic parameters `C`, `S`, `E` , representing the type of the values that `Decider` may contain or use. 97 | `Decider` can be specialized for any type `C` or `S` or `E` because these types do not affect its 98 | behavior. `Decider` behaves the same for `C`=`Int` or `C`=`YourCustomType`, for example. 99 | 100 | `Decider` is a pure domain component. 101 | 102 | - `C` - Command 103 | - `S` - State 104 | - `E` - Event 105 | 106 | ```rust 107 | pub type DecideFunction<'a, C, S, E, Error> = Box Result, Error> + 'a + Send + Sync>; 108 | pub type EvolveFunction<'a, S, E> = Box S + 'a + Send + Sync>; 109 | pub type InitialStateFunction<'a, S> = Box S + 'a + Send + Sync>; 110 | 111 | pub struct Decider<'a, C: 'a, S: 'a, E: 'a> { 112 | pub decide: DecideFunction<'a, C, S, E>, 113 | pub evolve: EvolveFunction<'a, S, E>, 114 | pub initial_state: InitialStateFunction<'a, S>, 115 | } 116 | ``` 117 | 118 | Additionally, `initialState` of the Decider is introduced to gain more control over the initial state of the Decider. 119 | 120 | ### Event-sourcing aggregate 121 | 122 | [Event sourcing aggregate](src/aggregate.rs) is using/delegating a `Decider` to handle commands and produce new events. 123 | It belongs to the 124 | Application layer. In order to 125 | handle the command, aggregate needs to fetch the current state (represented as a list/vector of events) 126 | via `EventRepository.fetchEvents` async function, and then delegate the command to the decider which can produce new 127 | events as 128 | a result. Produced events are then stored via `EventRepository.save` async function. 129 | 130 | It is a formalization of the event sourced information system. 131 | 132 | ### State-stored aggregate 133 | 134 | [State stored aggregate](src/aggregate.rs) is using/delegating a `Decider` to handle commands and produce new state. It 135 | belongs to the 136 | Application layer. In order to 137 | handle the command, aggregate needs to fetch the current state via `StateRepository.fetchState` async function first, 138 | and then 139 | delegate the command to the decider which can produce new state as a result. New state is then stored 140 | via `StateRepository.save` async function. 141 | 142 | It is a formalization of the state stored information system. 143 | 144 | 145 | ## View 146 | 147 | `View` is a datatype that represents the event handling algorithm, responsible for translating the events into 148 | denormalized state, which is more adequate for querying. It belongs to the Domain layer. It is usually used to create 149 | the view/query side of the CQRS pattern. Obviously, the command side of the CQRS is usually event-sourced aggregate. 150 | 151 | It has two generic parameters `S`, `E`, representing the type of the values that `View` may contain or use. 152 | `View` can be specialized for any type of `S`, `E` because these types do not affect its behavior. 153 | `View` behaves the same for `E`=`Int` or `E`=`YourCustomType`, for example. 154 | 155 | `View` is a pure domain component. 156 | 157 | - `S` - State 158 | - `E` - Event 159 | 160 | ```rust 161 | pub struct View<'a, S: 'a, E: 'a> { 162 | pub evolve: EvolveFunction<'a, S, E>, 163 | pub initial_state: InitialStateFunction<'a, S>, 164 | } 165 | ``` 166 | 167 | ### Materialized View 168 | 169 | [Materialized view](src/materialized_view.rs) is using/delegating a `View` to handle events of type `E` and to maintain 170 | a state of denormalized 171 | projection(s) as a 172 | result. Essentially, it represents the query/view side of the CQRS pattern. It belongs to the Application layer. 173 | 174 | In order to handle the event, materialized view needs to fetch the current state via `ViewStateRepository.fetchState` 175 | suspending function first, and then delegate the event to the view, which can produce new state as a result. New state 176 | is then stored via `ViewStateRepository.save` suspending function. 177 | 178 | ## Algebraic Data Types 179 | 180 | In Rust, we can use ADTs to model our application's domain entities and relationships in a functional way, clearly defining the set of possible values and states. 181 | Rust has two main types of ADTs: `enum` and `struct`. 182 | 183 | - `enum` is used to define a type that can take on one of several possible variants - modeling a `sum/OR` type. 184 | - `struct` is used to express a type that has named fields - modeling a `product/AND` type. 185 | 186 | ADTs will help with 187 | 188 | - representing the business domain in the code accurately 189 | - enforcing correctness 190 | - reducing the likelihood of bugs. 191 | 192 | In FModel, we extensively use ADTs to model the data. 193 | 194 | ### `C` / Command / Intent to change the state of the system 195 | 196 | ```rust 197 | // models Sum/Or type / multiple possible variants 198 | pub enum OrderCommand { 199 | Create(CreateOrderCommand), 200 | Update(UpdateOrderCommand), 201 | Cancel(CancelOrderCommand), 202 | } 203 | // models Product/And type / a concrete variant, consisting of named fields 204 | pub struct CreateOrderCommand { 205 | pub order_id: u32, 206 | pub customer_name: String, 207 | pub items: Vec, 208 | } 209 | // models Product/And type / a concrete variant, consisting of named fields 210 | pub struct UpdateOrderCommand { 211 | pub order_id: u32, 212 | pub new_items: Vec, 213 | } 214 | // models Product/And type / a concrete variant, consisting of named fields 215 | #[derive(Debug)] 216 | pub struct CancelOrderCommand { 217 | pub order_id: u32, 218 | } 219 | ``` 220 | 221 | ### `E` / Event / Fact 222 | 223 | ```rust 224 | // models Sum/Or type / multiple possible variants 225 | pub enum OrderEvent { 226 | Created(OrderCreatedEvent), 227 | Updated(OrderUpdatedEvent), 228 | Cancelled(OrderCancelledEvent), 229 | } 230 | // models Product/And type / a concrete variant, consisting of named fields 231 | pub struct OrderCreatedEvent { 232 | pub order_id: u32, 233 | pub customer_name: String, 234 | pub items: Vec, 235 | } 236 | // models Product/And type / a concrete variant, consisting of named fields 237 | pub struct OrderUpdatedEvent { 238 | pub order_id: u32, 239 | pub updated_items: Vec, 240 | } 241 | // models Product/And type / a concrete variant, consisting of named fields 242 | pub struct OrderCancelledEvent { 243 | pub order_id: u32, 244 | } 245 | 246 | ``` 247 | 248 | ### `S` / State / Current state of the system/aggregate/entity 249 | 250 | ```rust 251 | struct OrderState { 252 | order_id: u32, 253 | customer_name: String, 254 | items: Vec, 255 | is_cancelled: bool, 256 | } 257 | ``` 258 | 259 | ## Modeling the Behaviour of our domain 260 | 261 | - algebraic data types form the structure of our entities (commands, state, and events). 262 | - functions/lambda offers the algebra of manipulating the entities in a compositional manner, effectively modeling the behavior. 263 | 264 | This leads to modularity in design and a clear separation of the entity’s structure and functions/behaviour of the entity. 265 | 266 | Fmodel library offers generic and abstract components to specialize in for your specific case/expected behavior: 267 | 268 | - Decider - data type that represents the main decision-making algorithm. 269 | 270 | ```rust 271 | fn decider<'a>() -> Decider<'a, OrderCommand, OrderState, OrderEvent> { 272 | Decider { 273 | decide: Box::new(|command, state| match command { 274 | OrderCommand::Create(cmd) => Ok(vec![OrderEvent::Created(OrderCreatedEvent { 275 | order_id: cmd.order_id, 276 | customer_name: cmd.customer_name.to_owned(), 277 | items: cmd.items.to_owned(), 278 | })]), 279 | OrderCommand::Update(cmd) => { 280 | if state.order_id == cmd.order_id { 281 | Ok(vec![OrderEvent::Updated(OrderUpdatedEvent { 282 | order_id: cmd.order_id, 283 | updated_items: cmd.new_items.to_owned(), 284 | })]) 285 | } else { 286 | Ok(vec![]) 287 | } 288 | } 289 | OrderCommand::Cancel(cmd) => { 290 | if state.order_id == cmd.order_id { 291 | Ok(vec![OrderEvent::Cancelled(OrderCancelledEvent { 292 | order_id: cmd.order_id, 293 | })]) 294 | } else { 295 | Ok(vec![]) 296 | } 297 | } 298 | }), 299 | evolve: Box::new(|state, event| { 300 | let mut new_state = state.clone(); 301 | match event { 302 | OrderEvent::Created(evt) => { 303 | new_state.order_id = evt.order_id; 304 | new_state.customer_name = evt.customer_name.to_owned(); 305 | new_state.items = evt.items.to_owned(); 306 | } 307 | OrderEvent::Updated(evt) => { 308 | new_state.items = evt.updated_items.to_owned(); 309 | } 310 | OrderEvent::Cancelled(_) => { 311 | new_state.is_cancelled = true; 312 | } 313 | } 314 | new_state 315 | }), 316 | initial_state: Box::new(|| OrderState { 317 | order_id: 0, 318 | customer_name: "".to_string(), 319 | items: Vec::new(), 320 | is_cancelled: false, 321 | }), 322 | } 323 | } 324 | ``` 325 | - View - represents the event handling algorithm responsible for translating the events into the denormalized state, which is adequate for querying. 326 | 327 | ```rust 328 | // The state of the view component 329 | struct OrderViewState { 330 | order_id: u32, 331 | customer_name: String, 332 | items: Vec, 333 | is_cancelled: bool, 334 | } 335 | 336 | fn view<'a>() -> View<'a, OrderViewState, OrderEvent> { 337 | View { 338 | // Evolve the state of the `view` based on the event(s) 339 | evolve: Box::new(|state, event| { 340 | let mut new_state = state.clone(); 341 | // Exhaustive pattern matching on the event 342 | match event { 343 | OrderEvent::Created(created_event) => { 344 | new_state.order_id = created_event.order_id; 345 | new_state.customer_name = created_event.customer_name.to_owned(); 346 | new_state.items = created_event.items.to_owned(); 347 | } 348 | OrderEvent::Updated(updated_event) => { 349 | new_state.items = updated_event.updated_items.to_owned(); 350 | } 351 | OrderEvent::Cancelled(_) => { 352 | new_state.is_cancelled = true; 353 | } 354 | } 355 | new_state 356 | }), 357 | // Initial state 358 | initial_state: Box::new(|| OrderViewState { 359 | order_id: 0, 360 | customer_name: "".to_string(), 361 | items: Vec::new(), 362 | is_cancelled: false, 363 | }), 364 | } 365 | } 366 | 367 | ``` 368 | 369 | ## The Application layer 370 | 371 | The logic execution will be orchestrated by the outside components that use the domain components (decider, view) to do the computations. These components will be responsible for fetching and saving the data (repositories). 372 | 373 | 374 | The arrows in the image (adapters->application->domain) show the direction of the dependency. Notice that all dependencies point inward and that Domain does not depend on anybody or anything. 375 | 376 | Pushing these decisions from the core domain model is very valuable. Being able to postpone them is a sign of good architecture. 377 | 378 | **Event-sourcing aggregate** 379 | 380 | ```rust 381 | let repository = InMemoryOrderEventRepository::new(); 382 | let aggregate = EventSourcedAggregate::new(repository, decider()); 383 | 384 | let command = OrderCommand::Create(CreateOrderCommand { 385 | order_id: 1, 386 | customer_name: "John Doe".to_string(), 387 | items: vec!["Item 1".to_string(), "Item 2".to_string()], 388 | }); 389 | 390 | let result = aggregate.handle(&command).await; 391 | assert!(result.is_ok()); 392 | assert_eq!( 393 | result.unwrap(), 394 | [( 395 | OrderEvent::Created(OrderCreatedEvent { 396 | order_id: 1, 397 | customer_name: "John Doe".to_string(), 398 | items: vec!["Item 1".to_string(), "Item 2".to_string()], 399 | }), 400 | 0 401 | )] 402 | ); 403 | ``` 404 | 405 | **State-stored aggregate** 406 | ```rust 407 | let repository = InMemoryOrderStateRepository::new(); 408 | let aggregate = StateStoredAggregate::new(repository, decider()); 409 | 410 | let command = OrderCommand::Create(CreateOrderCommand { 411 | order_id: 1, 412 | customer_name: "John Doe".to_string(), 413 | items: vec!["Item 1".to_string(), "Item 2".to_string()], 414 | }); 415 | let result = aggregate.handle(&command).await; 416 | assert!(result.is_ok()); 417 | assert_eq!( 418 | result.unwrap(), 419 | ( 420 | OrderState { 421 | order_id: 1, 422 | customer_name: "John Doe".to_string(), 423 | items: vec!["Item 1".to_string(), "Item 2".to_string()], 424 | is_cancelled: false, 425 | }, 426 | 0 427 | ) 428 | ); 429 | ``` 430 | 431 | ## Fearless Concurrency 432 | 433 | Splitting the computation in your program into multiple threads to run multiple tasks at the same time can improve performance. 434 | However, programming with threads has a reputation for being difficult. Rust’s type system and ownership model guarantee thread safety. 435 | 436 | Example of the concurrent execution of the aggregate: 437 | 438 | ```rust 439 | async fn es_test() { 440 | let repository = InMemoryOrderEventRepository::new(); 441 | let aggregate = Arc::new(EventSourcedAggregate::new(repository, decider())); 442 | // Makes a clone of the Arc pointer. This creates another pointer to the same allocation, increasing the strong reference count. 443 | let aggregate2 = Arc::clone(&aggregate); 444 | 445 | // Lets spawn two threads to simulate two concurrent requests 446 | let handle1 = thread::spawn(|| async move { 447 | let command = OrderCommand::Create(CreateOrderCommand { 448 | order_id: 1, 449 | customer_name: "John Doe".to_string(), 450 | items: vec!["Item 1".to_string(), "Item 2".to_string()], 451 | }); 452 | 453 | let result = aggregate.handle(&command).await; 454 | assert!(result.is_ok()); 455 | assert_eq!( 456 | result.unwrap(), 457 | [( 458 | OrderEvent::Created(OrderCreatedEvent { 459 | order_id: 1, 460 | customer_name: "John Doe".to_string(), 461 | items: vec!["Item 1".to_string(), "Item 2".to_string()], 462 | }), 463 | 0 464 | )] 465 | ); 466 | let command = OrderCommand::Update(UpdateOrderCommand { 467 | order_id: 1, 468 | new_items: vec!["Item 3".to_string(), "Item 4".to_string()], 469 | }); 470 | let result = aggregate.handle(&command).await; 471 | assert!(result.is_ok()); 472 | assert_eq!( 473 | result.unwrap(), 474 | [( 475 | OrderEvent::Updated(OrderUpdatedEvent { 476 | order_id: 1, 477 | updated_items: vec!["Item 3".to_string(), "Item 4".to_string()], 478 | }), 479 | 1 480 | )] 481 | ); 482 | let command = OrderCommand::Cancel(CancelOrderCommand { order_id: 1 }); 483 | let result = aggregate.handle(&command).await; 484 | assert!(result.is_ok()); 485 | assert_eq!( 486 | result.unwrap(), 487 | [( 488 | OrderEvent::Cancelled(OrderCancelledEvent { order_id: 1 }), 489 | 2 490 | )] 491 | ); 492 | }); 493 | 494 | let handle2 = thread::spawn(|| async move { 495 | let command = OrderCommand::Create(CreateOrderCommand { 496 | order_id: 2, 497 | customer_name: "John Doe".to_string(), 498 | items: vec!["Item 1".to_string(), "Item 2".to_string()], 499 | }); 500 | let result = aggregate2.handle(&command).await; 501 | assert!(result.is_ok()); 502 | assert_eq!( 503 | result.unwrap(), 504 | [( 505 | OrderEvent::Created(OrderCreatedEvent { 506 | order_id: 2, 507 | customer_name: "John Doe".to_string(), 508 | items: vec!["Item 1".to_string(), "Item 2".to_string()], 509 | }), 510 | 0 511 | )] 512 | ); 513 | let command = OrderCommand::Update(UpdateOrderCommand { 514 | order_id: 2, 515 | new_items: vec!["Item 3".to_string(), "Item 4".to_string()], 516 | }); 517 | let result = aggregate2.handle(&command).await; 518 | assert!(result.is_ok()); 519 | assert_eq!( 520 | result.unwrap(), 521 | [( 522 | OrderEvent::Updated(OrderUpdatedEvent { 523 | order_id: 2, 524 | updated_items: vec!["Item 3".to_string(), "Item 4".to_string()], 525 | }), 526 | 1 527 | )] 528 | ); 529 | let command = OrderCommand::Cancel(CancelOrderCommand { order_id: 2 }); 530 | let result = aggregate2.handle(&command).await; 531 | assert!(result.is_ok()); 532 | assert_eq!( 533 | result.unwrap(), 534 | [( 535 | OrderEvent::Cancelled(OrderCancelledEvent { order_id: 2 }), 536 | 2 537 | )] 538 | ); 539 | }); 540 | 541 | handle1.join().unwrap().await; 542 | handle2.join().unwrap().await; 543 | } 544 | ``` 545 | 546 | You might wonder why all primitive types in Rust aren’t atomic and why standard library types aren’t implemented to use `Arc` by default. 547 | The reason is that thread safety comes with a performance penalty that you only want to pay when you really need to. 548 | 549 | **You choose how to run it!** You can run it in a single-threaded, multi-threaded, or distributed environment. 550 | 551 | ## Install the crate as a dependency of your project 552 | 553 | Run the following Cargo command in your project directory: 554 | ```shell 555 | cargo add fmodel-rust 556 | ``` 557 | Or add the following line to your `Cargo.toml` file: 558 | 559 | ```toml 560 | fmodel-rust = "0.8.0" 561 | ``` 562 | 563 | ## Examples 564 | 565 | - [Restaurant Demo - with Postgres](https://github.com/fraktalio/fmodel-rust-demo) 566 | - [Gift Card Demo - with Axon](https://github.com/AxonIQ/axon-rust/tree/main/gift-card-rust) 567 | - [Tests](tests) 568 | 569 | 570 | ## FModel in other languages 571 | 572 | - [FModel Kotlin](https://github.com/fraktalio/fmodel/) 573 | - [FModel TypeScript](https://github.com/fraktalio/fmodel-ts/) 574 | - [FModel Java](https://github.com/fraktalio/fmodel-java/) 575 | 576 | ## Further reading 577 | 578 | - [https://doc.rust-lang.org/book/](https://doc.rust-lang.org/book/) 579 | - [https://fraktalio.com/fmodel/](https://fraktalio.com/fmodel/) 580 | - [https://fraktalio.com/fmodel-ts/](https://fraktalio.com/fmodel-ts/) 581 | - [https://xebia.com/blog/functional-domain-modeling-in-rust-part-1/](https://xebia.com/blog/functional-domain-modeling-in-rust-part-1/) 582 | 583 | 584 | ## Credits 585 | 586 | Special credits to `Jérémie Chassaing` for sharing his [research](https://www.youtube.com/watch?v=kgYGMVDHQHs) 587 | and `Adam Dymitruk` for hosting the meetup. 588 | 589 | --- 590 | Created with :heart: by [Fraktalio](https://fraktalio.com/) 591 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /src/aggregate.rs: -------------------------------------------------------------------------------- 1 | use std::future::Future; 2 | use std::marker::PhantomData; 3 | 4 | use crate::decider::{Decider, EventComputation, StateComputation}; 5 | use crate::saga::{ActionComputation, Saga}; 6 | use crate::Identifier; 7 | 8 | /// Event Repository trait 9 | /// 10 | /// Generic parameters: 11 | /// 12 | /// - `C` - Command 13 | /// - `E` - Event 14 | /// - `Version` - Version/Offset/Sequence number 15 | /// - `Error` - Error 16 | pub trait EventRepository { 17 | /// Fetches current events, based on the command. 18 | /// Desugared `async fn fetch_events(&self, command: &C) -> Result, Error>;` to a normal `fn` that returns `impl Future`, and adds bound `Send`. 19 | /// You can freely move between the `async fn` and `-> impl Future` spelling in your traits and impls. This is true even when one form has a Send bound. 20 | fn fetch_events( 21 | &self, 22 | command: &C, 23 | ) -> impl Future, Error>> + Send; 24 | /// Saves events. 25 | /// Desugared `async fn save(&self, events: &[E], latest_version: &Option) -> Result, Error>;` to a normal `fn` that returns `impl Future`, and adds bound `Send` 26 | /// You can freely move between the `async fn` and `-> impl Future` spelling in your traits and impls. This is true even when one form has a Send bound. 27 | fn save(&self, events: &[E]) -> impl Future, Error>> + Send; 28 | 29 | /// Version provider. It is used to provide the version/sequence of the stream to wich this event belongs to. Optimistic locking is useing this version to check if the event is already saved. 30 | /// Desugared `async fn version_provider(&self, event: &E) -> Result, Error>;` to a normal `fn` that returns `impl Future`, and adds bound `Send` 31 | /// You can freely move between the `async fn` and `-> impl Future` spelling in your traits and impls. This is true even when one form has a Send bound. 32 | fn version_provider( 33 | &self, 34 | event: &E, 35 | ) -> impl Future, Error>> + Send; 36 | } 37 | 38 | /// Event Sourced Aggregate. 39 | /// 40 | /// It is using a `Decider` / [EventComputation] to compute new events based on the current events and the command. 41 | /// It is using a [EventRepository] to fetch the current events and to save the new events. 42 | /// 43 | /// Generic parameters: 44 | /// 45 | /// - `C` - Command 46 | /// - `S` - State 47 | /// - `E` - Event 48 | /// - `Repository` - Event repository 49 | /// - `Decider` - Event computation 50 | /// - `Version` - Version/Offset/Sequence number 51 | /// - `Error` - Error 52 | pub struct EventSourcedAggregate 53 | where 54 | Repository: EventRepository, 55 | Decider: EventComputation, 56 | { 57 | repository: Repository, 58 | decider: Decider, 59 | _marker: PhantomData<(C, S, E, Version, Error)>, 60 | } 61 | 62 | impl EventComputation 63 | for EventSourcedAggregate 64 | where 65 | Repository: EventRepository, 66 | Decider: EventComputation, 67 | { 68 | /// Computes new events based on the current events and the command. 69 | fn compute_new_events(&self, current_events: &[E], command: &C) -> Result, Error> { 70 | self.decider.compute_new_events(current_events, command) 71 | } 72 | } 73 | 74 | impl EventRepository 75 | for EventSourcedAggregate 76 | where 77 | Repository: EventRepository + Sync, 78 | Decider: EventComputation + Sync, 79 | C: Sync, 80 | S: Sync, 81 | E: Sync, 82 | Version: Sync, 83 | Error: Sync, 84 | { 85 | /// Fetches current events, based on the command. 86 | async fn fetch_events(&self, command: &C) -> Result, Error> { 87 | self.repository.fetch_events(command).await 88 | } 89 | /// Saves events. 90 | async fn save(&self, events: &[E]) -> Result, Error> { 91 | self.repository.save(events).await 92 | } 93 | /// Version provider. It is used to provide the version/sequence of the event. Optimistic locking is useing this version to check if the event is already saved. 94 | async fn version_provider(&self, event: &E) -> Result, Error> { 95 | self.repository.version_provider(event).await 96 | } 97 | } 98 | 99 | impl 100 | EventSourcedAggregate 101 | where 102 | Repository: EventRepository + Sync, 103 | Decider: EventComputation + Sync, 104 | C: Sync, 105 | S: Sync, 106 | E: Sync, 107 | Version: Sync, 108 | Error: Sync, 109 | { 110 | /// Creates a new instance of [EventSourcedAggregate]. 111 | pub fn new(repository: Repository, decider: Decider) -> Self { 112 | EventSourcedAggregate { 113 | repository, 114 | decider, 115 | _marker: PhantomData, 116 | } 117 | } 118 | /// Handles the command by fetching the events from the repository, computing new events based on the current events and the command, and saving the new events to the repository. 119 | pub async fn handle(&self, command: &C) -> Result, Error> { 120 | let events: Vec<(E, Version)> = self.fetch_events(command).await?; 121 | let mut current_events: Vec = vec![]; 122 | for (event, _) in events { 123 | current_events.push(event); 124 | } 125 | let new_events = self.compute_new_events(¤t_events, command)?; 126 | let saved_events = self.save(&new_events).await?; 127 | Ok(saved_events) 128 | } 129 | } 130 | 131 | /// State Repository trait 132 | /// 133 | /// Generic parameters: 134 | /// 135 | /// - `C` - Command 136 | /// - `S` - State 137 | /// - `Version` - Version 138 | /// - `Error` - Error 139 | pub trait StateRepository { 140 | /// Fetches current state, based on the command. 141 | /// Desugared `async fn fetch_state(&self, command: &C) -> Result, Error>;` to a normal `fn` that returns `impl Future` and adds bound `Send` 142 | /// You can freely move between the `async fn` and `-> impl Future` spelling in your traits and impls. This is true even when one form has a Send bound. 143 | fn fetch_state( 144 | &self, 145 | command: &C, 146 | ) -> impl Future, Error>> + Send; 147 | /// Saves state. 148 | /// Desugared `async fn save(&self, state: &S, version: &Option) -> Result<(S, Version), Error>;` to a normal `fn` that returns `impl Future` and adds bound `Send` 149 | /// You can freely move between the `async fn` and `-> impl Future` spelling in your traits and impls. This is true even when one form has a Send bound. 150 | fn save( 151 | &self, 152 | state: &S, 153 | version: &Option, 154 | ) -> impl Future> + Send; 155 | } 156 | 157 | /// State Stored Aggregate. 158 | /// 159 | /// It is using a `Decider` / [StateComputation] to compute new state based on the current state and the command. 160 | /// It is using a [StateRepository] to fetch the current state and to save the new state. 161 | /// 162 | /// Generic parameters: 163 | /// 164 | /// - `C` - Command 165 | /// - `S` - State 166 | /// - `E` - Event 167 | /// - `Repository` - State repository 168 | /// - `Decider` - State computation 169 | /// - `Version` - Version 170 | /// - `Error` - Error 171 | pub struct StateStoredAggregate 172 | where 173 | Repository: StateRepository, 174 | Decider: StateComputation, 175 | { 176 | repository: Repository, 177 | decider: Decider, 178 | _marker: PhantomData<(C, S, E, Version, Error)>, 179 | } 180 | 181 | impl StateComputation 182 | for StateStoredAggregate 183 | where 184 | Repository: StateRepository, 185 | Decider: StateComputation, 186 | { 187 | /// Computes new state based on the current state and the command. 188 | fn compute_new_state(&self, current_state: Option, command: &C) -> Result { 189 | self.decider.compute_new_state(current_state, command) 190 | } 191 | } 192 | 193 | impl StateRepository 194 | for StateStoredAggregate 195 | where 196 | Repository: StateRepository + Sync, 197 | Decider: StateComputation + Sync, 198 | C: Sync, 199 | S: Sync, 200 | E: Sync, 201 | Version: Sync, 202 | Error: Sync, 203 | { 204 | /// Fetches current state, based on the command. 205 | async fn fetch_state(&self, command: &C) -> Result, Error> { 206 | self.repository.fetch_state(command).await 207 | } 208 | /// Saves state. 209 | async fn save(&self, state: &S, version: &Option) -> Result<(S, Version), Error> { 210 | self.repository.save(state, version).await 211 | } 212 | } 213 | 214 | impl 215 | StateStoredAggregate 216 | where 217 | Repository: StateRepository + Sync, 218 | Decider: StateComputation + Sync, 219 | C: Sync, 220 | S: Sync, 221 | E: Sync, 222 | Version: Sync, 223 | Error: Sync, 224 | { 225 | /// Creates a new instance of [StateStoredAggregate]. 226 | pub fn new(repository: Repository, decider: Decider) -> Self { 227 | StateStoredAggregate { 228 | repository, 229 | decider, 230 | _marker: PhantomData, 231 | } 232 | } 233 | /// Handles the command by fetching the state from the repository, computing new state based on the current state and the command, and saving the new state to the repository. 234 | pub async fn handle(&self, command: &C) -> Result<(S, Version), Error> { 235 | let state_version = self.fetch_state(command).await?; 236 | match state_version { 237 | None => { 238 | let new_state = self.compute_new_state(None, command)?; 239 | let saved_state = self.save(&new_state, &None).await?; 240 | Ok(saved_state) 241 | } 242 | Some((state, version)) => { 243 | let new_state = self.compute_new_state(Some(state), command)?; 244 | let saved_state = self.save(&new_state, &Some(version)).await?; 245 | Ok(saved_state) 246 | } 247 | } 248 | } 249 | } 250 | 251 | /// Orchestrating Event Sourced Aggregate. 252 | /// It is using a [Decider] and [Saga] to compute new events based on the current events and the command. 253 | /// If the `decider` is combined out of many deciders via `combine` function, a `saga` could be used to react on new events and send new commands to the `decider` recursively, in single transaction. 254 | /// It is using a [EventRepository] to fetch the current events and to save the new events. 255 | /// Generic parameters: 256 | /// - `C` - Command 257 | /// - `S` - State 258 | /// - `E` - Event 259 | /// - `Repository` - Event repository 260 | /// - `Version` - Version/Offset/Sequence number 261 | /// - `Error` - Error 262 | pub struct EventSourcedOrchestratingAggregate<'a, C, S, E, Repository, Version, Error> 263 | where 264 | Repository: EventRepository, 265 | { 266 | repository: Repository, 267 | decider: Decider<'a, C, S, E, Error>, 268 | saga: Saga<'a, E, C>, 269 | _marker: PhantomData<(C, S, E, Version, Error)>, 270 | } 271 | 272 | impl EventRepository 273 | for EventSourcedOrchestratingAggregate<'_, C, S, E, Repository, Version, Error> 274 | where 275 | Repository: EventRepository + Sync, 276 | C: Sync, 277 | S: Sync, 278 | E: Sync, 279 | Version: Sync, 280 | Error: Sync, 281 | { 282 | /// Fetches current events, based on the command. 283 | async fn fetch_events(&self, command: &C) -> Result, Error> { 284 | self.repository.fetch_events(command).await 285 | } 286 | /// Saves events. 287 | async fn save(&self, events: &[E]) -> Result, Error> { 288 | self.repository.save(events).await 289 | } 290 | /// Version provider. It is used to provide the version/sequence of the event. Optimistic locking is useing this version to check if the event is already saved. 291 | async fn version_provider(&self, event: &E) -> Result, Error> { 292 | self.repository.version_provider(event).await 293 | } 294 | } 295 | 296 | impl<'a, C, S, E, Repository, Version, Error> 297 | EventSourcedOrchestratingAggregate<'a, C, S, E, Repository, Version, Error> 298 | where 299 | Repository: EventRepository + Sync, 300 | C: Sync, 301 | S: Sync, 302 | E: Sync + Clone, 303 | Version: Sync, 304 | Error: Sync, 305 | { 306 | /// Creates a new instance of [EventSourcedAggregate]. 307 | pub fn new( 308 | repository: Repository, 309 | decider: Decider<'a, C, S, E, Error>, 310 | saga: Saga<'a, E, C>, 311 | ) -> Self { 312 | EventSourcedOrchestratingAggregate { 313 | repository, 314 | decider, 315 | saga, 316 | _marker: PhantomData, 317 | } 318 | } 319 | /// Handles the command by fetching the events from the repository, computing new events based on the current events and the command, and saving the new events to the repository. 320 | pub async fn handle(&self, command: &C) -> Result, Error> 321 | where 322 | E: Identifier, 323 | C: Identifier, 324 | { 325 | let events: Vec<(E, Version)> = self.fetch_events(command).await?; 326 | let mut current_events: Vec = vec![]; 327 | for (event, _) in events { 328 | current_events.push(event); 329 | } 330 | let new_events = self 331 | .compute_new_events_dynamically(¤t_events, command) 332 | .await?; 333 | let saved_events = self.save(&new_events).await?; 334 | Ok(saved_events) 335 | } 336 | /// Computes new events based on the current events and the command. 337 | /// It is using a [Decider] and [Saga] to compute new events based on the current events and the command. 338 | /// If the `decider` is combined out of many deciders via `combine` function, a `saga` could be used to react on new events and send new commands to the `decider` recursively, in single transaction. 339 | /// It is using a [EventRepository] to fetch the current events for the command that is computed by the `saga`. 340 | async fn compute_new_events_dynamically( 341 | &self, 342 | current_events: &[E], 343 | command: &C, 344 | ) -> Result, Error> 345 | where 346 | E: Identifier, 347 | C: Identifier, 348 | { 349 | let current_state: S = current_events 350 | .iter() 351 | .fold((self.decider.initial_state)(), |state, event| { 352 | (self.decider.evolve)(&state, event) 353 | }); 354 | 355 | let initial_events = (self.decider.decide)(command, ¤t_state)?; 356 | 357 | let commands: Vec = initial_events 358 | .iter() 359 | .flat_map(|event: &E| self.saga.compute_new_actions(event)) 360 | .collect(); 361 | 362 | // Collect all events including recursively computed new events. 363 | let mut all_events = initial_events.clone(); 364 | 365 | for command in commands.iter() { 366 | let previous_events = [ 367 | self.repository 368 | .fetch_events(command) 369 | .await? 370 | .iter() 371 | .map(|(e, _)| e.clone()) 372 | .collect::>(), 373 | initial_events 374 | .clone() 375 | .into_iter() 376 | .filter(|e| e.identifier() == command.identifier()) 377 | .collect::>(), 378 | ] 379 | .concat(); 380 | 381 | // Recursively compute new events and extend the accumulated events list. 382 | // By wrapping the recursive call in a Box, we ensure that the future type is not self-referential. 383 | let new_events = 384 | Box::pin(self.compute_new_events_dynamically(&previous_events, command)).await?; 385 | all_events.extend(new_events); 386 | } 387 | 388 | Ok(all_events) 389 | } 390 | } 391 | 392 | /// Orchestrating State Stored Aggregate. 393 | /// 394 | /// It is using a [Decider] and [Saga] to compute new state based on the current state and the command. 395 | /// If the `decider` is combined out of many deciders via `combine` function, a `saga` could be used to react on new events and send new commands to the `decider` recursively, in single transaction. 396 | /// It is using a [StateRepository] to fetch the current state and to save the new state. 397 | /// 398 | /// Generic parameters: 399 | /// 400 | /// - `C` - Command 401 | /// - `S` - State 402 | /// - `E` - Event 403 | /// - `Repository` - State repository 404 | /// - `Version` - Version 405 | /// - `Error` - Error 406 | pub struct StateStoredOrchestratingAggregate<'a, C, S, E, Repository, Version, Error> 407 | where 408 | Repository: StateRepository, 409 | { 410 | repository: Repository, 411 | decider: Decider<'a, C, S, E, Error>, 412 | saga: Saga<'a, E, C>, 413 | _marker: PhantomData<(C, S, E, Version, Error)>, 414 | } 415 | 416 | impl StateComputation 417 | for StateStoredOrchestratingAggregate<'_, C, S, E, Repository, Version, Error> 418 | where 419 | Repository: StateRepository, 420 | S: Clone, 421 | { 422 | /// Computes new state based on the current state and the command. 423 | fn compute_new_state(&self, current_state: Option, command: &C) -> Result { 424 | let effective_current_state = 425 | current_state.unwrap_or_else(|| (self.decider.initial_state)()); 426 | let events = (self.decider.decide)(command, &effective_current_state)?; 427 | let mut new_state = events.iter().fold(effective_current_state, |state, event| { 428 | (self.decider.evolve)(&state, event) 429 | }); 430 | let commands = events 431 | .iter() 432 | .flat_map(|event: &E| self.saga.compute_new_actions(event)) 433 | .collect::>(); 434 | for action in commands { 435 | new_state = self.compute_new_state(Some(new_state.clone()), &action)?; 436 | } 437 | Ok(new_state) 438 | } 439 | } 440 | 441 | impl StateRepository 442 | for StateStoredOrchestratingAggregate<'_, C, S, E, Repository, Version, Error> 443 | where 444 | Repository: StateRepository + Sync, 445 | C: Sync, 446 | S: Sync, 447 | E: Sync, 448 | Version: Sync, 449 | Error: Sync, 450 | { 451 | /// Fetches current state, based on the command. 452 | async fn fetch_state(&self, command: &C) -> Result, Error> { 453 | self.repository.fetch_state(command).await 454 | } 455 | /// Saves state. 456 | async fn save(&self, state: &S, version: &Option) -> Result<(S, Version), Error> { 457 | self.repository.save(state, version).await 458 | } 459 | } 460 | 461 | impl<'a, C, S, E, Repository, Version, Error> 462 | StateStoredOrchestratingAggregate<'a, C, S, E, Repository, Version, Error> 463 | where 464 | Repository: StateRepository + Sync, 465 | C: Sync, 466 | S: Sync + Clone, 467 | E: Sync, 468 | Version: Sync, 469 | Error: Sync, 470 | { 471 | /// Creates a new instance of [StateStoredAggregate]. 472 | pub fn new( 473 | repository: Repository, 474 | decider: Decider<'a, C, S, E, Error>, 475 | saga: Saga<'a, E, C>, 476 | ) -> Self { 477 | StateStoredOrchestratingAggregate { 478 | repository, 479 | decider, 480 | saga, 481 | _marker: PhantomData, 482 | } 483 | } 484 | /// Handles the command by fetching the state from the repository, computing new state based on the current state and the command, and saving the new state to the repository. 485 | pub async fn handle(&self, command: &C) -> Result<(S, Version), Error> { 486 | let state_version = self.fetch_state(command).await?; 487 | match state_version { 488 | None => { 489 | let new_state = self.compute_new_state(None, command)?; 490 | let saved_state = self.save(&new_state, &None).await?; 491 | Ok(saved_state) 492 | } 493 | Some((state, version)) => { 494 | let new_state = self.compute_new_state(Some(state), command)?; 495 | let saved_state = self.save(&new_state, &Some(version)).await?; 496 | Ok(saved_state) 497 | } 498 | } 499 | } 500 | } 501 | -------------------------------------------------------------------------------- /src/decider.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use crate::{ 4 | DecideFunction, Decider3, Decider4, Decider5, Decider6, EvolveFunction, InitialStateFunction, 5 | Sum, Sum3, Sum4, Sum5, Sum6, 6 | }; 7 | 8 | /// [Decider] represents the main decision-making algorithm. 9 | /// It has three generic parameters `C`/`Command`, `S`/`State`, `E`/`Event` , representing the type of the values that Decider may contain or use. 10 | /// `'a` is used as a lifetime parameter, indicating that all references contained within the struct (e.g., references within the function closures) must have a lifetime that is at least as long as 'a. 11 | /// 12 | /// ## Example 13 | /// ``` 14 | /// use fmodel_rust::decider::{Decider, EventComputation, StateComputation}; 15 | /// 16 | /// fn decider<'a>() -> Decider<'a, OrderCommand, OrderState, OrderEvent> { 17 | /// Decider { 18 | /// // Exhaustive pattern matching is used to handle the commands (modeled as Enum - SUM/OR type). 19 | /// decide: Box::new(|command, state| { 20 | /// match command { 21 | /// OrderCommand::Create(create_cmd) => { 22 | /// Ok(vec![OrderEvent::Created(OrderCreatedEvent { 23 | /// order_id: create_cmd.order_id, 24 | /// customer_name: create_cmd.customer_name.to_owned(), 25 | /// items: create_cmd.items.to_owned(), 26 | /// })]) 27 | /// } 28 | /// OrderCommand::Update(update_cmd) => { 29 | /// if state.order_id == update_cmd.order_id { 30 | /// Ok(vec![OrderEvent::Updated(OrderUpdatedEvent { 31 | /// order_id: update_cmd.order_id, 32 | /// updated_items: update_cmd.new_items.to_owned(), 33 | /// })]) 34 | /// } else { 35 | /// Ok(vec![]) 36 | /// } 37 | /// } 38 | /// OrderCommand::Cancel(cancel_cmd) => { 39 | /// if state.order_id == cancel_cmd.order_id { 40 | /// Ok(vec![OrderEvent::Cancelled(OrderCancelledEvent { 41 | /// order_id: cancel_cmd.order_id, 42 | /// })]) 43 | /// } else { 44 | /// Ok(vec![]) 45 | /// } 46 | /// } 47 | /// } 48 | /// }), 49 | /// // Exhaustive pattern matching is used to handle the events (modeled as Enum - SUM/OR type). 50 | /// evolve: Box::new(|state, event| { 51 | /// let mut new_state = state.clone(); 52 | /// match event { 53 | /// OrderEvent::Created(created_event) => { 54 | /// new_state.order_id = created_event.order_id; 55 | /// new_state.customer_name = created_event.customer_name.to_owned(); 56 | /// new_state.items = created_event.items.to_owned(); 57 | /// } 58 | /// OrderEvent::Updated(updated_event) => { 59 | /// new_state.items = updated_event.updated_items.to_owned(); 60 | /// } 61 | /// OrderEvent::Cancelled(_) => { 62 | /// new_state.is_cancelled = true; 63 | /// } 64 | /// } 65 | /// new_state 66 | /// }), 67 | /// initial_state: Box::new(|| OrderState { 68 | /// order_id: 0, 69 | /// customer_name: "".to_string(), 70 | /// items: Vec::new(), 71 | /// is_cancelled: false, 72 | /// }), 73 | /// } 74 | /// } 75 | /// 76 | /// // Modeling the commands, events, and state. Enum is modeling the SUM/OR type, and struct is modeling the PRODUCT/AND type. 77 | /// #[derive(Debug)] 78 | /// pub enum OrderCommand { 79 | /// Create(CreateOrderCommand), 80 | /// Update(UpdateOrderCommand), 81 | /// Cancel(CancelOrderCommand), 82 | /// } 83 | /// 84 | /// #[derive(Debug)] 85 | /// pub struct CreateOrderCommand { 86 | /// pub order_id: u32, 87 | /// pub customer_name: String, 88 | /// pub items: Vec, 89 | /// } 90 | /// 91 | /// #[derive(Debug)] 92 | /// pub struct UpdateOrderCommand { 93 | /// pub order_id: u32, 94 | /// pub new_items: Vec, 95 | /// } 96 | /// 97 | /// #[derive(Debug)] 98 | /// pub struct CancelOrderCommand { 99 | /// pub order_id: u32, 100 | /// } 101 | /// 102 | /// #[derive(Debug, PartialEq)] 103 | /// pub enum OrderEvent { 104 | /// Created(OrderCreatedEvent), 105 | /// Updated(OrderUpdatedEvent), 106 | /// Cancelled(OrderCancelledEvent), 107 | /// } 108 | /// 109 | /// #[derive(Debug, PartialEq)] 110 | /// pub struct OrderCreatedEvent { 111 | /// pub order_id: u32, 112 | /// pub customer_name: String, 113 | /// pub items: Vec, 114 | /// } 115 | /// 116 | /// #[derive(Debug, PartialEq)] 117 | /// pub struct OrderUpdatedEvent { 118 | /// pub order_id: u32, 119 | /// pub updated_items: Vec, 120 | /// } 121 | /// 122 | /// #[derive(Debug, PartialEq)] 123 | /// pub struct OrderCancelledEvent { 124 | /// pub order_id: u32, 125 | /// } 126 | /// 127 | /// #[derive(Debug, Clone, PartialEq)] 128 | /// struct OrderState { 129 | /// order_id: u32, 130 | /// customer_name: String, 131 | /// items: Vec, 132 | /// is_cancelled: bool, 133 | /// } 134 | /// 135 | /// let decider: Decider = decider(); 136 | /// let create_order_command = OrderCommand::Create(CreateOrderCommand { 137 | /// order_id: 1, 138 | /// customer_name: "John Doe".to_string(), 139 | /// items: vec!["Item 1".to_string(), "Item 2".to_string()], 140 | /// }); 141 | /// let new_events = decider.compute_new_events(&[], &create_order_command); 142 | /// assert_eq!(new_events, Ok(vec![OrderEvent::Created(OrderCreatedEvent { 143 | /// order_id: 1, 144 | /// customer_name: "John Doe".to_string(), 145 | /// items: vec!["Item 1".to_string(), "Item 2".to_string()], 146 | /// })])); 147 | /// let new_state = decider.compute_new_state(None, &create_order_command); 148 | /// assert_eq!(new_state, Ok(OrderState { 149 | /// order_id: 1, 150 | /// customer_name: "John Doe".to_string(), 151 | /// items: vec!["Item 1".to_string(), "Item 2".to_string()], 152 | /// is_cancelled: false, 153 | /// })); 154 | /// 155 | /// ``` 156 | pub struct Decider<'a, C: 'a, S: 'a, E: 'a, Error: 'a = ()> { 157 | /// The `decide` function is used to decide which events to produce based on the command and the current state. 158 | pub decide: DecideFunction<'a, C, S, E, Error>, 159 | /// The `evolve` function is used to evolve the state based on the current state and the event. 160 | pub evolve: EvolveFunction<'a, S, E>, 161 | /// The `initial_state` function is used to produce the initial state of the decider. 162 | pub initial_state: InitialStateFunction<'a, S>, 163 | } 164 | 165 | impl<'a, C, S, E, Error> Decider<'a, C, S, E, Error> { 166 | /// Maps the Decider over the S/State type parameter. 167 | /// Creates a new instance of [Decider]``. 168 | pub fn map_state(self, f1: F1, f2: F2) -> Decider<'a, C, S2, E, Error> 169 | where 170 | F1: Fn(&S2) -> S + Send + Sync + 'a, 171 | F2: Fn(&S) -> S2 + Send + Sync + 'a, 172 | { 173 | let f1 = Arc::new(f1); 174 | let f2 = Arc::new(f2); 175 | 176 | let new_decide = { 177 | let f1 = Arc::clone(&f1); 178 | Box::new(move |c: &C, s2: &S2| { 179 | let s = f1(s2); 180 | (self.decide)(c, &s) 181 | }) 182 | }; 183 | 184 | let new_evolve = { 185 | let f2 = Arc::clone(&f2); 186 | Box::new(move |s2: &S2, e: &E| { 187 | let s = f1(s2); 188 | f2(&(self.evolve)(&s, e)) 189 | }) 190 | }; 191 | 192 | let new_initial_state = { Box::new(move || f2(&(self.initial_state)())) }; 193 | 194 | Decider { 195 | decide: new_decide, 196 | evolve: new_evolve, 197 | initial_state: new_initial_state, 198 | } 199 | } 200 | 201 | /// Maps the Decider over the E/Event type parameter. 202 | /// Creates a new instance of [Decider]``. 203 | pub fn map_event(self, f1: F1, f2: F2) -> Decider<'a, C, S, E2, Error> 204 | where 205 | F1: Fn(&E2) -> E + Send + Sync + 'a, 206 | F2: Fn(&E) -> E2 + Send + Sync + 'a, 207 | { 208 | let new_decide = Box::new(move |c: &C, s: &S| { 209 | (self.decide)(c, s).map(|result| result.into_iter().map(|e: E| f2(&e)).collect()) 210 | }); 211 | 212 | let new_evolve = Box::new(move |s: &S, e2: &E2| { 213 | let e = f1(e2); 214 | (self.evolve)(s, &e) 215 | }); 216 | 217 | let new_initial_state = Box::new(move || (self.initial_state)()); 218 | 219 | Decider { 220 | decide: new_decide, 221 | evolve: new_evolve, 222 | initial_state: new_initial_state, 223 | } 224 | } 225 | 226 | /// Maps the Decider over the C/Command type parameter. 227 | /// Creates a new instance of [Decider]``. 228 | pub fn map_command(self, f: F) -> Decider<'a, C2, S, E, Error> 229 | where 230 | F: Fn(&C2) -> C + Send + Sync + 'a, 231 | { 232 | let new_decide = Box::new(move |c2: &C2, s: &S| { 233 | let c = f(c2); 234 | (self.decide)(&c, s) 235 | }); 236 | 237 | let new_evolve = Box::new(move |s: &S, e: &E| (self.evolve)(s, e)); 238 | 239 | let new_initial_state = Box::new(move || (self.initial_state)()); 240 | 241 | Decider { 242 | decide: new_decide, 243 | evolve: new_evolve, 244 | initial_state: new_initial_state, 245 | } 246 | } 247 | 248 | /// Maps the Decider over the Error type parameter. 249 | /// Creates a new instance of [Decider]``. 250 | pub fn map_error(self, f: F) -> Decider<'a, C, S, E, Error2> 251 | where 252 | F: Fn(&Error) -> Error2 + Send + Sync + 'a, 253 | { 254 | let new_decide = Box::new(move |c: &C, s: &S| (self.decide)(c, s).map_err(|e| f(&e))); 255 | 256 | let new_evolve = Box::new(move |s: &S, e: &E| (self.evolve)(s, e)); 257 | 258 | let new_initial_state = Box::new(move || (self.initial_state)()); 259 | 260 | Decider { 261 | decide: new_decide, 262 | evolve: new_evolve, 263 | initial_state: new_initial_state, 264 | } 265 | } 266 | 267 | /// Combines two deciders into one bigger decider 268 | /// Creates a new instance of a Decider by combining two deciders of type `C`, `S`, `E` and `C2`, `S2`, `E2` into a new decider of type `Sum`, `(S, S2)`, `Sum` 269 | #[allow(clippy::type_complexity)] 270 | pub fn combine( 271 | self, 272 | decider2: Decider<'a, C2, S2, E2, Error>, 273 | ) -> Decider<'a, Sum, (S, S2), Sum, Error> 274 | where 275 | S: Clone, 276 | S2: Clone, 277 | { 278 | let new_decide = Box::new(move |c: &Sum, s: &(S, S2)| match c { 279 | Sum::First(c) => { 280 | let s1 = &s.0; 281 | let events = (self.decide)(c, s1); 282 | events.map(|result| { 283 | result 284 | .into_iter() 285 | .map(|e: E| Sum::First(e)) 286 | .collect::>>() 287 | }) 288 | } 289 | Sum::Second(c) => { 290 | let s2 = &s.1; 291 | let events = (decider2.decide)(c, s2); 292 | events.map(|result| { 293 | result 294 | .into_iter() 295 | .map(|e: E2| Sum::Second(e)) 296 | .collect::>>() 297 | }) 298 | } 299 | }); 300 | 301 | let new_evolve = Box::new(move |s: &(S, S2), e: &Sum| match e { 302 | Sum::First(e) => { 303 | let s1 = &s.0; 304 | let new_state = (self.evolve)(s1, e); 305 | (new_state, s.1.to_owned()) 306 | } 307 | Sum::Second(e) => { 308 | let s2 = &s.1; 309 | let new_state = (decider2.evolve)(s2, e); 310 | (s.0.to_owned(), new_state) 311 | } 312 | }); 313 | 314 | let new_initial_state = Box::new(move || { 315 | let s1 = (self.initial_state)(); 316 | let s2 = (decider2.initial_state)(); 317 | (s1, s2) 318 | }); 319 | 320 | Decider { 321 | decide: new_decide, 322 | evolve: new_evolve, 323 | initial_state: new_initial_state, 324 | } 325 | } 326 | 327 | /// Combines three deciders into one bigger decider 328 | pub fn combine3( 329 | self, 330 | decider2: Decider<'a, C2, S2, E2, Error>, 331 | decider3: Decider<'a, C3, S3, E3, Error>, 332 | ) -> Decider3<'a, C, C2, C3, S, S2, S3, E, E2, E3, Error> 333 | where 334 | S: Clone, 335 | S2: Clone, 336 | S3: Clone, 337 | E: Clone, 338 | E2: Clone, 339 | E3: Clone, 340 | C: Clone, 341 | C2: Clone, 342 | C3: Clone, 343 | { 344 | // First combine self with decider2 345 | let combined = self.combine(decider2); 346 | 347 | // Then combine with decider3 and map the types 348 | combined 349 | .combine(decider3) 350 | .map_state( 351 | |s: &(S, S2, S3)| ((s.0.clone(), s.1.clone()), s.2.clone()), 352 | |s: &((S, S2), S3)| (s.0 .0.clone(), s.0 .1.clone(), s.1.clone()), 353 | ) 354 | .map_event( 355 | |e: &Sum3| match e { 356 | Sum3::First(ref e) => Sum::First(Sum::First(e.clone())), 357 | Sum3::Second(ref e) => Sum::First(Sum::Second(e.clone())), 358 | Sum3::Third(ref e) => Sum::Second(e.clone()), 359 | }, 360 | |e: &Sum, E3>| match e { 361 | Sum::First(Sum::First(e)) => Sum3::First(e.clone()), 362 | Sum::First(Sum::Second(e)) => Sum3::Second(e.clone()), 363 | Sum::Second(e) => Sum3::Third(e.clone()), 364 | }, 365 | ) 366 | .map_command(|c: &Sum3| match c { 367 | Sum3::First(c) => Sum::First(Sum::First(c.clone())), 368 | Sum3::Second(c) => Sum::First(Sum::Second(c.clone())), 369 | Sum3::Third(c) => Sum::Second(c.clone()), 370 | }) 371 | } 372 | 373 | #[allow(clippy::type_complexity)] 374 | /// Combines four deciders into one bigger decider 375 | pub fn combine4( 376 | self, 377 | decider2: Decider<'a, C2, S2, E2, Error>, 378 | decider3: Decider<'a, C3, S3, E3, Error>, 379 | decider4: Decider<'a, C4, S4, E4, Error>, 380 | ) -> Decider4<'a, C, C2, C3, C4, S, S2, S3, S4, E, E2, E3, E4, Error> 381 | where 382 | S: Clone, 383 | S2: Clone, 384 | S3: Clone, 385 | S4: Clone, 386 | E: Clone, 387 | E2: Clone, 388 | E3: Clone, 389 | E4: Clone, 390 | C: Clone, 391 | C2: Clone, 392 | C3: Clone, 393 | C4: Clone, 394 | { 395 | let combined = self 396 | .combine(decider2) 397 | .combine(decider3) 398 | .combine(decider4) 399 | .map_state( 400 | |s: &(S, S2, S3, S4)| (((s.0.clone(), s.1.clone()), s.2.clone()), s.3.clone()), 401 | |s: &(((S, S2), S3), S4)| { 402 | ( 403 | s.0 .0 .0.clone(), 404 | s.0 .0 .1.clone(), 405 | s.0 .1.clone(), 406 | s.1.clone(), 407 | ) 408 | }, 409 | ) 410 | .map_event( 411 | |e: &Sum4| match e { 412 | Sum4::First(e) => Sum::First(Sum::First(Sum::First(e.clone()))), 413 | Sum4::Second(e) => Sum::First(Sum::First(Sum::Second(e.clone()))), 414 | Sum4::Third(e) => Sum::First(Sum::Second(e.clone())), 415 | Sum4::Fourth(e) => Sum::Second(e.clone()), 416 | }, 417 | |e: &Sum, E3>, E4>| match e { 418 | Sum::First(Sum::First(Sum::First(e))) => Sum4::First(e.clone()), 419 | Sum::First(Sum::First(Sum::Second(e))) => Sum4::Second(e.clone()), 420 | Sum::First(Sum::Second(e)) => Sum4::Third(e.clone()), 421 | Sum::Second(e) => Sum4::Fourth(e.clone()), 422 | }, 423 | ) 424 | .map_command(|c: &Sum4| match c { 425 | Sum4::First(c) => Sum::First(Sum::First(Sum::First(c.clone()))), 426 | Sum4::Second(c) => Sum::First(Sum::First(Sum::Second(c.clone()))), 427 | Sum4::Third(c) => Sum::First(Sum::Second(c.clone())), 428 | Sum4::Fourth(c) => Sum::Second(c.clone()), 429 | }); 430 | combined 431 | } 432 | 433 | #[allow(clippy::type_complexity)] 434 | /// Combines five deciders into one bigger decider 435 | pub fn combine5( 436 | self, 437 | decider2: Decider<'a, C2, S2, E2, Error>, 438 | decider3: Decider<'a, C3, S3, E3, Error>, 439 | decider4: Decider<'a, C4, S4, E4, Error>, 440 | decider5: Decider<'a, C5, S5, E5, Error>, 441 | ) -> Decider5<'a, C, C2, C3, C4, C5, S, S2, S3, S4, S5, E, E2, E3, E4, E5, Error> 442 | where 443 | S: Clone, 444 | S2: Clone, 445 | S3: Clone, 446 | S4: Clone, 447 | S5: Clone, 448 | E: Clone, 449 | E2: Clone, 450 | E3: Clone, 451 | E4: Clone, 452 | E5: Clone, 453 | C: Clone, 454 | C2: Clone, 455 | C3: Clone, 456 | C4: Clone, 457 | C5: Clone, 458 | { 459 | let combined = self 460 | .combine(decider2) 461 | .combine(decider3) 462 | .combine(decider4) 463 | .combine(decider5) 464 | .map_state( 465 | |s: &(S, S2, S3, S4, S5)| { 466 | ( 467 | (((s.0.clone(), s.1.clone()), s.2.clone()), s.3.clone()), 468 | s.4.clone(), 469 | ) 470 | }, 471 | |s: &((((S, S2), S3), S4), S5)| { 472 | ( 473 | s.0 .0 .0 .0.clone(), 474 | s.0 .0 .0 .1.clone(), 475 | s.0 .0 .1.clone(), 476 | s.0 .1.clone(), 477 | s.1.clone(), 478 | ) 479 | }, 480 | ) 481 | .map_event( 482 | |e: &Sum5| match e { 483 | Sum5::First(e) => Sum::First(Sum::First(Sum::First(Sum::First(e.clone())))), 484 | Sum5::Second(e) => Sum::First(Sum::First(Sum::First(Sum::Second(e.clone())))), 485 | Sum5::Third(e) => Sum::First(Sum::First(Sum::Second(e.clone()))), 486 | Sum5::Fourth(e) => Sum::First(Sum::Second(e.clone())), 487 | Sum5::Fifth(e) => Sum::Second(e.clone()), 488 | }, 489 | |e: &Sum, E3>, E4>, E5>| match e { 490 | Sum::First(Sum::First(Sum::First(Sum::First(e)))) => Sum5::First(e.clone()), 491 | Sum::First(Sum::First(Sum::First(Sum::Second(e)))) => Sum5::Second(e.clone()), 492 | Sum::First(Sum::First(Sum::Second(e))) => Sum5::Third(e.clone()), 493 | Sum::First(Sum::Second(e)) => Sum5::Fourth(e.clone()), 494 | Sum::Second(e) => Sum5::Fifth(e.clone()), 495 | }, 496 | ) 497 | .map_command(|c: &Sum5| match c { 498 | Sum5::First(c) => Sum::First(Sum::First(Sum::First(Sum::First(c.clone())))), 499 | Sum5::Second(c) => Sum::First(Sum::First(Sum::First(Sum::Second(c.clone())))), 500 | Sum5::Third(c) => Sum::First(Sum::First(Sum::Second(c.clone()))), 501 | Sum5::Fourth(c) => Sum::First(Sum::Second(c.clone())), 502 | Sum5::Fifth(c) => Sum::Second(c.clone()), 503 | }); 504 | combined 505 | } 506 | 507 | #[allow(clippy::type_complexity)] 508 | /// Combines six deciders into one bigger decider 509 | pub fn combine6( 510 | self, 511 | decider2: Decider<'a, C2, S2, E2, Error>, 512 | decider3: Decider<'a, C3, S3, E3, Error>, 513 | decider4: Decider<'a, C4, S4, E4, Error>, 514 | decider5: Decider<'a, C5, S5, E5, Error>, 515 | decider6: Decider<'a, C6, S6, E6, Error>, 516 | ) -> Decider6<'a, C, C2, C3, C4, C5, C6, S, S2, S3, S4, S5, S6, E, E2, E3, E4, E5, E6, Error> 517 | where 518 | S: Clone, 519 | S2: Clone, 520 | S3: Clone, 521 | S4: Clone, 522 | S5: Clone, 523 | S6: Clone, 524 | E: Clone, 525 | E2: Clone, 526 | E3: Clone, 527 | E4: Clone, 528 | E5: Clone, 529 | E6: Clone, 530 | C: Clone, 531 | C2: Clone, 532 | C3: Clone, 533 | C4: Clone, 534 | C5: Clone, 535 | C6: Clone, 536 | { 537 | let combined = self 538 | .combine(decider2) 539 | .combine(decider3) 540 | .combine(decider4) 541 | .combine(decider5) 542 | .combine(decider6) 543 | .map_state( 544 | |s: &(S, S2, S3, S4, S5, S6)| { 545 | ( 546 | ( 547 | (((s.0.clone(), s.1.clone()), s.2.clone()), s.3.clone()), 548 | s.4.clone(), 549 | ), 550 | s.5.clone(), 551 | ) 552 | }, 553 | |s: &(((((S, S2), S3), S4), S5), S6)| { 554 | ( 555 | s.0 .0 .0 .0 .0.clone(), 556 | s.0 .0 .0 .0 .1.clone(), 557 | s.0 .0 .0 .1.clone(), 558 | s.0 .0 .1.clone(), 559 | s.0 .1.clone(), 560 | s.1.clone(), 561 | ) 562 | }, 563 | ) 564 | .map_event( 565 | |e: &Sum6| match e { 566 | Sum6::First(e) => { 567 | Sum::First(Sum::First(Sum::First(Sum::First(Sum::First(e.clone()))))) 568 | } 569 | Sum6::Second(e) => { 570 | Sum::First(Sum::First(Sum::First(Sum::First(Sum::Second(e.clone()))))) 571 | } 572 | Sum6::Third(e) => Sum::First(Sum::First(Sum::First(Sum::Second(e.clone())))), 573 | Sum6::Fourth(e) => Sum::First(Sum::First(Sum::Second(e.clone()))), 574 | Sum6::Fifth(e) => Sum::First(Sum::Second(e.clone())), 575 | Sum6::Sixth(e) => Sum::Second(e.clone()), 576 | }, 577 | |e: &Sum, E3>, E4>, E5>, E6>| match e { 578 | Sum::First(Sum::First(Sum::First(Sum::First(Sum::First(e))))) => { 579 | Sum6::First(e.clone()) 580 | } 581 | Sum::First(Sum::First(Sum::First(Sum::First(Sum::Second(e))))) => { 582 | Sum6::Second(e.clone()) 583 | } 584 | Sum::First(Sum::First(Sum::First(Sum::Second(e)))) => Sum6::Third(e.clone()), 585 | Sum::First(Sum::First(Sum::Second(e))) => Sum6::Fourth(e.clone()), 586 | Sum::First(Sum::Second(e)) => Sum6::Fifth(e.clone()), 587 | Sum::Second(e) => Sum6::Sixth(e.clone()), 588 | }, 589 | ) 590 | .map_command(|c: &Sum6| match c { 591 | Sum6::First(c) => { 592 | Sum::First(Sum::First(Sum::First(Sum::First(Sum::First(c.clone()))))) 593 | } 594 | Sum6::Second(c) => { 595 | Sum::First(Sum::First(Sum::First(Sum::First(Sum::Second(c.clone()))))) 596 | } 597 | Sum6::Third(c) => Sum::First(Sum::First(Sum::First(Sum::Second(c.clone())))), 598 | Sum6::Fourth(c) => Sum::First(Sum::First(Sum::Second(c.clone()))), 599 | Sum6::Fifth(c) => Sum::First(Sum::Second(c.clone())), 600 | Sum6::Sixth(c) => Sum::Second(c.clone()), 601 | }); 602 | combined 603 | } 604 | } 605 | 606 | /// Formalizes the `Event Computation` algorithm / event sourced system for the `decider` to handle commands based on the current events, and produce new events. 607 | pub trait EventComputation { 608 | /// Computes new events based on the current events and the command. 609 | fn compute_new_events(&self, current_events: &[E], command: &C) -> Result, Error>; 610 | } 611 | 612 | /// Formalizes the `State Computation` algorithm / state-stored system for the `decider` to handle commands based on the current state, and produce new state. 613 | pub trait StateComputation { 614 | /// Computes new state based on the current state and the command. 615 | fn compute_new_state(&self, current_state: Option, command: &C) -> Result; 616 | } 617 | 618 | impl EventComputation for Decider<'_, C, S, E, Error> { 619 | /// Computes new events based on the current events and the command. 620 | fn compute_new_events(&self, current_events: &[E], command: &C) -> Result, Error> { 621 | let current_state: S = current_events 622 | .iter() 623 | .fold((self.initial_state)(), |state, event| { 624 | (self.evolve)(&state, event) 625 | }); 626 | (self.decide)(command, ¤t_state) 627 | } 628 | } 629 | 630 | impl StateComputation for Decider<'_, C, S, E, Error> { 631 | /// Computes new state based on the current state and the command. 632 | fn compute_new_state(&self, current_state: Option, command: &C) -> Result { 633 | let effective_current_state = current_state.unwrap_or_else(|| (self.initial_state)()); 634 | let events = (self.decide)(command, &effective_current_state); 635 | events.map(|result| { 636 | result 637 | .into_iter() 638 | .fold(effective_current_state, |state, event| { 639 | (self.evolve)(&state, &event) 640 | }) 641 | }) 642 | } 643 | } 644 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(missing_docs)] 2 | //! # FModel Rust 3 | //! 4 | //! When you’re developing an information system to automate the activities of the business, you are modeling the business. 5 | //! The abstractions that you design, the behaviors that you implement, and the UI interactions that you build all reflect 6 | //! the business — together, they constitute the model of the domain. 7 | //! 8 | //! ![event-modeling](https://github.com/fraktalio/fmodel-ts/raw/main/.assets/event-modeling.png) 9 | //! 10 | //! ## `IOR` 11 | //! 12 | //! This crate can be used as a library, or as an inspiration, or both. It provides just enough tactical Domain-Driven 13 | //! Design patterns, optimised for Event Sourcing and CQRS. 14 | //! 15 | //! ![onion architecture image](https://github.com/fraktalio/fmodel/blob/d8643a7d0de30b79f0b904f7a40233419c463fc8/.assets/onion.png?raw=true) 16 | //! 17 | //!## Decider 18 | //! 19 | //! `Decider` is a datatype/struct that represents the main decision-making algorithm. It belongs to the Domain layer. It 20 | //! has three 21 | //! generic parameters `C`, `S`, `E` , representing the type of the values that `Decider` may contain or use. 22 | //! `Decider` can be specialized for any type `C` or `S` or `E` because these types do not affect its 23 | //! behavior. `Decider` behaves the same for `C`=`Int` or `C`=`YourCustomType`, for example. 24 | //! 25 | //! `Decider` is a pure domain component. 26 | //! 27 | //! - `C` - Command 28 | //! - `S` - State 29 | //! - `E` - Event 30 | //! 31 | //! ```rust 32 | //! pub type DecideFunction<'a, C, S, E> = Box Vec + 'a + Send + Sync>; 33 | //! pub type EvolveFunction<'a, S, E> = Box S + 'a + Send + Sync>; 34 | //! pub type InitialStateFunction<'a, S> = Box S + 'a + Send + Sync>; 35 | //! 36 | //! pub struct Decider<'a, C: 'a, S: 'a, E: 'a> { 37 | //! pub decide: DecideFunction<'a, C, S, E>, 38 | //! pub evolve: EvolveFunction<'a, S, E>, 39 | //! pub initial_state: InitialStateFunction<'a, S>, 40 | //! } 41 | //! ``` 42 | //! 43 | //! Additionally, `initialState` of the Decider is introduced to gain more control over the initial state of the Decider. 44 | //! 45 | //! ### Event-sourcing aggregate 46 | //! 47 | //! [aggregate::EventSourcedAggregate] is using/delegating a `Decider` to handle commands and produce new events. 48 | //! 49 | //! It belongs to the Application layer. 50 | //! 51 | //! In order to handle the command, aggregate needs to fetch the current state (represented as a list/vector of events) 52 | //! via `EventRepository.fetchEvents` async function, and then delegate the command to the decider which can produce new 53 | //! events as 54 | //! a result. Produced events are then stored via `EventRepository.save` async function. 55 | //! 56 | //! It is a formalization of the event sourced information system. 57 | //! 58 | //! ### State-stored aggregate 59 | //! 60 | //! [aggregate::StateStoredAggregate] is using/delegating a `Decider` to handle commands and produce new state. 61 | //! 62 | //! It belongs to the Application layer. 63 | //! 64 | //! In order to handle the command, aggregate needs to fetch the current state via `StateRepository.fetchState` async function first, 65 | //! and then 66 | //! delegate the command to the decider which can produce new state as a result. New state is then stored 67 | //! via `StateRepository.save` async function. 68 | //! 69 | //! ## View 70 | //! 71 | //! `View` is a datatype that represents the event handling algorithm, responsible for translating the events into 72 | //! denormalized state, which is more adequate for querying. It belongs to the Domain layer. It is usually used to create 73 | //! the view/query side of the CQRS pattern. Obviously, the command side of the CQRS is usually event-sourced aggregate. 74 | //! 75 | //! It has two generic parameters `S`, `E`, representing the type of the values that `View` may contain or use. 76 | //! `View` can be specialized for any type of `S`, `E` because these types do not affect its behavior. 77 | //! `View` behaves the same for `E`=`Int` or `E`=`YourCustomType`, for example. 78 | //! 79 | //! `View` is a pure domain component. 80 | //! 81 | //! - `S` - State 82 | //! - `E` - Event 83 | //! 84 | //! ```rust 85 | //! pub type EvolveFunction<'a, S, E> = Box S + 'a + Send + Sync>; 86 | //! pub type InitialStateFunction<'a, S> = Box S + 'a + Send + Sync>; 87 | //! 88 | //! pub struct View<'a, S: 'a, E: 'a> { 89 | //! pub evolve: EvolveFunction<'a, S, E>, 90 | //! pub initial_state: InitialStateFunction<'a, S>, 91 | //! } 92 | //! ``` 93 | //! 94 | //! ### Materialized View 95 | //! 96 | //! [materialized_view::MaterializedView] is using/delegating a `View` to handle events of type `E` and to maintain 97 | //! a state of denormalized 98 | //! projection(s) as a 99 | //! result. Essentially, it represents the query/view side of the CQRS pattern. 100 | //! 101 | //! It belongs to the Application layer. 102 | //! 103 | //! In order to handle the event, materialized view needs to fetch the current state via `ViewStateRepository.fetchState` 104 | //! suspending function first, and then delegate the event to the view, which can produce new state as a result. New state 105 | //! is then stored via `ViewStateRepository.save` suspending function. 106 | //! 107 | //! 108 | //! ## Saga 109 | //! 110 | //! `Saga` is a datatype that represents the central point of control, deciding what to execute next (`A`), based on the action result (`AR`). 111 | //! It has two generic parameters `AR`/Action Result, `A`/Action , representing the type of the values that Saga may contain or use. 112 | //! `'a` is used as a lifetime parameter, indicating that all references contained within the struct (e.g., references within the function closures) must have a lifetime that is at least as long as 'a. 113 | //! 114 | //! `Saga` is a pure domain component. 115 | //! 116 | //! - `AR` - Action Result/Event 117 | //! - `A` - Action/Command 118 | //! 119 | //! ```rust 120 | //! pub type ReactFunction<'a, AR, A> = Box Vec + 'a + Send + Sync>; 121 | //! pub struct Saga<'a, AR: 'a, A: 'a> { 122 | //! pub react: ReactFunction<'a, AR, A>, 123 | //! } 124 | //! ``` 125 | //! 126 | //! ### Saga Manager 127 | //! 128 | //! [saga_manager::SagaManager] is using/delegating a `Saga` to react to the action result and to publish the new actions. 129 | //! 130 | //! It belongs to the Application layer. 131 | //! 132 | //! It is using a [saga::Saga] to react to the action result and to publish the new actions. 133 | //! It is using an [saga_manager::ActionPublisher] to publish the new actions. 134 | //! 135 | //! ## Clear separation between data and behaviour 136 | //! 137 | //!```rust 138 | //! use fmodel_rust::decider::Decider; 139 | //! // ## Algebraic Data Types 140 | //! // 141 | //! // In Rust, we can use ADTs to model our application's domain entities and relationships in a functional way, clearly defining the set of possible values and states. 142 | //! // Rust has two main types of ADTs: `enum` and `struct`. 143 | //! // 144 | //! // - `enum` is used to define a type that can take on one of several possible variants - modeling a `sum/OR` type. 145 | //! // - `struct` is used to express a type that has named fields - modeling a `product/AND` type. 146 | //! // 147 | //! // ADTs will help with 148 | //! // 149 | //! // - representing the business domain in the code accurately 150 | //! // - enforcing correctness 151 | //! // - reducing the likelihood of bugs. 152 | //! 153 | //! 154 | //! // ### `C` / Command / Intent to change the state of the system 155 | //! 156 | //! // models Sum/Or type / multiple possible variants 157 | //! pub enum OrderCommand { 158 | //! Create(CreateOrderCommand), 159 | //! Update(UpdateOrderCommand), 160 | //! Cancel(CancelOrderCommand), 161 | //! } 162 | //! // models Product/And type / a concrete variant, consisting of named fields 163 | //! pub struct CreateOrderCommand { 164 | //! pub order_id: u32, 165 | //! pub customer_name: String, 166 | //! pub items: Vec, 167 | //! } 168 | //! // models Product/And type / a concrete variant, consisting of named fields 169 | //! pub struct UpdateOrderCommand { 170 | //! pub order_id: u32, 171 | //! pub new_items: Vec, 172 | //! } 173 | //! // models Product/And type / a concrete variant, consisting of named fields 174 | //! pub struct CancelOrderCommand { 175 | //! pub order_id: u32, 176 | //! } 177 | //! 178 | //! // ### `E` / Event / Fact 179 | //! 180 | //! // models Sum/Or type / multiple possible variants 181 | //! pub enum OrderEvent { 182 | //! Created(OrderCreatedEvent), 183 | //! Updated(OrderUpdatedEvent), 184 | //! Cancelled(OrderCancelledEvent), 185 | //! } 186 | //! // models Product/And type / a concrete variant, consisting of named fields 187 | //! pub struct OrderCreatedEvent { 188 | //! pub order_id: u32, 189 | //! pub customer_name: String, 190 | //! pub items: Vec, 191 | //! } 192 | //! // models Product/And type / a concrete variant, consisting of named fields 193 | //! pub struct OrderUpdatedEvent { 194 | //! pub order_id: u32, 195 | //! pub updated_items: Vec, 196 | //! } 197 | //! // models Product/And type / a concrete variant, consisting of named fields 198 | //! pub struct OrderCancelledEvent { 199 | //! pub order_id: u32, 200 | //! } 201 | //! 202 | //! // ### `S` / State / Current state of the system/aggregate/entity 203 | //! #[derive(Clone)] 204 | //! struct OrderState { 205 | //! order_id: u32, 206 | //! customer_name: String, 207 | //! items: Vec, 208 | //! is_cancelled: bool, 209 | //! } 210 | //! 211 | //! // ## Modeling the Behaviour of our domain 212 | //! // 213 | //! // - algebraic data types form the structure of our entities (commands, state, and events). 214 | //! // - functions/lambda offers the algebra of manipulating the entities in a compositional manner, effectively modeling the behavior. 215 | //! // 216 | //! // This leads to modularity in design and a clear separation of the entity’s structure and functions/behaviour of the entity. 217 | //! // 218 | //! // Fmodel library offers generic and abstract components to specialize in for your specific case/expected behavior 219 | //! 220 | //! fn decider<'a>() -> Decider<'a, OrderCommand, OrderState, OrderEvent> { 221 | //! Decider { 222 | //! // Your decision logic goes here. 223 | //! decide: Box::new(|command, state| match command { 224 | //! // Exhaustive pattern matching on the command 225 | //! OrderCommand::Create(create_cmd) => { 226 | //! Ok(vec![OrderEvent::Created(OrderCreatedEvent { 227 | //! order_id: create_cmd.order_id, 228 | //! customer_name: create_cmd.customer_name.to_owned(), 229 | //! items: create_cmd.items.to_owned(), 230 | //! })]) 231 | //! } 232 | //! OrderCommand::Update(update_cmd) => { 233 | //! // Your validation logic goes here 234 | //! if state.order_id == update_cmd.order_id { 235 | //! Ok(vec![OrderEvent::Updated(OrderUpdatedEvent { 236 | //! order_id: update_cmd.order_id, 237 | //! updated_items: update_cmd.new_items.to_owned(), 238 | //! })]) 239 | //! } else { 240 | //! // In case of validation failure, return empty list of events or error event 241 | //! Ok(vec![]) 242 | //! } 243 | //! } 244 | //! OrderCommand::Cancel(cancel_cmd) => { 245 | //! // Your validation logic goes here 246 | //! if state.order_id == cancel_cmd.order_id { 247 | //! Ok(vec![OrderEvent::Cancelled(OrderCancelledEvent { 248 | //! order_id: cancel_cmd.order_id, 249 | //! })]) 250 | //! } else { 251 | //! // In case of validation failure, return empty list of events or error event 252 | //! Ok(vec![]) 253 | //! } 254 | //! } 255 | //! }), 256 | //! // Evolve the state based on the event(s) 257 | //! evolve: Box::new(|state, event| { 258 | //! let mut new_state = state.clone(); 259 | //! // Exhaustive pattern matching on the event 260 | //! match event { 261 | //! OrderEvent::Created(created_event) => { 262 | //! new_state.order_id = created_event.order_id; 263 | //! new_state.customer_name = created_event.customer_name.to_owned(); 264 | //! new_state.items = created_event.items.to_owned(); 265 | //! } 266 | //! OrderEvent::Updated(updated_event) => { 267 | //! new_state.items = updated_event.updated_items.to_owned(); 268 | //! } 269 | //! OrderEvent::Cancelled(_) => { 270 | //! new_state.is_cancelled = true; 271 | //! } 272 | //! } 273 | //! new_state 274 | //! }), 275 | //! // Initial state 276 | //! initial_state: Box::new(|| OrderState { 277 | //! order_id: 0, 278 | //! customer_name: "".to_string(), 279 | //! items: Vec::new(), 280 | //! is_cancelled: false, 281 | //! }), 282 | //! } 283 | //! } 284 | //! ``` 285 | //! 286 | //! ## Examples 287 | //! 288 | //! - [Restaurant Demo - with Postgres](https://github.com/fraktalio/fmodel-rust-demo) 289 | //! - [Gift Card Demo - with Axon](https://!github.com/AxonIQ/axon-rust/tree/main/gift-card-rust) 290 | //! - [FModel Rust Tests](https://!github.com/fraktalio/fmodel-rust/tree/main/tests) 291 | //! 292 | //! ## GitHub 293 | //! 294 | //! - [FModel Rust](https://!github.com/fraktalio/fmodel-rust) 295 | //! 296 | //! ## FModel in other languages 297 | //! 298 | //! - [FModel Kotlin](https://!github.com/fraktalio/fmodel/) 299 | //! - [FModel TypeScript](https://!github.com/fraktalio/fmodel-ts/) 300 | //! - [FModel Java](https://!github.com/fraktalio/fmodel-java/) 301 | //! 302 | //! ## Credits 303 | //! 304 | //! Special credits to `Jérémie Chassaing` for sharing his [research](https://!www.youtube.com/watch?v=kgYGMVDHQHs) 305 | //! and `Adam Dymitruk` for hosting the meetup. 306 | //! 307 | //! --- 308 | //! Created with `love` by [Fraktalio](https://!fraktalio.com/) 309 | 310 | use decider::Decider; 311 | use saga::Saga; 312 | use serde::{Deserialize, Serialize}; 313 | use view::View; 314 | 315 | /// Aggregate module - belongs to the `Application` layer - composes pure logic and effects (fetching, storing) 316 | pub mod aggregate; 317 | /// Decider module - belongs to the `Domain` layer - pure decision making component - pure logic 318 | pub mod decider; 319 | /// Materialized View module - belongs to the `Application` layer - composes pure event handling algorithm and effects (fetching, storing) 320 | pub mod materialized_view; 321 | /// Saga module - belongs to the `Domain` layer - pure mapper of action results/events into new actions/commands 322 | pub mod saga; 323 | /// Saga Manager module - belongs to the `Application` layer - composes pure saga and effects (publishing) 324 | pub mod saga_manager; 325 | /// Given-When-Then Test specificatin domain specific language - unit testing 326 | pub mod specification; 327 | /// View module - belongs to the `Domain` layer - pure event handling algorithm 328 | pub mod view; 329 | 330 | /// The [DecideFunction] function is used to decide which events to produce based on the command and the current state. 331 | pub type DecideFunction<'a, C, S, E, Error> = 332 | Box Result, Error> + 'a + Send + Sync>; 333 | /// The [EvolveFunction] function is used to evolve the state based on the current state and the event. 334 | pub type EvolveFunction<'a, S, E> = Box S + 'a + Send + Sync>; 335 | /// The [InitialStateFunction] function is used to produce the initial state. 336 | pub type InitialStateFunction<'a, S> = Box S + 'a + Send + Sync>; 337 | /// The [ReactFunction] function is used to decide what actions/A to execute next based on the action result/AR. 338 | pub type ReactFunction<'a, AR, A> = Box Vec + 'a + Send + Sync>; 339 | 340 | /// Generic Combined/Sum Enum of two variants 341 | #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] 342 | pub enum Sum { 343 | /// First variant 344 | First(A), 345 | /// Second variant 346 | Second(B), 347 | } 348 | 349 | /// Generic Combined/Sum Enum of three variants 350 | #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] 351 | pub enum Sum3 { 352 | /// First variant 353 | First(A), 354 | /// Second variant 355 | Second(B), 356 | /// Third variant 357 | Third(C), 358 | } 359 | 360 | #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] 361 | /// Generic Combined/Sum Enum of four variants 362 | pub enum Sum4 { 363 | /// First variant 364 | First(A), 365 | /// Second variant 366 | Second(B), 367 | /// Third variant 368 | Third(C), 369 | /// Fourth variant 370 | Fourth(D), 371 | } 372 | 373 | #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] 374 | /// Generic Combined/Sum Enum of five variants 375 | pub enum Sum5 { 376 | /// First variant 377 | First(A), 378 | /// Second variant 379 | Second(B), 380 | /// Third variant 381 | Third(C), 382 | /// Fourth variant 383 | Fourth(D), 384 | /// Fifth variant 385 | Fifth(E), 386 | } 387 | 388 | #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] 389 | /// Generic Combined/Sum Enum of six variants 390 | pub enum Sum6 { 391 | /// First variant 392 | First(A), 393 | /// Second variant 394 | Second(B), 395 | /// Third variant 396 | Third(C), 397 | /// Fourth variant 398 | Fourth(D), 399 | /// Fifth variant 400 | Fifth(E), 401 | /// Sixth variant 402 | Sixth(F), 403 | } 404 | 405 | /// Convenient type alias that represents 3 combined Deciders 406 | type Decider3<'a, C1, C2, C3, S1, S2, S3, E1, E2, E3, Error> = 407 | Decider<'a, Sum3, (S1, S2, S3), Sum3, Error>; 408 | 409 | /// Convenient type alias that represents 4 combined Deciders 410 | type Decider4<'a, C1, C2, C3, C4, S1, S2, S3, S4, E1, E2, E3, E4, Error> = 411 | Decider<'a, Sum4, (S1, S2, S3, S4), Sum4, Error>; 412 | 413 | /// Convenient type alias that represents 5 combined Deciders 414 | type Decider5<'a, C1, C2, C3, C4, C5, S1, S2, S3, S4, S5, E1, E2, E3, E4, E5, Error> = 415 | Decider<'a, Sum5, (S1, S2, S3, S4, S5), Sum5, Error>; 416 | 417 | /// Convenient type alias that represents 6 combined Deciders 418 | type Decider6<'a, C1, C2, C3, C4, C5, C6, S1, S2, S3, S4, S5, S6, E1, E2, E3, E4, E5, E6, Error> = 419 | Decider< 420 | 'a, 421 | Sum6, 422 | (S1, S2, S3, S4, S5, S6), 423 | Sum6, 424 | Error, 425 | >; 426 | 427 | /// Convenient type alias that represents 3 merged Views 428 | type View3<'a, S1, S2, S3, E> = View<'a, (S1, S2, S3), E>; 429 | 430 | /// Convenient type alias that represents 4 merged Deciders 431 | type View4<'a, S1, S2, S3, S4, E> = View<'a, (S1, S2, S3, S4), E>; 432 | 433 | /// Convenient type alias that represents 5 merged Deciders 434 | type View5<'a, S1, S2, S3, S4, S5, E> = View<'a, (S1, S2, S3, S4, S5), E>; 435 | 436 | /// Convenient type alias that represents 6 merged Deciders 437 | type View6<'a, S1, S2, S3, S4, S5, S6, E> = View<'a, (S1, S2, S3, S4, S5, S6), E>; 438 | 439 | /// Convenient type alias that represents 3 merged Sagas 440 | type Saga3<'a, AR, A1, A2, A3> = Saga<'a, AR, Sum3>; 441 | 442 | /// Convenient type alias that represents 4 merged Sagas 443 | type Saga4<'a, AR, A1, A2, A3, A4> = Saga<'a, AR, Sum4>; 444 | 445 | /// Convenient type alias that represents 5 merged Sagas 446 | type Saga5<'a, AR, A1, A2, A3, A4, A5> = Saga<'a, AR, Sum5>; 447 | 448 | /// Convenient type alias that represents 6 merged Sagas 449 | type Saga6<'a, AR, A1, A2, A3, A4, A5, A6> = Saga<'a, AR, Sum6>; 450 | 451 | /// Identify the state/command/event. 452 | /// It is used to identify the concept to what the state/command/event belongs to. For example, the `order_id` or `restaurant_id`. 453 | pub trait Identifier { 454 | /// Returns the identifier of the state/command/event 455 | fn identifier(&self) -> String; 456 | } 457 | 458 | impl Identifier for Sum 459 | where 460 | A: Identifier, 461 | B: Identifier, 462 | { 463 | fn identifier(&self) -> String { 464 | match self { 465 | Sum::First(a) => a.identifier(), 466 | Sum::Second(b) => b.identifier(), 467 | } 468 | } 469 | } 470 | -------------------------------------------------------------------------------- /src/materialized_view.rs: -------------------------------------------------------------------------------- 1 | use std::future::Future; 2 | use std::marker::PhantomData; 3 | 4 | use crate::view::ViewStateComputation; 5 | 6 | /// View State Repository trait 7 | /// 8 | /// Generic parameters: 9 | /// 10 | /// - `E` - Event 11 | /// - `S` - State 12 | /// - `Error` - Error 13 | pub trait ViewStateRepository { 14 | /// Fetches current state, based on the event. 15 | /// Desugared `async fn fetch_state(&self, event: &E) -> Result, Error>;` to a normal `fn` that returns `impl Future`, and adds bound `Send`. 16 | /// You can freely move between the `async fn` and `-> impl Future` spelling in your traits and impls. This is true even when one form has a Send bound. 17 | fn fetch_state(&self, event: &E) -> impl Future, Error>> + Send; 18 | /// Saves the new state. 19 | /// Desugared `async fn save(&self, state: &S) -> Result;` to a normal `fn` that returns `impl Future`, and adds bound `Send`. 20 | /// You can freely move between the `async fn` and `-> impl Future` spelling in your traits and impls. This is true even when one form has a Send bound. 21 | fn save(&self, state: &S) -> impl Future> + Send; 22 | } 23 | 24 | /// Materialized View. 25 | /// 26 | /// It is using a `View` / [ViewStateComputation] to compute new state based on the current state and the event. 27 | /// It is using a [ViewStateRepository] to fetch the current state and to save the new state. 28 | /// 29 | /// Generic parameters: 30 | /// 31 | /// - `S` - State 32 | /// - `E` - Event 33 | /// - `Repository` - View State repository 34 | /// - `View` - View 35 | /// - `Error` - Error 36 | pub struct MaterializedView 37 | where 38 | Repository: ViewStateRepository, 39 | View: ViewStateComputation, 40 | { 41 | repository: Repository, 42 | view: View, 43 | _marker: PhantomData<(S, E, Error)>, 44 | } 45 | 46 | impl ViewStateComputation 47 | for MaterializedView 48 | where 49 | Repository: ViewStateRepository, 50 | View: ViewStateComputation, 51 | { 52 | /// Computes new state based on the current state and the events. 53 | fn compute_new_state(&self, current_state: Option, events: &[&E]) -> S { 54 | self.view.compute_new_state(current_state, events) 55 | } 56 | } 57 | 58 | impl ViewStateRepository 59 | for MaterializedView 60 | where 61 | Repository: ViewStateRepository + Sync, 62 | View: ViewStateComputation + Sync, 63 | E: Sync, 64 | S: Sync, 65 | Error: Sync, 66 | { 67 | /// Fetches current state, based on the event. 68 | async fn fetch_state(&self, event: &E) -> Result, Error> { 69 | let state = self.repository.fetch_state(event).await?; 70 | Ok(state) 71 | } 72 | /// Saves the new state. 73 | async fn save(&self, state: &S) -> Result { 74 | self.repository.save(state).await 75 | } 76 | } 77 | 78 | impl MaterializedView 79 | where 80 | Repository: ViewStateRepository + Sync, 81 | View: ViewStateComputation + Sync, 82 | E: Sync, 83 | S: Sync, 84 | Error: Sync, 85 | { 86 | /// Creates a new instance of [MaterializedView]. 87 | pub fn new(repository: Repository, view: View) -> Self { 88 | MaterializedView { 89 | repository, 90 | view, 91 | _marker: PhantomData, 92 | } 93 | } 94 | /// Handles the event by fetching the state from the repository, computing new state based on the current state and the event, and saving the new state to the repository. 95 | pub async fn handle(&self, event: &E) -> Result { 96 | let state = self.fetch_state(event).await?; 97 | let new_state = self.compute_new_state(state, &[event]); 98 | let saved_state = self.save(&new_state).await?; 99 | Ok(saved_state) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/saga.rs: -------------------------------------------------------------------------------- 1 | use crate::{ReactFunction, Saga3, Saga4, Saga5, Saga6, Sum, Sum3, Sum4, Sum5, Sum6}; 2 | 3 | /// [Saga] is a datatype that represents the central point of control, deciding what to execute next (`A`), based on the action result (`AR`). 4 | /// It has two generic parameters `AR`/Action Result, `A`/Action , representing the type of the values that Saga may contain or use. 5 | /// `'a` is used as a lifetime parameter, indicating that all references contained within the struct (e.g., references within the function closures) must have a lifetime that is at least as long as 'a. 6 | /// 7 | /// It is common to consider Event as Action Result, and Command as Action, but it is not mandatory. 8 | /// For example, Action Result can be a request response from a remote service. 9 | /// 10 | /// ## Example 11 | /// 12 | /// ``` 13 | /// use fmodel_rust::saga::Saga; 14 | /// 15 | /// fn saga<'a>() -> Saga<'a, OrderEvent, ShipmentCommand> { 16 | /// Saga { 17 | /// react: Box::new(|event| match event { 18 | /// OrderEvent::Created(created_event) => { 19 | /// vec![ShipmentCommand::Create(CreateShipmentCommand { 20 | /// shipment_id: created_event.order_id, 21 | /// order_id: created_event.order_id, 22 | /// customer_name: created_event.customer_name.to_owned(), 23 | /// items: created_event.items.to_owned(), 24 | /// })] 25 | /// } 26 | /// OrderEvent::Updated(_updated_event) => { 27 | /// vec![] 28 | /// } 29 | /// OrderEvent::Cancelled(_cancelled_event) => { 30 | /// vec![] 31 | /// } 32 | /// }), 33 | /// } 34 | /// } 35 | /// 36 | /// #[derive(Debug, PartialEq)] 37 | /// #[allow(dead_code)] 38 | /// pub enum ShipmentCommand { 39 | /// Create(CreateShipmentCommand), 40 | /// } 41 | /// 42 | /// #[derive(Debug, PartialEq)] 43 | /// pub struct CreateShipmentCommand { 44 | /// pub shipment_id: u32, 45 | /// pub order_id: u32, 46 | /// pub customer_name: String, 47 | /// pub items: Vec, 48 | /// } 49 | /// 50 | /// #[derive(Debug)] 51 | /// pub enum OrderEvent { 52 | /// Created(OrderCreatedEvent), 53 | /// Updated(OrderUpdatedEvent), 54 | /// Cancelled(OrderCancelledEvent), 55 | /// } 56 | /// 57 | /// #[derive(Debug)] 58 | /// pub struct OrderCreatedEvent { 59 | /// pub order_id: u32, 60 | /// pub customer_name: String, 61 | /// pub items: Vec, 62 | /// } 63 | /// 64 | /// #[derive(Debug)] 65 | /// pub struct OrderUpdatedEvent { 66 | /// pub order_id: u32, 67 | /// pub updated_items: Vec, 68 | /// } 69 | /// 70 | /// #[derive(Debug)] 71 | /// pub struct OrderCancelledEvent { 72 | /// pub order_id: u32, 73 | /// } 74 | /// 75 | /// let saga: Saga = saga(); 76 | /// let order_created_event = OrderEvent::Created(OrderCreatedEvent { 77 | /// order_id: 1, 78 | /// customer_name: "John Doe".to_string(), 79 | /// items: vec!["Item 1".to_string(), "Item 2".to_string()], 80 | /// }); 81 | /// 82 | /// let commands = (saga.react)(&order_created_event); 83 | /// ``` 84 | pub struct Saga<'a, AR: 'a, A: 'a> { 85 | /// The `react` function is driving the next action based on the action result. 86 | pub react: ReactFunction<'a, AR, A>, 87 | } 88 | 89 | impl<'a, AR, A> Saga<'a, AR, A> { 90 | /// Maps the Saga over the A/Action type parameter. 91 | /// Creates a new instance of [Saga]``. 92 | pub fn map_action(self, f: F) -> Saga<'a, AR, A2> 93 | where 94 | F: Fn(&A) -> A2 + Send + Sync + 'a, 95 | { 96 | let new_react = Box::new(move |ar: &AR| { 97 | let a = (self.react)(ar); 98 | a.into_iter().map(|a: A| f(&a)).collect() 99 | }); 100 | 101 | Saga { react: new_react } 102 | } 103 | 104 | /// Maps the Saga over the AR/ActionResult type parameter. 105 | /// Creates a new instance of [Saga]``. 106 | pub fn map_action_result(self, f: F) -> Saga<'a, AR2, A> 107 | where 108 | F: Fn(&AR2) -> AR + Send + Sync + 'a, 109 | { 110 | let new_react = Box::new(move |ar2: &AR2| { 111 | let ar = f(ar2); 112 | (self.react)(&ar) 113 | }); 114 | 115 | Saga { react: new_react } 116 | } 117 | 118 | /// Combines two sagas into one. 119 | /// Creates a new instance of a Saga by combining two sagas of type `AR`, `A` and `AR2`, `A2` into a new saga of type `Sum`, `Sum` 120 | #[deprecated( 121 | since = "0.8.0", 122 | note = "Use the `merge` function instead. This ensures all your sagas can subscribe to all `Event`/`E` in the system." 123 | )] 124 | pub fn combine(self, saga2: Saga<'a, AR2, A2>) -> Saga<'a, Sum, Sum> { 125 | let new_react = Box::new(move |ar: &Sum| match ar { 126 | Sum::First(ar) => { 127 | let a = (self.react)(ar); 128 | a.into_iter().map(|a: A| Sum::Second(a)).collect() 129 | } 130 | Sum::Second(ar2) => { 131 | let a2 = (saga2.react)(ar2); 132 | a2.into_iter().map(|a: A2| Sum::First(a)).collect() 133 | } 134 | }); 135 | 136 | Saga { react: new_react } 137 | } 138 | 139 | /// Merges two sagas into one. 140 | /// Creates a new instance of a Saga by merging two sagas of type `AR`, `A` and `AR`, `A2` into a new saga of type `AR`, `Sum` 141 | /// Similar to `combine`, but the event type is the same for both sagas. 142 | /// This ensures all your sagas can subscribe to all `Event`/`E` in the system. 143 | pub fn merge(self, saga2: Saga<'a, AR, A2>) -> Saga<'a, AR, Sum> { 144 | let new_react = Box::new(move |ar: &AR| { 145 | let a: Vec> = (self.react)(ar) 146 | .into_iter() 147 | .map(|a: A| Sum::Second(a)) 148 | .collect(); 149 | let a2: Vec> = (saga2.react)(ar) 150 | .into_iter() 151 | .map(|a2: A2| Sum::First(a2)) 152 | .collect(); 153 | 154 | a.into_iter().chain(a2).collect() 155 | }); 156 | 157 | Saga { react: new_react } 158 | } 159 | 160 | /// Merges three sagas into one. 161 | pub fn merge3( 162 | self, 163 | saga2: Saga<'a, AR, A2>, 164 | saga3: Saga<'a, AR, A3>, 165 | ) -> Saga3<'a, AR, A, A2, A3> 166 | where 167 | A: Clone, 168 | A2: Clone, 169 | A3: Clone, 170 | { 171 | self.merge(saga2) 172 | .merge(saga3) 173 | .map_action(|a: &Sum>| match a { 174 | Sum::First(a) => Sum3::Third(a.clone()), 175 | Sum::Second(Sum::First(a)) => Sum3::Second(a.clone()), 176 | Sum::Second(Sum::Second(a)) => Sum3::First(a.clone()), 177 | }) 178 | } 179 | 180 | /// Merges four sagas into one. 181 | pub fn merge4( 182 | self, 183 | saga2: Saga<'a, AR, A2>, 184 | saga3: Saga<'a, AR, A3>, 185 | saga4: Saga<'a, AR, A4>, 186 | ) -> Saga4<'a, AR, A, A2, A3, A4> 187 | where 188 | A: Clone, 189 | A2: Clone, 190 | A3: Clone, 191 | A4: Clone, 192 | { 193 | self.merge(saga2).merge(saga3).merge(saga4).map_action( 194 | |a: &Sum>>| match a { 195 | Sum::First(a) => Sum4::Fourth(a.clone()), 196 | Sum::Second(Sum::First(a)) => Sum4::Third(a.clone()), 197 | Sum::Second(Sum::Second(Sum::First(a))) => Sum4::Second(a.clone()), 198 | Sum::Second(Sum::Second(Sum::Second(a))) => Sum4::First(a.clone()), 199 | }, 200 | ) 201 | } 202 | 203 | #[allow(clippy::type_complexity)] 204 | /// Merges five sagas into one. 205 | pub fn merge5( 206 | self, 207 | saga2: Saga<'a, AR, A2>, 208 | saga3: Saga<'a, AR, A3>, 209 | saga4: Saga<'a, AR, A4>, 210 | saga5: Saga<'a, AR, A5>, 211 | ) -> Saga5<'a, AR, A, A2, A3, A4, A5> 212 | where 213 | A: Clone, 214 | A2: Clone, 215 | A3: Clone, 216 | A4: Clone, 217 | A5: Clone, 218 | { 219 | self.merge(saga2) 220 | .merge(saga3) 221 | .merge(saga4) 222 | .merge(saga5) 223 | .map_action(|a: &Sum>>>| match a { 224 | Sum::First(a) => Sum5::Fifth(a.clone()), 225 | Sum::Second(Sum::First(a)) => Sum5::Fourth(a.clone()), 226 | Sum::Second(Sum::Second(Sum::First(a))) => Sum5::Third(a.clone()), 227 | Sum::Second(Sum::Second(Sum::Second(Sum::First(a)))) => Sum5::Second(a.clone()), 228 | Sum::Second(Sum::Second(Sum::Second(Sum::Second(a)))) => Sum5::First(a.clone()), 229 | }) 230 | } 231 | 232 | #[allow(clippy::type_complexity)] 233 | /// Merges six sagas into one. 234 | pub fn merge6( 235 | self, 236 | saga2: Saga<'a, AR, A2>, 237 | saga3: Saga<'a, AR, A3>, 238 | saga4: Saga<'a, AR, A4>, 239 | saga5: Saga<'a, AR, A5>, 240 | saga6: Saga<'a, AR, A6>, 241 | ) -> Saga6<'a, AR, A, A2, A3, A4, A5, A6> 242 | where 243 | A: Clone, 244 | A2: Clone, 245 | A3: Clone, 246 | A4: Clone, 247 | A5: Clone, 248 | A6: Clone, 249 | { 250 | self.merge(saga2) 251 | .merge(saga3) 252 | .merge(saga4) 253 | .merge(saga5) 254 | .merge(saga6) 255 | .map_action( 256 | |a: &Sum>>>>| match a { 257 | Sum::First(a) => Sum6::Sixth(a.clone()), 258 | Sum::Second(Sum::First(a)) => Sum6::Fifth(a.clone()), 259 | Sum::Second(Sum::Second(Sum::First(a))) => Sum6::Fourth(a.clone()), 260 | Sum::Second(Sum::Second(Sum::Second(Sum::First(a)))) => Sum6::Third(a.clone()), 261 | Sum::Second(Sum::Second(Sum::Second(Sum::Second(Sum::First(a))))) => { 262 | Sum6::Second(a.clone()) 263 | } 264 | Sum::Second(Sum::Second(Sum::Second(Sum::Second(Sum::Second(a))))) => { 265 | Sum6::First(a.clone()) 266 | } 267 | }, 268 | ) 269 | } 270 | } 271 | 272 | /// Formalizes the `Action Computation` algorithm for the `saga` to handle events/action_results, and produce new commands/actions. 273 | pub trait ActionComputation { 274 | /// Computes new commands/actions based on the event/action_result. 275 | fn compute_new_actions(&self, event: &AR) -> Vec; 276 | } 277 | 278 | impl ActionComputation for Saga<'_, AR, A> { 279 | /// Computes new commands/actions based on the event/action_result. 280 | fn compute_new_actions(&self, event: &AR) -> Vec { 281 | (self.react)(event).into_iter().collect() 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /src/saga_manager.rs: -------------------------------------------------------------------------------- 1 | use std::future::Future; 2 | use std::marker::PhantomData; 3 | 4 | use crate::saga::ActionComputation; 5 | 6 | /// Publishes the action/command to some external system. 7 | /// 8 | /// Generic parameter: 9 | /// 10 | /// - `A`. - action 11 | /// - `Error` - error 12 | pub trait ActionPublisher { 13 | /// Publishes the action/command to some external system, returning either the actions that are successfully published or error. 14 | /// Desugared `async fn publish(&self, action: &[A]) -> Result, Error>;` to a normal `fn` that returns `impl Future`, and adds bound `Send`. 15 | /// You can freely move between the `async fn` and `-> impl Future` spelling in your traits and impls. This is true even when one form has a Send bound. 16 | fn publish(&self, action: &[A]) -> impl Future, Error>> + Send; 17 | } 18 | 19 | /// Saga Manager. 20 | /// 21 | /// It is using a `Saga` to react to the action result and to publish the new actions. 22 | /// It is using an [ActionPublisher] to publish the new actions. 23 | /// 24 | /// Generic parameters: 25 | /// - `A` - Action / Command 26 | /// - `AR` - Action Result / Event 27 | /// - `Publisher` - Action Publisher 28 | /// - `Error` - Error 29 | pub struct SagaManager 30 | where 31 | Publisher: ActionPublisher, 32 | Saga: ActionComputation, 33 | { 34 | action_publisher: Publisher, 35 | saga: Saga, 36 | _marker: PhantomData<(A, AR, Error)>, 37 | } 38 | 39 | impl ActionComputation 40 | for SagaManager 41 | where 42 | Publisher: ActionPublisher, 43 | Saga: ActionComputation, 44 | { 45 | /// Computes new actions based on the action result. 46 | fn compute_new_actions(&self, action_result: &AR) -> Vec { 47 | self.saga.compute_new_actions(action_result) 48 | } 49 | } 50 | 51 | impl ActionPublisher 52 | for SagaManager 53 | where 54 | Publisher: ActionPublisher + Sync, 55 | Saga: ActionComputation + Sync, 56 | A: Sync, 57 | AR: Sync, 58 | Error: Sync, 59 | { 60 | /// Publishes the action/command to some external system, returning either the actions that are successfully published or error. 61 | async fn publish(&self, action: &[A]) -> Result, Error> { 62 | self.action_publisher.publish(action).await 63 | } 64 | } 65 | 66 | impl SagaManager 67 | where 68 | Publisher: ActionPublisher + Sync, 69 | Saga: ActionComputation + Sync, 70 | A: Sync, 71 | AR: Sync, 72 | Error: Sync, 73 | { 74 | /// Creates a new instance of [SagaManager]. 75 | pub fn new(action_publisher: Publisher, saga: Saga) -> Self { 76 | SagaManager { 77 | action_publisher, 78 | saga, 79 | _marker: PhantomData, 80 | } 81 | } 82 | /// Handles the `action result` by computing new `actions` based on `action result`, and publishing new `actions` to the external system. 83 | /// In most cases: 84 | /// - the `action result` is an `event` that you react, 85 | /// - the `actions` are `commands` that you publish downstream. 86 | pub async fn handle(&self, action_result: &AR) -> Result, Error> { 87 | let new_actions = self.compute_new_actions(action_result); 88 | let published_actions = self.publish(&new_actions).await?; 89 | Ok(published_actions) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/specification.rs: -------------------------------------------------------------------------------- 1 | //! ## A test specification DSL for deciders and views that supports the given-when-then format. 2 | 3 | use crate::{ 4 | decider::{Decider, EventComputation, StateComputation}, 5 | view::{View, ViewStateComputation}, 6 | }; 7 | 8 | // ######################################################## 9 | // ############# Decider Specification DSL ################ 10 | // ######################################################## 11 | 12 | /// A test specification DSL for deciders that supports the `given-when-then` format. 13 | /// The DSL is used to specify the events that have already occurred (GIVEN), the command that is being executed (WHEN), and the expected events (THEN) that should be generated. 14 | pub struct DeciderTestSpecification<'a, Command, State, Event, Error> 15 | where 16 | Event: PartialEq + std::fmt::Debug, 17 | Error: PartialEq + std::fmt::Debug, 18 | { 19 | events: Vec, 20 | state: Option, 21 | command: Option, 22 | decider: Option>, 23 | } 24 | 25 | impl Default 26 | for DeciderTestSpecification<'_, Command, State, Event, Error> 27 | where 28 | Event: PartialEq + std::fmt::Debug, 29 | Error: PartialEq + std::fmt::Debug, 30 | { 31 | fn default() -> Self { 32 | Self { 33 | events: Vec::new(), 34 | state: None, 35 | command: None, 36 | decider: None, 37 | } 38 | } 39 | } 40 | 41 | impl<'a, Command, State, Event, Error> DeciderTestSpecification<'a, Command, State, Event, Error> 42 | where 43 | Event: PartialEq + std::fmt::Debug, 44 | State: PartialEq + std::fmt::Debug, 45 | Error: PartialEq + std::fmt::Debug, 46 | { 47 | #[allow(dead_code)] 48 | /// Specify the decider you want to test 49 | pub fn for_decider(mut self, decider: Decider<'a, Command, State, Event, Error>) -> Self { 50 | self.decider = Some(decider); 51 | self 52 | } 53 | 54 | #[allow(dead_code)] 55 | /// Given preconditions / previous events 56 | pub fn given(mut self, events: Vec) -> Self { 57 | self.events = events; 58 | self 59 | } 60 | 61 | #[allow(dead_code)] 62 | /// Given preconditions / previous state 63 | pub fn given_state(mut self, state: Option) -> Self { 64 | self.state = state; 65 | self 66 | } 67 | 68 | #[allow(dead_code)] 69 | /// When action/command 70 | pub fn when(mut self, command: Command) -> Self { 71 | self.command = Some(command); 72 | self 73 | } 74 | 75 | #[allow(dead_code)] 76 | /// Then expect result / new events 77 | pub fn then(self, expected_events: Vec) { 78 | let decider = self 79 | .decider 80 | .expect("Decider must be initialized. Did you forget to call `for_decider`?"); 81 | let command = self 82 | .command 83 | .expect("Command must be initialized. Did you forget to call `when`?"); 84 | let events = self.events; 85 | 86 | let new_events_result = decider.compute_new_events(&events, &command); 87 | let new_events = match new_events_result { 88 | Ok(events) => events, 89 | Err(error) => panic!( 90 | "Events were expected but the decider returned an error instead: {:?}", 91 | error 92 | ), 93 | }; 94 | assert_eq!(new_events, expected_events); 95 | } 96 | 97 | #[allow(dead_code)] 98 | /// Then expect result / new events 99 | pub fn then_state(self, expected_state: State) { 100 | let decider = self 101 | .decider 102 | .expect("Decider must be initialized. Did you forget to call `for_decider`?"); 103 | let command = self 104 | .command 105 | .expect("Command must be initialized. Did you forget to call `when`?"); 106 | let state = self.state; 107 | 108 | let new_state_result = decider.compute_new_state(state, &command); 109 | let new_state = match new_state_result { 110 | Ok(state) => state, 111 | Err(error) => panic!( 112 | "State was expected but the decider returned an error instead: {:?}", 113 | error 114 | ), 115 | }; 116 | assert_eq!(new_state, expected_state); 117 | } 118 | 119 | #[allow(dead_code)] 120 | /// Then expect error result / these are not events 121 | pub fn then_error(self, expected_error: Error) { 122 | let decider = self 123 | .decider 124 | .expect("Decider must be initialized. Did you forget to call `for_decider`?"); 125 | let command = self 126 | .command 127 | .expect("Command must be initialized. Did you forget to call `when`?"); 128 | let events = self.events; 129 | 130 | let error_result = decider.compute_new_events(&events, &command); 131 | let error = match error_result { 132 | Ok(events) => panic!( 133 | "An error was expected but the decider returned events instead: {:?}", 134 | events 135 | ), 136 | Err(error) => error, 137 | }; 138 | assert_eq!(error, expected_error); 139 | } 140 | } 141 | 142 | // ######################################################## 143 | // ############### View Specification DSL ################# 144 | // ######################################################## 145 | 146 | /// A test specification DSL for views that supports the `given-then`` format. 147 | /// The DSL is used to specify the events that have already occurred (GIVEN), and the expected view state (THEN) that should be generated based on these events. 148 | pub struct ViewTestSpecification<'a, State, Event> 149 | where 150 | State: PartialEq + std::fmt::Debug, 151 | { 152 | events: Vec, 153 | view: Option>, 154 | } 155 | 156 | impl Default for ViewTestSpecification<'_, State, Event> 157 | where 158 | State: PartialEq + std::fmt::Debug, 159 | { 160 | fn default() -> Self { 161 | Self { 162 | events: Vec::new(), 163 | view: None, 164 | } 165 | } 166 | } 167 | 168 | impl<'a, State, Event> ViewTestSpecification<'a, State, Event> 169 | where 170 | State: PartialEq + std::fmt::Debug, 171 | { 172 | #[allow(dead_code)] 173 | /// Specify the view you want to test 174 | pub fn for_view(mut self, view: View<'a, State, Event>) -> Self { 175 | self.view = Some(view); 176 | self 177 | } 178 | 179 | #[allow(dead_code)] 180 | /// Given preconditions / events 181 | pub fn given(mut self, events: Vec) -> Self { 182 | self.events = events; 183 | self 184 | } 185 | 186 | #[allow(dead_code)] 187 | /// Then expect evolving new state of the view 188 | pub fn then(self, expected_state: State) { 189 | let view = self 190 | .view 191 | .expect("View must be initialized. Did you forget to call `for_view`?"); 192 | 193 | let events = self.events; 194 | 195 | let initial_state = (view.initial_state)(); 196 | let event_refs: Vec<&Event> = events.iter().collect(); 197 | let new_state_result = view.compute_new_state(Some(initial_state), &event_refs); 198 | 199 | assert_eq!(new_state_result, expected_state); 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/view.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use crate::{EvolveFunction, InitialStateFunction, Sum, View3, View4, View5, View6}; 4 | 5 | /// [View] represents the event handling algorithm, responsible for translating the events into denormalized state, which is more adequate for querying. 6 | /// It has two generic parameters `S`/State, `E`/Event , representing the type of the values that View may contain or use. 7 | /// `'a` is used as a lifetime parameter, indicating that all references contained within the struct (e.g., references within the function closures) must have a lifetime that is at least as long as 'a. 8 | /// 9 | /// ## Example 10 | /// ``` 11 | /// use fmodel_rust::view::View; 12 | /// 13 | /// fn view<'a>() -> View<'a, OrderViewState, OrderEvent> { 14 | /// View { 15 | /// // Exhaustive pattern matching is used to handle the events (modeled as Enum - SUM/OR type). 16 | /// evolve: Box::new(|state, event| { 17 | /// let mut new_state = state.clone(); 18 | /// match event { 19 | /// OrderEvent::Created(created_event) => { 20 | /// new_state.order_id = created_event.order_id; 21 | /// new_state.customer_name = created_event.customer_name.to_owned(); 22 | /// new_state.items = created_event.items.to_owned(); 23 | /// } 24 | /// OrderEvent::Updated(updated_event) => { 25 | /// new_state.items = updated_event.updated_items.to_owned(); 26 | /// } 27 | /// OrderEvent::Cancelled(_) => { 28 | /// new_state.is_cancelled = true; 29 | /// } 30 | /// } 31 | /// new_state 32 | /// }), 33 | /// initial_state: Box::new(|| OrderViewState { 34 | /// order_id: 0, 35 | /// customer_name: "".to_string(), 36 | /// items: Vec::new(), 37 | /// is_cancelled: false, 38 | /// }), 39 | /// } 40 | /// } 41 | /// 42 | /// #[derive(Debug)] 43 | /// pub enum OrderEvent { 44 | /// Created(OrderCreatedEvent), 45 | /// Updated(OrderUpdatedEvent), 46 | /// Cancelled(OrderCancelledEvent), 47 | /// } 48 | /// 49 | /// #[derive(Debug)] 50 | /// pub struct OrderCreatedEvent { 51 | /// pub order_id: u32, 52 | /// pub customer_name: String, 53 | /// pub items: Vec, 54 | /// } 55 | /// 56 | /// #[derive(Debug)] 57 | /// pub struct OrderUpdatedEvent { 58 | /// pub order_id: u32, 59 | /// pub updated_items: Vec, 60 | /// } 61 | /// 62 | /// #[derive(Debug)] 63 | /// pub struct OrderCancelledEvent { 64 | /// pub order_id: u32, 65 | /// } 66 | /// 67 | /// #[derive(Debug, Clone)] 68 | /// struct OrderViewState { 69 | /// order_id: u32, 70 | /// customer_name: String, 71 | /// items: Vec, 72 | /// is_cancelled: bool, 73 | /// } 74 | /// 75 | /// let view: View = view(); 76 | /// let order_created_event = OrderEvent::Created(OrderCreatedEvent { 77 | /// order_id: 1, 78 | /// customer_name: "John Doe".to_string(), 79 | /// items: vec!["Item 1".to_string(), "Item 2".to_string()], 80 | /// }); 81 | /// let new_state = (view.evolve)(&(view.initial_state)(), &order_created_event); 82 | /// ``` 83 | pub struct View<'a, S: 'a, E: 'a> { 84 | /// The `evolve` function is the main state evolution algorithm. 85 | pub evolve: EvolveFunction<'a, S, E>, 86 | /// The `initial_state` function is the initial state. 87 | pub initial_state: InitialStateFunction<'a, S>, 88 | } 89 | 90 | impl<'a, S, E> View<'a, S, E> { 91 | /// Maps the View over the S/State type parameter. 92 | /// Creates a new instance of [View]``. 93 | pub fn map_state(self, f1: F1, f2: F2) -> View<'a, S2, E> 94 | where 95 | F1: Fn(&S2) -> S + Send + Sync + 'a, 96 | F2: Fn(&S) -> S2 + Send + Sync + 'a, 97 | { 98 | let f1 = Arc::new(f1); 99 | let f2 = Arc::new(f2); 100 | 101 | let new_evolve = { 102 | let f2 = Arc::clone(&f2); 103 | Box::new(move |s2: &S2, e: &E| { 104 | let s = f1(s2); 105 | f2(&(self.evolve)(&s, e)) 106 | }) 107 | }; 108 | 109 | let new_initial_state = { Box::new(move || f2(&(self.initial_state)())) }; 110 | 111 | View { 112 | evolve: new_evolve, 113 | initial_state: new_initial_state, 114 | } 115 | } 116 | 117 | /// Maps the View over the E/Event type parameter. 118 | /// Creates a new instance of [View]``. 119 | pub fn map_event(self, f: F) -> View<'a, S, E2> 120 | where 121 | F: Fn(&E2) -> E + Send + Sync + 'a, 122 | { 123 | let new_evolve = Box::new(move |s: &S, e2: &E2| { 124 | let e = f(e2); 125 | (self.evolve)(s, &e) 126 | }); 127 | 128 | let new_initial_state = Box::new(move || (self.initial_state)()); 129 | 130 | View { 131 | evolve: new_evolve, 132 | initial_state: new_initial_state, 133 | } 134 | } 135 | 136 | /// Combines two views into one. 137 | /// Creates a new instance of a View by combining two views of type `S`, `E` and `S2`, `E2` into a new view of type `(S, S2)`, `Sum` 138 | /// Combines two views that operate on different event types (`E`` and `E2``) into a new view operating on `Sum` 139 | #[deprecated( 140 | since = "0.8.0", 141 | note = "Use the `merge` function instead. This ensures all your views can subscribe to all `Event`/`E` in the system." 142 | )] 143 | pub fn combine(self, view2: View<'a, S2, E2>) -> View<'a, (S, S2), Sum> 144 | where 145 | S: Clone, 146 | S2: Clone, 147 | { 148 | let new_evolve = Box::new(move |s: &(S, S2), e: &Sum| match e { 149 | Sum::First(e) => { 150 | let s1 = &s.0; 151 | let new_state = (self.evolve)(s1, e); 152 | (new_state, s.1.to_owned()) 153 | } 154 | Sum::Second(e) => { 155 | let s2 = &s.1; 156 | let new_state = (view2.evolve)(s2, e); 157 | (s.0.to_owned(), new_state) 158 | } 159 | }); 160 | 161 | let new_initial_state = Box::new(move || { 162 | let s1 = (self.initial_state)(); 163 | let s2 = (view2.initial_state)(); 164 | (s1, s2) 165 | }); 166 | 167 | View { 168 | evolve: new_evolve, 169 | initial_state: new_initial_state, 170 | } 171 | } 172 | 173 | /// Merges two views into one. 174 | /// Creates a new instance of a View by merging two views of type `S`, `E` and `S2`, `E` into a new view of type `(S, S2)`, `E` 175 | /// Similar to `combine`, but the event type is the same for both views. 176 | /// This ensures all your views can subscribe to all `Event`/`E` in the system. 177 | pub fn merge(self, view2: View<'a, S2, E>) -> View<'a, (S, S2), E> 178 | where 179 | S: Clone, 180 | S2: Clone, 181 | { 182 | let new_evolve = Box::new(move |s: &(S, S2), e: &E| { 183 | let s1 = &s.0; 184 | let s2 = &s.1; 185 | 186 | let new_state = (self.evolve)(s1, e); 187 | let new_state2 = (view2.evolve)(s2, e); 188 | (new_state, new_state2) 189 | }); 190 | 191 | let new_initial_state = Box::new(move || { 192 | let s1 = (self.initial_state)(); 193 | let s2 = (view2.initial_state)(); 194 | (s1, s2) 195 | }); 196 | 197 | View { 198 | evolve: new_evolve, 199 | initial_state: new_initial_state, 200 | } 201 | } 202 | 203 | /// Merges three views into one. 204 | pub fn merge3( 205 | self, 206 | view2: View<'a, S2, E>, 207 | view3: View<'a, S3, E>, 208 | ) -> View3<'a, S, S2, S3, E> 209 | where 210 | S: Clone, 211 | S2: Clone, 212 | S3: Clone, 213 | { 214 | self.merge(view2).merge(view3).map_state( 215 | |s: &(S, S2, S3)| ((s.0.clone(), s.1.clone()), s.2.clone()), 216 | |s: &((S, S2), S3)| (s.0 .0.clone(), s.0 .1.clone(), s.1.clone()), 217 | ) 218 | } 219 | 220 | /// Merges four views into one. 221 | pub fn merge4( 222 | self, 223 | view2: View<'a, S2, E>, 224 | view3: View<'a, S3, E>, 225 | view4: View<'a, S4, E>, 226 | ) -> View4<'a, S, S2, S3, S4, E> 227 | where 228 | S: Clone, 229 | S2: Clone, 230 | S3: Clone, 231 | S4: Clone, 232 | { 233 | self.merge(view2).merge(view3).merge(view4).map_state( 234 | |s: &(S, S2, S3, S4)| (((s.0.clone(), s.1.clone()), s.2.clone()), s.3.clone()), 235 | |s: &(((S, S2), S3), S4)| { 236 | ( 237 | s.0 .0 .0.clone(), 238 | s.0 .0 .1.clone(), 239 | s.0 .1.clone(), 240 | s.1.clone(), 241 | ) 242 | }, 243 | ) 244 | } 245 | 246 | #[allow(clippy::type_complexity)] 247 | /// Merges five views into one. 248 | pub fn merge5( 249 | self, 250 | view2: View<'a, S2, E>, 251 | view3: View<'a, S3, E>, 252 | view4: View<'a, S4, E>, 253 | view5: View<'a, S5, E>, 254 | ) -> View5<'a, S, S2, S3, S4, S5, E> 255 | where 256 | S: Clone, 257 | S2: Clone, 258 | S3: Clone, 259 | S4: Clone, 260 | S5: Clone, 261 | { 262 | self.merge(view2) 263 | .merge(view3) 264 | .merge(view4) 265 | .merge(view5) 266 | .map_state( 267 | |s: &(S, S2, S3, S4, S5)| { 268 | ( 269 | (((s.0.clone(), s.1.clone()), s.2.clone()), s.3.clone()), 270 | s.4.clone(), 271 | ) 272 | }, 273 | |s: &((((S, S2), S3), S4), S5)| { 274 | ( 275 | s.0 .0 .0 .0.clone(), 276 | s.0 .0 .0 .1.clone(), 277 | s.0 .0 .1.clone(), 278 | s.0 .1.clone(), 279 | s.1.clone(), 280 | ) 281 | }, 282 | ) 283 | } 284 | 285 | #[allow(clippy::type_complexity)] 286 | /// Merges six views into one. 287 | pub fn merge6( 288 | self, 289 | view2: View<'a, S2, E>, 290 | view3: View<'a, S3, E>, 291 | view4: View<'a, S4, E>, 292 | view5: View<'a, S5, E>, 293 | view6: View<'a, S6, E>, 294 | ) -> View6<'a, S, S2, S3, S4, S5, S6, E> 295 | where 296 | S: Clone, 297 | S2: Clone, 298 | S3: Clone, 299 | S4: Clone, 300 | S5: Clone, 301 | S6: Clone, 302 | { 303 | self.merge(view2) 304 | .merge(view3) 305 | .merge(view4) 306 | .merge(view5) 307 | .merge(view6) 308 | .map_state( 309 | |s: &(S, S2, S3, S4, S5, S6)| { 310 | ( 311 | ( 312 | (((s.0.clone(), s.1.clone()), s.2.clone()), s.3.clone()), 313 | s.4.clone(), 314 | ), 315 | s.5.clone(), 316 | ) 317 | }, 318 | |s: &(((((S, S2), S3), S4), S5), S6)| { 319 | ( 320 | s.0 .0 .0 .0 .0.clone(), 321 | s.0 .0 .0 .0 .1.clone(), 322 | s.0 .0 .0 .1.clone(), 323 | s.0 .0 .1.clone(), 324 | s.0 .1.clone(), 325 | s.1.clone(), 326 | ) 327 | }, 328 | ) 329 | } 330 | } 331 | 332 | /// Formalizes the `State Computation` algorithm for the `view` to handle events based on the current state, and produce new state. 333 | pub trait ViewStateComputation { 334 | /// Computes new state based on the current state and the events. 335 | fn compute_new_state(&self, current_state: Option, events: &[&E]) -> S; 336 | } 337 | 338 | impl ViewStateComputation for View<'_, S, E> { 339 | /// Computes new state based on the current state and the events. 340 | fn compute_new_state(&self, current_state: Option, events: &[&E]) -> S { 341 | let effective_current_state = current_state.unwrap_or_else(|| (self.initial_state)()); 342 | events.iter().fold(effective_current_state, |state, event| { 343 | (self.evolve)(&state, event) 344 | }) 345 | } 346 | } 347 | -------------------------------------------------------------------------------- /tests/aggregate_test.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::sync::{Arc, Mutex}; 3 | use std::thread; 4 | 5 | use fmodel_rust::aggregate::{ 6 | EventRepository, EventSourcedAggregate, StateRepository, StateStoredAggregate, 7 | }; 8 | use fmodel_rust::decider::Decider; 9 | use fmodel_rust::Identifier; 10 | 11 | use crate::api::{ 12 | CancelOrderCommand, CreateOrderCommand, OrderCancelledEvent, OrderCommand, OrderCreatedEvent, 13 | OrderEvent, OrderState, OrderUpdatedEvent, UpdateOrderCommand, 14 | }; 15 | use crate::application::AggregateError; 16 | 17 | mod api; 18 | mod application; 19 | 20 | /// A simple in-memory event repository - infrastructure 21 | struct InMemoryOrderEventRepository { 22 | events: Mutex>, 23 | } 24 | 25 | impl InMemoryOrderEventRepository { 26 | fn new() -> Self { 27 | InMemoryOrderEventRepository { 28 | events: Mutex::new(vec![]), 29 | } 30 | } 31 | } 32 | 33 | /// Implementation of [EventRepository] for [InMemoryOrderEventRepository] - infrastructure 34 | impl EventRepository 35 | for InMemoryOrderEventRepository 36 | { 37 | async fn fetch_events( 38 | &self, 39 | command: &OrderCommand, 40 | ) -> Result, AggregateError> { 41 | Ok(self 42 | .events 43 | .lock() 44 | .unwrap() 45 | .clone() 46 | .into_iter() 47 | .filter(|(event, _)| event.identifier() == command.identifier()) 48 | .collect()) 49 | } 50 | 51 | async fn save(&self, events: &[OrderEvent]) -> Result, AggregateError> { 52 | let mut latest_version = self 53 | .version_provider(events.first().unwrap()) 54 | .await? 55 | .unwrap_or(-1); 56 | let events = events 57 | .iter() 58 | .map(|event| { 59 | latest_version += 1; 60 | (event.clone(), latest_version) 61 | }) 62 | .collect::>(); 63 | 64 | self.events 65 | .lock() 66 | .unwrap() 67 | .extend_from_slice(&events.clone()); 68 | Ok(events) 69 | } 70 | 71 | async fn version_provider(&self, event: &OrderEvent) -> Result, AggregateError> { 72 | Ok(self 73 | .events 74 | .lock() 75 | .unwrap() 76 | .clone() 77 | .into_iter() 78 | .filter(|(e, _)| e.identifier() == event.identifier()) 79 | .map(|(_, version)| version) 80 | .last()) 81 | } 82 | } 83 | 84 | struct InMemoryOrderStateRepository { 85 | states: Mutex>, 86 | } 87 | 88 | impl InMemoryOrderStateRepository { 89 | fn new() -> Self { 90 | InMemoryOrderStateRepository { 91 | states: Mutex::new(HashMap::new()), 92 | } 93 | } 94 | } 95 | 96 | // Implementation of [StateRepository] for [InMemoryOrderStateRepository] 97 | impl StateRepository 98 | for InMemoryOrderStateRepository 99 | { 100 | async fn fetch_state( 101 | &self, 102 | command: &OrderCommand, 103 | ) -> Result, AggregateError> { 104 | Ok(self 105 | .states 106 | .lock() 107 | .unwrap() 108 | .get(&command.identifier().parse::().unwrap()) 109 | .cloned()) 110 | } 111 | 112 | async fn save( 113 | &self, 114 | state: &OrderState, 115 | version: &Option, 116 | ) -> Result<(OrderState, i32), AggregateError> { 117 | let version = version.to_owned().unwrap_or(0); 118 | self.states 119 | .lock() 120 | .unwrap() 121 | .insert(state.order_id, (state.clone(), version + 1)); 122 | Ok((state.clone(), version)) 123 | } 124 | } 125 | 126 | /// Decider for the Order aggregate - Domain logic 127 | fn decider<'a>() -> Decider<'a, OrderCommand, OrderState, OrderEvent> { 128 | Decider { 129 | decide: Box::new(|command, state| match command { 130 | OrderCommand::Create(cmd) => Ok(vec![OrderEvent::Created(OrderCreatedEvent { 131 | order_id: cmd.order_id, 132 | customer_name: cmd.customer_name.to_owned(), 133 | items: cmd.items.to_owned(), 134 | })]), 135 | OrderCommand::Update(cmd) => { 136 | if state.order_id == cmd.order_id { 137 | Ok(vec![OrderEvent::Updated(OrderUpdatedEvent { 138 | order_id: cmd.order_id, 139 | updated_items: cmd.new_items.to_owned(), 140 | })]) 141 | } else { 142 | Ok(vec![]) 143 | } 144 | } 145 | OrderCommand::Cancel(cmd) => { 146 | if state.order_id == cmd.order_id { 147 | Ok(vec![OrderEvent::Cancelled(OrderCancelledEvent { 148 | order_id: cmd.order_id, 149 | })]) 150 | } else { 151 | Ok(vec![]) 152 | } 153 | } 154 | }), 155 | evolve: Box::new(|state, event| { 156 | let mut new_state = state.clone(); 157 | match event { 158 | OrderEvent::Created(evt) => { 159 | new_state.order_id = evt.order_id; 160 | new_state.customer_name = evt.customer_name.to_owned(); 161 | new_state.items = evt.items.to_owned(); 162 | } 163 | OrderEvent::Updated(evt) => { 164 | new_state.items = evt.updated_items.to_owned(); 165 | } 166 | OrderEvent::Cancelled(_) => { 167 | new_state.is_cancelled = true; 168 | } 169 | } 170 | new_state 171 | }), 172 | initial_state: Box::new(|| OrderState { 173 | order_id: 0, 174 | customer_name: "".to_string(), 175 | items: Vec::new(), 176 | is_cancelled: false, 177 | }), 178 | } 179 | } 180 | 181 | #[tokio::test] 182 | async fn es_test() { 183 | let repository = InMemoryOrderEventRepository::new(); 184 | let aggregate = Arc::new(EventSourcedAggregate::new( 185 | repository, 186 | decider().map_error(|()| AggregateError::DomainError("Decider error".to_string())), 187 | )); 188 | // Makes a clone of the Arc pointer. 189 | // This creates another pointer to the same allocation, increasing the strong reference count. 190 | let aggregate2 = Arc::clone(&aggregate); 191 | 192 | // Let's spawn two threads to simulate two concurrent requests 193 | let handle1 = thread::spawn(|| async move { 194 | let command = OrderCommand::Create(CreateOrderCommand { 195 | order_id: 1, 196 | customer_name: "John Doe".to_string(), 197 | items: vec!["Item 1".to_string(), "Item 2".to_string()], 198 | }); 199 | 200 | let result = aggregate.handle(&command).await; 201 | assert!(result.is_ok()); 202 | assert_eq!( 203 | result.unwrap(), 204 | [( 205 | OrderEvent::Created(OrderCreatedEvent { 206 | order_id: 1, 207 | customer_name: "John Doe".to_string(), 208 | items: vec!["Item 1".to_string(), "Item 2".to_string()], 209 | }), 210 | 0 211 | )] 212 | ); 213 | let command = OrderCommand::Update(UpdateOrderCommand { 214 | order_id: 1, 215 | new_items: vec!["Item 3".to_string(), "Item 4".to_string()], 216 | }); 217 | let result = aggregate.handle(&command).await; 218 | assert!(result.is_ok()); 219 | assert_eq!( 220 | result.unwrap(), 221 | [( 222 | OrderEvent::Updated(OrderUpdatedEvent { 223 | order_id: 1, 224 | updated_items: vec!["Item 3".to_string(), "Item 4".to_string()], 225 | }), 226 | 1 227 | )] 228 | ); 229 | let command = OrderCommand::Cancel(CancelOrderCommand { order_id: 1 }); 230 | let result = aggregate.handle(&command).await; 231 | assert!(result.is_ok()); 232 | assert_eq!( 233 | result.unwrap(), 234 | [( 235 | OrderEvent::Cancelled(OrderCancelledEvent { order_id: 1 }), 236 | 2 237 | )] 238 | ); 239 | }); 240 | 241 | let handle2 = thread::spawn(|| async move { 242 | let command = OrderCommand::Create(CreateOrderCommand { 243 | order_id: 2, 244 | customer_name: "John Doe".to_string(), 245 | items: vec!["Item 1".to_string(), "Item 2".to_string()], 246 | }); 247 | let result = aggregate2.handle(&command).await; 248 | assert!(result.is_ok()); 249 | assert_eq!( 250 | result.unwrap(), 251 | [( 252 | OrderEvent::Created(OrderCreatedEvent { 253 | order_id: 2, 254 | customer_name: "John Doe".to_string(), 255 | items: vec!["Item 1".to_string(), "Item 2".to_string()], 256 | }), 257 | 0 258 | )] 259 | ); 260 | let command = OrderCommand::Update(UpdateOrderCommand { 261 | order_id: 2, 262 | new_items: vec!["Item 3".to_string(), "Item 4".to_string()], 263 | }); 264 | let result = aggregate2.handle(&command).await; 265 | assert!(result.is_ok()); 266 | assert_eq!( 267 | result.unwrap(), 268 | [( 269 | OrderEvent::Updated(OrderUpdatedEvent { 270 | order_id: 2, 271 | updated_items: vec!["Item 3".to_string(), "Item 4".to_string()], 272 | }), 273 | 1 274 | )] 275 | ); 276 | let command = OrderCommand::Cancel(CancelOrderCommand { order_id: 2 }); 277 | let result = aggregate2.handle(&command).await; 278 | assert!(result.is_ok()); 279 | assert_eq!( 280 | result.unwrap(), 281 | [( 282 | OrderEvent::Cancelled(OrderCancelledEvent { order_id: 2 }), 283 | 2 284 | )] 285 | ); 286 | }); 287 | 288 | handle1.join().unwrap().await; 289 | handle2.join().unwrap().await; 290 | } 291 | 292 | #[tokio::test] 293 | async fn ss_test() { 294 | let repository = InMemoryOrderStateRepository::new(); 295 | let aggregate = Arc::new(StateStoredAggregate::new( 296 | repository, 297 | decider().map_error(|()| AggregateError::DomainError("Decider error".to_string())), 298 | )); 299 | let aggregate2 = Arc::clone(&aggregate); 300 | 301 | let handle1 = thread::spawn(|| async move { 302 | let command = OrderCommand::Create(CreateOrderCommand { 303 | order_id: 1, 304 | customer_name: "John Doe".to_string(), 305 | items: vec!["Item 1".to_string(), "Item 2".to_string()], 306 | }); 307 | let result = aggregate.handle(&command).await; 308 | assert!(result.is_ok()); 309 | assert_eq!( 310 | result.unwrap(), 311 | ( 312 | OrderState { 313 | order_id: 1, 314 | customer_name: "John Doe".to_string(), 315 | items: vec!["Item 1".to_string(), "Item 2".to_string()], 316 | is_cancelled: false, 317 | }, 318 | 0 319 | ) 320 | ); 321 | let command = OrderCommand::Update(UpdateOrderCommand { 322 | order_id: 1, 323 | new_items: vec!["Item 3".to_string(), "Item 4".to_string()], 324 | }); 325 | let result = aggregate.handle(&command).await; 326 | assert!(result.is_ok()); 327 | assert_eq!( 328 | result.unwrap(), 329 | ( 330 | OrderState { 331 | order_id: 1, 332 | customer_name: "John Doe".to_string(), 333 | items: vec!["Item 3".to_string(), "Item 4".to_string()], 334 | is_cancelled: false, 335 | }, 336 | 1 337 | ) 338 | ); 339 | let command = OrderCommand::Cancel(CancelOrderCommand { order_id: 1 }); 340 | let result = aggregate.handle(&command).await; 341 | assert!(result.is_ok()); 342 | assert_eq!( 343 | result.unwrap(), 344 | ( 345 | OrderState { 346 | order_id: 1, 347 | customer_name: "John Doe".to_string(), 348 | items: vec!["Item 3".to_string(), "Item 4".to_string()], 349 | is_cancelled: true, 350 | }, 351 | 2 352 | ) 353 | ); 354 | }); 355 | 356 | let handle2 = thread::spawn(|| async move { 357 | let command = OrderCommand::Create(CreateOrderCommand { 358 | order_id: 2, 359 | customer_name: "John Doe".to_string(), 360 | items: vec!["Item 1".to_string(), "Item 2".to_string()], 361 | }); 362 | let result = aggregate2.handle(&command).await; 363 | assert!(result.is_ok()); 364 | assert_eq!( 365 | result.unwrap(), 366 | ( 367 | OrderState { 368 | order_id: 2, 369 | customer_name: "John Doe".to_string(), 370 | items: vec!["Item 1".to_string(), "Item 2".to_string()], 371 | is_cancelled: false, 372 | }, 373 | 0 374 | ) 375 | ); 376 | let command = OrderCommand::Update(UpdateOrderCommand { 377 | order_id: 2, 378 | new_items: vec!["Item 3".to_string(), "Item 4".to_string()], 379 | }); 380 | let result = aggregate2.handle(&command).await; 381 | assert!(result.is_ok()); 382 | assert_eq!( 383 | result.unwrap(), 384 | ( 385 | OrderState { 386 | order_id: 2, 387 | customer_name: "John Doe".to_string(), 388 | items: vec!["Item 3".to_string(), "Item 4".to_string()], 389 | is_cancelled: false, 390 | }, 391 | 1 392 | ) 393 | ); 394 | let command = OrderCommand::Cancel(CancelOrderCommand { order_id: 2 }); 395 | let result = aggregate2.handle(&command).await; 396 | assert!(result.is_ok()); 397 | assert_eq!( 398 | result.unwrap(), 399 | ( 400 | OrderState { 401 | order_id: 2, 402 | customer_name: "John Doe".to_string(), 403 | items: vec!["Item 3".to_string(), "Item 4".to_string()], 404 | is_cancelled: true, 405 | }, 406 | 2 407 | ) 408 | ); 409 | }); 410 | 411 | handle1.join().unwrap().await; 412 | handle2.join().unwrap().await; 413 | } 414 | -------------------------------------------------------------------------------- /tests/api/mod.rs: -------------------------------------------------------------------------------- 1 | // ################################################################### 2 | // ############################ Order API ############################ 3 | // ################################################################### 4 | 5 | use fmodel_rust::Identifier; 6 | 7 | /// The state of the Order entity 8 | #[derive(Debug, Clone, PartialEq)] 9 | pub struct OrderState { 10 | pub order_id: u32, 11 | pub customer_name: String, 12 | pub items: Vec, 13 | pub is_cancelled: bool, 14 | } 15 | 16 | /// The state of the ViewOrder entity / It represents the Query Model 17 | #[derive(Debug, Clone, PartialEq)] 18 | pub struct OrderViewState { 19 | pub order_id: u32, 20 | pub customer_name: String, 21 | pub items: Vec, 22 | pub is_cancelled: bool, 23 | } 24 | 25 | /// A second version of the ViewOrder entity / It represents the Query Model 26 | #[derive(Debug, Clone, PartialEq)] 27 | pub struct OrderView2State { 28 | pub order_id: u32, 29 | pub customer_name: String, 30 | pub items: Vec, 31 | pub is_cancelled: bool, 32 | } 33 | 34 | /// All variants of Order commands 35 | #[derive(Debug, Clone, PartialEq)] 36 | #[allow(dead_code)] 37 | pub enum OrderCommand { 38 | Create(CreateOrderCommand), 39 | Update(UpdateOrderCommand), 40 | Cancel(CancelOrderCommand), 41 | } 42 | 43 | #[derive(Debug, Clone, PartialEq)] 44 | pub struct CreateOrderCommand { 45 | pub order_id: u32, 46 | pub customer_name: String, 47 | pub items: Vec, 48 | } 49 | 50 | #[derive(Debug, Clone, PartialEq)] 51 | pub struct UpdateOrderCommand { 52 | pub order_id: u32, 53 | pub new_items: Vec, 54 | } 55 | 56 | #[derive(Debug, Clone, PartialEq)] 57 | pub struct CancelOrderCommand { 58 | pub order_id: u32, 59 | } 60 | 61 | impl Identifier for OrderCommand { 62 | #[allow(dead_code)] 63 | fn identifier(&self) -> String { 64 | match self { 65 | OrderCommand::Create(c) => c.order_id.to_string(), 66 | OrderCommand::Update(c) => c.order_id.to_string(), 67 | OrderCommand::Cancel(c) => c.order_id.to_string(), 68 | } 69 | } 70 | } 71 | 72 | /// All variants of Order events 73 | #[derive(Debug, Clone, PartialEq)] 74 | #[allow(dead_code)] 75 | pub enum OrderEvent { 76 | Created(OrderCreatedEvent), 77 | Updated(OrderUpdatedEvent), 78 | Cancelled(OrderCancelledEvent), 79 | } 80 | 81 | #[derive(Debug, Clone, PartialEq)] 82 | pub struct OrderCreatedEvent { 83 | pub order_id: u32, 84 | pub customer_name: String, 85 | pub items: Vec, 86 | } 87 | 88 | #[derive(Debug, Clone, PartialEq)] 89 | pub struct OrderUpdatedEvent { 90 | pub order_id: u32, 91 | pub updated_items: Vec, 92 | } 93 | 94 | #[derive(Debug, Clone, PartialEq)] 95 | pub struct OrderCancelledEvent { 96 | pub order_id: u32, 97 | } 98 | 99 | /// Provides a way to get the id of the Order events 100 | impl Identifier for OrderEvent { 101 | #[allow(dead_code)] 102 | fn identifier(&self) -> String { 103 | match self { 104 | OrderEvent::Created(c) => c.order_id.to_string(), 105 | OrderEvent::Updated(c) => c.order_id.to_string(), 106 | OrderEvent::Cancelled(c) => c.order_id.to_string(), 107 | } 108 | } 109 | } 110 | 111 | // ###################################################################### 112 | // ############################ Shipment API ############################ 113 | // ###################################################################### 114 | 115 | /// The state of the Shipment entity 116 | #[derive(Debug, Clone, PartialEq)] 117 | pub struct ShipmentState { 118 | pub shipment_id: u32, 119 | pub order_id: u32, 120 | pub customer_name: String, 121 | pub items: Vec, 122 | } 123 | 124 | /// The state of the ViewShipment entity / It represents the Query Model 125 | #[derive(Debug, Clone, PartialEq)] 126 | pub struct ShipmentViewState { 127 | pub shipment_id: u32, 128 | pub order_id: u32, 129 | pub customer_name: String, 130 | pub items: Vec, 131 | } 132 | 133 | /// All variants of Shipment commands 134 | #[derive(Debug, PartialEq, Clone)] 135 | #[allow(dead_code)] 136 | pub enum ShipmentCommand { 137 | Create(CreateShipmentCommand), 138 | } 139 | 140 | #[derive(Debug, PartialEq, Clone)] 141 | pub struct CreateShipmentCommand { 142 | pub shipment_id: u32, 143 | pub order_id: u32, 144 | pub customer_name: String, 145 | pub items: Vec, 146 | } 147 | 148 | /// Provides a way to get the id of the Shipment commands 149 | impl Identifier for ShipmentCommand { 150 | #[allow(dead_code)] 151 | fn identifier(&self) -> String { 152 | match self { 153 | ShipmentCommand::Create(c) => c.shipment_id.to_string(), 154 | } 155 | } 156 | } 157 | 158 | /// All variants of Shipment events 159 | #[derive(Debug, PartialEq, Clone)] 160 | #[allow(dead_code)] 161 | pub enum ShipmentEvent { 162 | Created(ShipmentCreatedEvent), 163 | } 164 | #[derive(Debug, PartialEq, Clone)] 165 | pub struct ShipmentCreatedEvent { 166 | pub shipment_id: u32, 167 | pub order_id: u32, 168 | pub customer_name: String, 169 | pub items: Vec, 170 | } 171 | 172 | /// Provides a way to get the id of the Shipment events 173 | impl ShipmentEvent { 174 | #[allow(dead_code)] 175 | pub fn id(&self) -> u32 { 176 | match self { 177 | ShipmentEvent::Created(c) => c.shipment_id.to_owned(), 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /tests/application/mod.rs: -------------------------------------------------------------------------------- 1 | use derive_more::Display; 2 | use fmodel_rust::{Identifier, Sum}; 3 | use std::error::Error; 4 | 5 | use crate::api::{ 6 | CancelOrderCommand, CreateOrderCommand, CreateShipmentCommand, OrderCancelledEvent, 7 | OrderCommand, OrderCreatedEvent, OrderEvent, OrderUpdatedEvent, ShipmentCommand, 8 | ShipmentCreatedEvent, ShipmentEvent, UpdateOrderCommand, 9 | }; 10 | 11 | /// The command enum for all the domain commands (shipment and order) 12 | /// It is convenient to have a single enum for all the command variants in your system to make it easy to combine all deciders into a single decider 13 | /// Consider exposing this API to the outside world, instead of exposing the Order or Shipment commands individually. It is on you! 14 | #[derive(Debug, PartialEq, Clone)] 15 | #[allow(dead_code)] 16 | pub enum Command { 17 | ShipmentCreate(CreateShipmentCommand), 18 | OrderCreate(CreateOrderCommand), 19 | OrderUpdate(UpdateOrderCommand), 20 | OrderCancel(CancelOrderCommand), 21 | } 22 | 23 | /// A mapping function to contra map the domain command to the inconvenient Sum 24 | #[allow(dead_code)] 25 | pub fn command_from_sum(command: &Command) -> Sum { 26 | match command { 27 | Command::ShipmentCreate(c) => Sum::Second(ShipmentCommand::Create(c.to_owned())), 28 | Command::OrderCreate(c) => Sum::First(OrderCommand::Create(c.to_owned())), 29 | Command::OrderUpdate(c) => Sum::First(OrderCommand::Update(c.to_owned())), 30 | Command::OrderCancel(c) => Sum::First(OrderCommand::Cancel(c.to_owned())), 31 | } 32 | } 33 | /// A mapping function to map the inconvenient Sum to the domain command 34 | #[allow(dead_code)] 35 | pub fn sum_to_command(command: &Sum) -> Command { 36 | match command { 37 | Sum::First(c) => match c { 38 | OrderCommand::Create(c) => Command::OrderCreate(c.to_owned()), 39 | OrderCommand::Update(c) => Command::OrderUpdate(c.to_owned()), 40 | OrderCommand::Cancel(c) => Command::OrderCancel(c.to_owned()), 41 | }, 42 | Sum::Second(c) => match c { 43 | ShipmentCommand::Create(c) => Command::ShipmentCreate(c.to_owned()), 44 | }, 45 | } 46 | } 47 | #[allow(dead_code)] 48 | pub fn sum_to_command2(command: &Sum) -> Command { 49 | match command { 50 | Sum::First(c) => match c { 51 | ShipmentCommand::Create(c) => Command::ShipmentCreate(c.to_owned()), 52 | }, 53 | Sum::Second(c) => match c { 54 | OrderCommand::Create(c) => Command::OrderCreate(c.to_owned()), 55 | OrderCommand::Update(c) => Command::OrderUpdate(c.to_owned()), 56 | OrderCommand::Cancel(c) => Command::OrderCancel(c.to_owned()), 57 | }, 58 | } 59 | } 60 | 61 | /// The event enum for all the domain events (shipment and order) 62 | /// It is convenient to have a single enum for all the event variants in your system to make it easy to combine all deciders/sagas/views into a single decider/saga/view 63 | /// Consider exposing this API to the outside world, instead of exposing the Order or Shipment events individually. It is on you! 64 | #[derive(Debug, PartialEq, Clone)] 65 | #[allow(dead_code)] 66 | pub enum Event { 67 | ShipmentCreated(ShipmentCreatedEvent), 68 | OrderCreated(OrderCreatedEvent), 69 | OrderUpdated(OrderUpdatedEvent), 70 | OrderCancelled(OrderCancelledEvent), 71 | } 72 | 73 | /// A mapping function to contra map the domain event to the inconvenient Sum 74 | #[allow(dead_code)] 75 | pub fn event_from_sum(event: &Event) -> Sum { 76 | match event { 77 | Event::ShipmentCreated(c) => Sum::Second(ShipmentEvent::Created(c.to_owned())), 78 | Event::OrderCreated(c) => Sum::First(OrderEvent::Created(c.to_owned())), 79 | Event::OrderUpdated(c) => Sum::First(OrderEvent::Updated(c.to_owned())), 80 | Event::OrderCancelled(c) => Sum::First(OrderEvent::Cancelled(c.to_owned())), 81 | } 82 | } 83 | #[allow(dead_code)] 84 | pub fn event_from_sum2(event: &Event) -> Sum { 85 | match event { 86 | Event::ShipmentCreated(c) => Sum::First(ShipmentEvent::Created(c.to_owned())), 87 | Event::OrderCreated(c) => Sum::Second(OrderEvent::Created(c.to_owned())), 88 | Event::OrderUpdated(c) => Sum::Second(OrderEvent::Updated(c.to_owned())), 89 | Event::OrderCancelled(c) => Sum::Second(OrderEvent::Cancelled(c.to_owned())), 90 | } 91 | } 92 | /// A mapping function to map the inconvenient Sum to the domain event 93 | #[allow(dead_code)] 94 | pub fn sum_to_event(event: &Sum) -> Event { 95 | match event { 96 | Sum::First(e) => match e { 97 | OrderEvent::Created(c) => Event::OrderCreated(c.to_owned()), 98 | OrderEvent::Updated(c) => Event::OrderUpdated(c.to_owned()), 99 | OrderEvent::Cancelled(c) => Event::OrderCancelled(c.to_owned()), 100 | }, 101 | Sum::Second(e) => match e { 102 | ShipmentEvent::Created(c) => Event::ShipmentCreated(c.to_owned()), 103 | }, 104 | } 105 | } 106 | 107 | impl Identifier for Event { 108 | fn identifier(&self) -> String { 109 | match self { 110 | Event::ShipmentCreated(evt) => evt.shipment_id.to_string(), 111 | Event::OrderCreated(evt) => evt.order_id.to_string(), 112 | Event::OrderUpdated(evt) => evt.order_id.to_string(), 113 | Event::OrderCancelled(evt) => evt.order_id.to_string(), 114 | } 115 | } 116 | } 117 | 118 | impl Identifier for Command { 119 | fn identifier(&self) -> String { 120 | match self { 121 | Command::OrderCreate(cmd) => cmd.order_id.to_string(), 122 | Command::OrderUpdate(cmd) => cmd.order_id.to_string(), 123 | Command::OrderCancel(cmd) => cmd.order_id.to_string(), 124 | Command::ShipmentCreate(cmd) => cmd.shipment_id.to_string(), 125 | } 126 | } 127 | } 128 | 129 | /// Error type for the application/aggregate 130 | #[derive(Debug, Display)] 131 | #[allow(dead_code)] 132 | pub enum AggregateError { 133 | DomainError(String), 134 | FetchEvents(String), 135 | SaveEvents(String), 136 | FetchState(String), 137 | SaveState(String), 138 | } 139 | 140 | impl Error for AggregateError {} 141 | 142 | /// Error type for the application/materialized view 143 | #[derive(Debug, Display)] 144 | #[allow(dead_code)] 145 | pub enum MaterializedViewError { 146 | FetchState(String), 147 | SaveState(String), 148 | } 149 | 150 | impl Error for MaterializedViewError {} 151 | 152 | /// Error type for the saga manager 153 | #[derive(Debug, Display)] 154 | #[allow(dead_code)] 155 | pub enum SagaManagerError { 156 | PublishAction(String), 157 | } 158 | 159 | impl Error for SagaManagerError {} 160 | -------------------------------------------------------------------------------- /tests/decider_test.rs: -------------------------------------------------------------------------------- 1 | use fmodel_rust::decider::Decider; 2 | use fmodel_rust::specification::DeciderTestSpecification; 3 | 4 | use crate::api::{ 5 | CreateOrderCommand, CreateShipmentCommand, OrderCancelledEvent, OrderCommand, 6 | OrderCreatedEvent, OrderEvent, OrderState, OrderUpdatedEvent, ShipmentCommand, 7 | ShipmentCreatedEvent, ShipmentEvent, ShipmentState, 8 | }; 9 | use crate::application::Event::{OrderCreated, ShipmentCreated}; 10 | use crate::application::{command_from_sum, event_from_sum, sum_to_event, Command, Event}; 11 | 12 | mod api; 13 | mod application; 14 | 15 | fn order_decider<'a>() -> Decider<'a, OrderCommand, OrderState, OrderEvent> { 16 | Decider { 17 | decide: Box::new(|command, state| match command { 18 | OrderCommand::Create(cmd) => Ok(vec![OrderEvent::Created(OrderCreatedEvent { 19 | order_id: cmd.order_id, 20 | customer_name: cmd.customer_name.to_owned(), 21 | items: cmd.items.to_owned(), 22 | })]), 23 | OrderCommand::Update(cmd) => { 24 | if state.order_id == cmd.order_id { 25 | Ok(vec![OrderEvent::Updated(OrderUpdatedEvent { 26 | order_id: cmd.order_id, 27 | updated_items: cmd.new_items.to_owned(), 28 | })]) 29 | } else { 30 | Ok(vec![]) 31 | } 32 | } 33 | OrderCommand::Cancel(cmd) => { 34 | if state.order_id == cmd.order_id { 35 | Ok(vec![OrderEvent::Cancelled(OrderCancelledEvent { 36 | order_id: cmd.order_id, 37 | })]) 38 | } else { 39 | Ok(vec![]) 40 | } 41 | } 42 | }), 43 | evolve: Box::new(|state, event| { 44 | let mut new_state = state.clone(); 45 | match event { 46 | OrderEvent::Created(evt) => { 47 | new_state.order_id = evt.order_id; 48 | new_state.customer_name = evt.customer_name.to_owned(); 49 | new_state.items = evt.items.to_owned(); 50 | } 51 | OrderEvent::Updated(evt) => { 52 | new_state.items = evt.updated_items.to_owned(); 53 | } 54 | OrderEvent::Cancelled(_) => { 55 | new_state.is_cancelled = true; 56 | } 57 | } 58 | new_state 59 | }), 60 | initial_state: Box::new(|| OrderState { 61 | order_id: 0, 62 | customer_name: "".to_string(), 63 | items: Vec::new(), 64 | is_cancelled: false, 65 | }), 66 | } 67 | } 68 | 69 | fn shipment_decider<'a>() -> Decider<'a, ShipmentCommand, ShipmentState, ShipmentEvent> { 70 | Decider { 71 | decide: Box::new(|command, _state| match command { 72 | ShipmentCommand::Create(cmd) => { 73 | Ok(vec![ShipmentEvent::Created(ShipmentCreatedEvent { 74 | shipment_id: cmd.shipment_id, 75 | order_id: cmd.order_id, 76 | customer_name: cmd.customer_name.to_owned(), 77 | items: cmd.items.to_owned(), 78 | })]) 79 | } 80 | }), 81 | evolve: Box::new(|state, event| { 82 | let mut new_state = state.clone(); 83 | match event { 84 | ShipmentEvent::Created(evt) => { 85 | new_state.shipment_id = evt.shipment_id; 86 | new_state.order_id = evt.order_id; 87 | new_state.customer_name = evt.customer_name.to_owned(); 88 | new_state.items = evt.items.to_owned(); 89 | } 90 | } 91 | new_state 92 | }), 93 | initial_state: Box::new(|| ShipmentState { 94 | shipment_id: 0, 95 | order_id: 0, 96 | customer_name: "".to_string(), 97 | items: Vec::new(), 98 | }), 99 | } 100 | } 101 | 102 | fn combined_decider<'a>() -> Decider<'a, Command, (OrderState, ShipmentState), Event> { 103 | order_decider() 104 | .combine(shipment_decider()) 105 | .map_command(command_from_sum) // Decider> 106 | .map_event(event_from_sum, sum_to_event) 107 | } 108 | 109 | #[test] 110 | fn create_order_event_sourced_test() { 111 | let create_order_command = CreateOrderCommand { 112 | order_id: 1, 113 | customer_name: "John Doe".to_string(), 114 | items: vec!["Item 1".to_string(), "Item 2".to_string()], 115 | }; 116 | 117 | // Test the OrderDecider (event-sourced) 118 | DeciderTestSpecification::default() 119 | .for_decider(self::order_decider()) // Set the decider 120 | .given(vec![]) // no existing events 121 | .when(OrderCommand::Create(create_order_command.clone())) // Create an Order 122 | .then(vec![OrderEvent::Created(OrderCreatedEvent { 123 | order_id: 1, 124 | customer_name: "John Doe".to_string(), 125 | items: vec!["Item 1".to_string(), "Item 2".to_string()], 126 | })]); 127 | 128 | // Test the Decider that combines OrderDecider and ShipmentDecider and can handle both OrderCommand and ShipmentCommand and produce Event (event-sourced) 129 | DeciderTestSpecification::default() 130 | .for_decider(self::combined_decider()) 131 | .given(vec![]) 132 | .when(Command::OrderCreate(create_order_command.clone())) 133 | .then(vec![OrderCreated(OrderCreatedEvent { 134 | order_id: 1, 135 | customer_name: "John Doe".to_string(), 136 | items: vec!["Item 1".to_string(), "Item 2".to_string()], 137 | })]); 138 | } 139 | 140 | #[test] 141 | fn create_shipment_event_sourced_test() { 142 | let create_shipment_command = CreateShipmentCommand { 143 | shipment_id: 1, 144 | order_id: 1, 145 | customer_name: "John Doe".to_string(), 146 | items: vec!["Item 1".to_string(), "Item 2".to_string()], 147 | }; 148 | 149 | DeciderTestSpecification::default() 150 | .for_decider(self::combined_decider()) 151 | .given(vec![]) 152 | .when(Command::ShipmentCreate(create_shipment_command.clone())) 153 | .then(vec![ShipmentCreated(ShipmentCreatedEvent { 154 | shipment_id: 1, 155 | order_id: 1, 156 | customer_name: "John Doe".to_string(), 157 | items: vec!["Item 1".to_string(), "Item 2".to_string()], 158 | })]); 159 | } 160 | 161 | #[test] 162 | fn create_order_state_stored_test() { 163 | let create_order_command = CreateOrderCommand { 164 | order_id: 1, 165 | customer_name: "John Doe".to_string(), 166 | items: vec!["Item 1".to_string(), "Item 2".to_string()], 167 | }; 168 | 169 | // Test the OrderDecider (state stored) 170 | DeciderTestSpecification::default() 171 | .for_decider(self::order_decider()) // Set the decider 172 | .given_state(None) // no existing state 173 | .when(OrderCommand::Create(create_order_command.clone())) // Create an Order 174 | .then_state(OrderState { 175 | order_id: 1, 176 | customer_name: "John Doe".to_string(), 177 | items: vec!["Item 1".to_string(), "Item 2".to_string()], 178 | is_cancelled: false, 179 | }); 180 | } 181 | 182 | #[test] 183 | fn create_shipment_state_stored_test() { 184 | let create_shipment_command = CreateShipmentCommand { 185 | shipment_id: 1, 186 | order_id: 1, 187 | customer_name: "John Doe".to_string(), 188 | items: vec!["Item 1".to_string(), "Item 2".to_string()], 189 | }; 190 | // Test the Decider (state stored) that combines OrderDecider and ShipmentDecider and can handle both OrderCommand and ShipmentCommand and produce a tuple of (OrderState, ShipmentState) 191 | DeciderTestSpecification::default() 192 | .for_decider(self::combined_decider()) 193 | .given_state(None) 194 | .when(Command::ShipmentCreate(create_shipment_command.clone())) 195 | .then_state(( 196 | OrderState { 197 | order_id: 0, 198 | customer_name: "".to_string(), 199 | items: Vec::new(), 200 | is_cancelled: false, 201 | }, 202 | ShipmentState { 203 | shipment_id: 1, 204 | order_id: 1, 205 | customer_name: "John Doe".to_string(), 206 | items: vec!["Item 1".to_string(), "Item 2".to_string()], 207 | }, 208 | )); 209 | } 210 | -------------------------------------------------------------------------------- /tests/materialized_view_merged_test.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::sync::{Arc, Mutex}; 3 | use std::thread; 4 | 5 | use fmodel_rust::materialized_view::{MaterializedView, ViewStateRepository}; 6 | use fmodel_rust::view::View; 7 | use fmodel_rust::Identifier; 8 | 9 | use crate::api::{ 10 | OrderCancelledEvent, OrderCreatedEvent, OrderUpdatedEvent, OrderViewState, ShipmentViewState, 11 | }; 12 | use crate::application::{Event, MaterializedViewError}; 13 | 14 | mod api; 15 | mod application; 16 | 17 | fn order_view<'a>() -> View<'a, OrderViewState, Event> { 18 | View { 19 | evolve: Box::new(|state, event| { 20 | let mut new_state = state.clone(); 21 | match event { 22 | Event::OrderCreated(evt) => { 23 | new_state.order_id = evt.order_id; 24 | new_state.customer_name = evt.customer_name.to_owned(); 25 | new_state.items = evt.items.to_owned(); 26 | } 27 | Event::OrderUpdated(evt) => { 28 | new_state.items = evt.updated_items.to_owned(); 29 | } 30 | Event::OrderCancelled(_) => { 31 | new_state.is_cancelled = true; 32 | } 33 | Event::ShipmentCreated(_) => {} 34 | } 35 | new_state 36 | }), 37 | initial_state: Box::new(|| OrderViewState { 38 | order_id: 0, 39 | customer_name: "".to_string(), 40 | items: Vec::new(), 41 | is_cancelled: false, 42 | }), 43 | } 44 | } 45 | 46 | fn shipment_view<'a>() -> View<'a, ShipmentViewState, Event> { 47 | View { 48 | evolve: Box::new(|state, event| { 49 | let mut new_state = state.clone(); 50 | match event { 51 | Event::ShipmentCreated(evt) => { 52 | new_state.shipment_id = evt.shipment_id; 53 | new_state.order_id = evt.order_id; 54 | new_state.customer_name = evt.customer_name.to_owned(); 55 | new_state.items = evt.items.to_owned(); 56 | } 57 | Event::OrderCreated(_) => {} 58 | Event::OrderUpdated(_) => {} 59 | Event::OrderCancelled(_) => {} 60 | } 61 | new_state 62 | }), 63 | initial_state: Box::new(|| ShipmentViewState { 64 | shipment_id: 0, 65 | order_id: 0, 66 | customer_name: "".to_string(), 67 | items: Vec::new(), 68 | }), 69 | } 70 | } 71 | 72 | struct InMemoryViewStateRepository { 73 | states: Mutex>, 74 | } 75 | 76 | impl InMemoryViewStateRepository { 77 | fn new() -> Self { 78 | InMemoryViewStateRepository { 79 | states: Mutex::new(HashMap::new()), 80 | } 81 | } 82 | } 83 | 84 | // Implementation of [ViewStateRepository] for [InMemoryViewOrderStateRepository] 85 | impl ViewStateRepository 86 | for InMemoryViewStateRepository 87 | { 88 | async fn fetch_state( 89 | &self, 90 | event: &Event, 91 | ) -> Result, MaterializedViewError> { 92 | Ok(self 93 | .states 94 | .lock() 95 | .unwrap() 96 | .get(&event.identifier().parse::().unwrap()) 97 | .cloned()) 98 | } 99 | 100 | async fn save( 101 | &self, 102 | state: &(OrderViewState, ShipmentViewState), 103 | ) -> Result<(OrderViewState, ShipmentViewState), MaterializedViewError> { 104 | self.states 105 | .lock() 106 | .unwrap() 107 | .insert(state.0.order_id, state.clone()); 108 | Ok(state.clone()) 109 | } 110 | } 111 | 112 | #[tokio::test] 113 | async fn test() { 114 | let combined_view = order_view().merge(shipment_view()); 115 | let repository = InMemoryViewStateRepository::new(); 116 | let materialized_view = Arc::new(MaterializedView::new(repository, combined_view)); 117 | let materialized_view1 = Arc::clone(&materialized_view); 118 | let materialized_view2 = Arc::clone(&materialized_view); 119 | 120 | // Lets spawn two threads to simulate two concurrent requests 121 | let handle1 = thread::spawn(|| async move { 122 | let event = Event::OrderCreated(OrderCreatedEvent { 123 | order_id: 1, 124 | customer_name: "John Doe".to_string(), 125 | items: vec!["Item 1".to_string(), "Item 2".to_string()], 126 | }); 127 | let result = materialized_view1.handle(&event).await; 128 | assert!(result.is_ok()); 129 | assert_eq!( 130 | result.unwrap(), 131 | ( 132 | OrderViewState { 133 | order_id: 1, 134 | customer_name: "John Doe".to_string(), 135 | items: vec!["Item 1".to_string(), "Item 2".to_string()], 136 | is_cancelled: false, 137 | }, 138 | ShipmentViewState { 139 | shipment_id: 0, 140 | order_id: 0, 141 | customer_name: "".to_string(), 142 | items: Vec::new(), 143 | } 144 | ) 145 | ); 146 | let event = Event::OrderUpdated(OrderUpdatedEvent { 147 | order_id: 1, 148 | updated_items: vec!["Item 3".to_string(), "Item 4".to_string()], 149 | }); 150 | let result = materialized_view1.handle(&event).await; 151 | assert!(result.is_ok()); 152 | assert_eq!( 153 | result.unwrap(), 154 | ( 155 | OrderViewState { 156 | order_id: 1, 157 | customer_name: "John Doe".to_string(), 158 | items: vec!["Item 3".to_string(), "Item 4".to_string()], 159 | is_cancelled: false, 160 | }, 161 | ShipmentViewState { 162 | shipment_id: 0, 163 | order_id: 0, 164 | customer_name: "".to_string(), 165 | items: Vec::new(), 166 | } 167 | ) 168 | ); 169 | let event = Event::OrderCancelled(OrderCancelledEvent { order_id: 1 }); 170 | let result = materialized_view1.handle(&event).await; 171 | assert!(result.is_ok()); 172 | assert_eq!( 173 | result.unwrap(), 174 | ( 175 | OrderViewState { 176 | order_id: 1, 177 | customer_name: "John Doe".to_string(), 178 | items: vec!["Item 3".to_string(), "Item 4".to_string()], 179 | is_cancelled: true, 180 | }, 181 | ShipmentViewState { 182 | shipment_id: 0, 183 | order_id: 0, 184 | customer_name: "".to_string(), 185 | items: Vec::new(), 186 | } 187 | ) 188 | ); 189 | }); 190 | 191 | let handle2 = thread::spawn(|| async move { 192 | let event = Event::OrderCreated(OrderCreatedEvent { 193 | order_id: 2, 194 | customer_name: "John Doe".to_string(), 195 | items: vec!["Item 1".to_string(), "Item 2".to_string()], 196 | }); 197 | let result = materialized_view2.handle(&event).await; 198 | assert!(result.is_ok()); 199 | assert_eq!( 200 | result.unwrap(), 201 | ( 202 | OrderViewState { 203 | order_id: 2, 204 | customer_name: "John Doe".to_string(), 205 | items: vec!["Item 1".to_string(), "Item 2".to_string()], 206 | is_cancelled: false, 207 | }, 208 | ShipmentViewState { 209 | shipment_id: 0, 210 | order_id: 0, 211 | customer_name: "".to_string(), 212 | items: Vec::new(), 213 | } 214 | ) 215 | ); 216 | let event = Event::OrderUpdated(OrderUpdatedEvent { 217 | order_id: 2, 218 | updated_items: vec!["Item 3".to_string(), "Item 4".to_string()], 219 | }); 220 | let result = materialized_view2.handle(&event).await; 221 | assert!(result.is_ok()); 222 | assert_eq!( 223 | result.unwrap(), 224 | ( 225 | OrderViewState { 226 | order_id: 2, 227 | customer_name: "John Doe".to_string(), 228 | items: vec!["Item 3".to_string(), "Item 4".to_string()], 229 | is_cancelled: false, 230 | }, 231 | ShipmentViewState { 232 | shipment_id: 0, 233 | order_id: 0, 234 | customer_name: "".to_string(), 235 | items: Vec::new(), 236 | } 237 | ) 238 | ); 239 | let event = Event::OrderCancelled(OrderCancelledEvent { order_id: 2 }); 240 | let result = materialized_view2.handle(&event).await; 241 | assert!(result.is_ok()); 242 | assert_eq!( 243 | result.unwrap(), 244 | ( 245 | OrderViewState { 246 | order_id: 2, 247 | customer_name: "John Doe".to_string(), 248 | items: vec!["Item 3".to_string(), "Item 4".to_string()], 249 | is_cancelled: true, 250 | }, 251 | ShipmentViewState { 252 | shipment_id: 0, 253 | order_id: 0, 254 | customer_name: "".to_string(), 255 | items: Vec::new(), 256 | } 257 | ) 258 | ); 259 | }); 260 | 261 | handle1.join().unwrap().await; 262 | handle2.join().unwrap().await; 263 | } 264 | -------------------------------------------------------------------------------- /tests/materialized_view_test.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::sync::{Arc, Mutex}; 3 | use std::thread; 4 | 5 | use fmodel_rust::materialized_view::{MaterializedView, ViewStateRepository}; 6 | use fmodel_rust::view::View; 7 | use fmodel_rust::Identifier; 8 | 9 | use crate::api::{ 10 | OrderCancelledEvent, OrderCreatedEvent, OrderEvent, OrderUpdatedEvent, OrderViewState, 11 | }; 12 | use crate::application::MaterializedViewError; 13 | 14 | mod api; 15 | mod application; 16 | 17 | fn view<'a>() -> View<'a, OrderViewState, OrderEvent> { 18 | View { 19 | evolve: Box::new(|state, event| { 20 | let mut new_state = state.clone(); 21 | match event { 22 | OrderEvent::Created(evt) => { 23 | new_state.order_id = evt.order_id; 24 | new_state.customer_name = evt.customer_name.to_owned(); 25 | new_state.items = evt.items.to_owned(); 26 | } 27 | OrderEvent::Updated(evt) => { 28 | new_state.items = evt.updated_items.to_owned(); 29 | } 30 | OrderEvent::Cancelled(_) => { 31 | new_state.is_cancelled = true; 32 | } 33 | } 34 | new_state 35 | }), 36 | initial_state: Box::new(|| OrderViewState { 37 | order_id: 0, 38 | customer_name: "".to_string(), 39 | items: Vec::new(), 40 | is_cancelled: false, 41 | }), 42 | } 43 | } 44 | 45 | struct InMemoryViewOrderStateRepository { 46 | states: Mutex>, 47 | } 48 | 49 | impl InMemoryViewOrderStateRepository { 50 | fn new() -> Self { 51 | InMemoryViewOrderStateRepository { 52 | states: Mutex::new(HashMap::new()), 53 | } 54 | } 55 | } 56 | 57 | // Implementation of [ViewStateRepository] for [InMemoryViewOrderStateRepository] 58 | impl ViewStateRepository 59 | for InMemoryViewOrderStateRepository 60 | { 61 | async fn fetch_state( 62 | &self, 63 | event: &OrderEvent, 64 | ) -> Result, MaterializedViewError> { 65 | Ok(self 66 | .states 67 | .lock() 68 | .unwrap() 69 | .get(&event.identifier().parse::().unwrap()) 70 | .cloned()) 71 | } 72 | 73 | async fn save(&self, state: &OrderViewState) -> Result { 74 | self.states 75 | .lock() 76 | .unwrap() 77 | .insert(state.order_id, state.clone()); 78 | Ok(state.clone()) 79 | } 80 | } 81 | 82 | #[tokio::test] 83 | async fn test() { 84 | let repository = InMemoryViewOrderStateRepository::new(); 85 | let materialized_view = Arc::new(MaterializedView::new(repository, view())); 86 | let materialized_view1 = Arc::clone(&materialized_view); 87 | let materialized_view2 = Arc::clone(&materialized_view); 88 | 89 | // Let's spawn two threads to simulate two concurrent requests 90 | let handle1 = thread::spawn(|| async move { 91 | let event = OrderEvent::Created(OrderCreatedEvent { 92 | order_id: 1, 93 | customer_name: "John Doe".to_string(), 94 | items: vec!["Item 1".to_string(), "Item 2".to_string()], 95 | }); 96 | let result = materialized_view1.handle(&event).await; 97 | assert!(result.is_ok()); 98 | assert_eq!( 99 | result.unwrap(), 100 | OrderViewState { 101 | order_id: 1, 102 | customer_name: "John Doe".to_string(), 103 | items: vec!["Item 1".to_string(), "Item 2".to_string()], 104 | is_cancelled: false, 105 | } 106 | ); 107 | let event = OrderEvent::Updated(OrderUpdatedEvent { 108 | order_id: 1, 109 | updated_items: vec!["Item 3".to_string(), "Item 4".to_string()], 110 | }); 111 | let result = materialized_view1.handle(&event).await; 112 | assert!(result.is_ok()); 113 | assert_eq!( 114 | result.unwrap(), 115 | OrderViewState { 116 | order_id: 1, 117 | customer_name: "John Doe".to_string(), 118 | items: vec!["Item 3".to_string(), "Item 4".to_string()], 119 | is_cancelled: false, 120 | } 121 | ); 122 | let event = OrderEvent::Cancelled(OrderCancelledEvent { order_id: 1 }); 123 | let result = materialized_view1.handle(&event).await; 124 | assert!(result.is_ok()); 125 | assert_eq!( 126 | result.unwrap(), 127 | OrderViewState { 128 | order_id: 1, 129 | customer_name: "John Doe".to_string(), 130 | items: vec!["Item 3".to_string(), "Item 4".to_string()], 131 | is_cancelled: true, 132 | } 133 | ); 134 | }); 135 | 136 | let handle2 = thread::spawn(|| async move { 137 | let event = OrderEvent::Created(OrderCreatedEvent { 138 | order_id: 2, 139 | customer_name: "John Doe".to_string(), 140 | items: vec!["Item 1".to_string(), "Item 2".to_string()], 141 | }); 142 | let result = materialized_view2.handle(&event).await; 143 | assert!(result.is_ok()); 144 | assert_eq!( 145 | result.unwrap(), 146 | OrderViewState { 147 | order_id: 2, 148 | customer_name: "John Doe".to_string(), 149 | items: vec!["Item 1".to_string(), "Item 2".to_string()], 150 | is_cancelled: false, 151 | } 152 | ); 153 | let event = OrderEvent::Updated(OrderUpdatedEvent { 154 | order_id: 2, 155 | updated_items: vec!["Item 3".to_string(), "Item 4".to_string()], 156 | }); 157 | let result = materialized_view2.handle(&event).await; 158 | assert!(result.is_ok()); 159 | assert_eq!( 160 | result.unwrap(), 161 | OrderViewState { 162 | order_id: 2, 163 | customer_name: "John Doe".to_string(), 164 | items: vec!["Item 3".to_string(), "Item 4".to_string()], 165 | is_cancelled: false, 166 | } 167 | ); 168 | let event = OrderEvent::Cancelled(OrderCancelledEvent { order_id: 2 }); 169 | let result = materialized_view2.handle(&event).await; 170 | assert!(result.is_ok()); 171 | assert_eq!( 172 | result.unwrap(), 173 | OrderViewState { 174 | order_id: 2, 175 | customer_name: "John Doe".to_string(), 176 | items: vec!["Item 3".to_string(), "Item 4".to_string()], 177 | is_cancelled: true, 178 | } 179 | ); 180 | }); 181 | 182 | handle1.join().unwrap().await; 183 | handle2.join().unwrap().await; 184 | } 185 | -------------------------------------------------------------------------------- /tests/saga_manager_merged_test.rs: -------------------------------------------------------------------------------- 1 | use fmodel_rust::saga::Saga; 2 | use fmodel_rust::saga_manager::{ActionPublisher, SagaManager}; 3 | 4 | use crate::api::{ 5 | CreateShipmentCommand, OrderCommand, OrderCreatedEvent, ShipmentCommand, UpdateOrderCommand, 6 | }; 7 | use crate::application::{sum_to_command2, Command, Event, SagaManagerError}; 8 | 9 | mod api; 10 | mod application; 11 | 12 | fn order_saga<'a>() -> Saga<'a, Event, ShipmentCommand> { 13 | Saga { 14 | react: Box::new(|event| match event { 15 | Event::OrderCreated(evt) => { 16 | vec![ShipmentCommand::Create(CreateShipmentCommand { 17 | shipment_id: evt.order_id, 18 | order_id: evt.order_id, 19 | customer_name: evt.customer_name.to_owned(), 20 | items: evt.items.to_owned(), 21 | })] 22 | } 23 | Event::OrderUpdated(_) => { 24 | vec![] 25 | } 26 | Event::OrderCancelled(_) => { 27 | vec![] 28 | } 29 | Event::ShipmentCreated(_) => { 30 | vec![] 31 | } 32 | }), 33 | } 34 | } 35 | 36 | fn shipment_saga<'a>() -> Saga<'a, Event, OrderCommand> { 37 | Saga { 38 | react: Box::new(|event| match event { 39 | Event::ShipmentCreated(evt) => { 40 | vec![OrderCommand::Update(UpdateOrderCommand { 41 | order_id: evt.order_id, 42 | new_items: evt.items.to_owned(), 43 | })] 44 | } 45 | 46 | Event::OrderCreated(_) => { 47 | vec![] 48 | } 49 | Event::OrderUpdated(_) => { 50 | vec![] 51 | } 52 | Event::OrderCancelled(_) => { 53 | vec![] 54 | } 55 | }), 56 | } 57 | } 58 | /// Simple action publisher that just returns the action/command. 59 | /// It is used for testing. In real life, it would publish the action/command to some external system. or to an aggregate that is able to handel the action/command. 60 | struct SimpleActionPublisher; 61 | 62 | impl SimpleActionPublisher { 63 | fn new() -> Self { 64 | SimpleActionPublisher {} 65 | } 66 | } 67 | 68 | impl ActionPublisher for SimpleActionPublisher { 69 | async fn publish(&self, action: &[Command]) -> Result, SagaManagerError> { 70 | Ok(Vec::from(action)) 71 | } 72 | } 73 | 74 | #[tokio::test] 75 | async fn test() { 76 | let order_created_event = Event::OrderCreated(OrderCreatedEvent { 77 | order_id: 1, 78 | customer_name: "John Doe".to_string(), 79 | items: vec!["Item 1".to_string(), "Item 2".to_string()], 80 | }); 81 | 82 | let saga_manager = SagaManager::new( 83 | SimpleActionPublisher::new(), 84 | shipment_saga() 85 | .merge(order_saga()) 86 | .map_action(sum_to_command2), 87 | ); 88 | let result = saga_manager.handle(&order_created_event).await; 89 | assert!(result.is_ok()); 90 | assert_eq!( 91 | result.unwrap(), 92 | vec![Command::ShipmentCreate(CreateShipmentCommand { 93 | shipment_id: 1, 94 | order_id: 1, 95 | customer_name: "John Doe".to_string(), 96 | items: vec!["Item 1".to_string(), "Item 2".to_string()], 97 | })] 98 | ); 99 | } 100 | -------------------------------------------------------------------------------- /tests/saga_manager_test.rs: -------------------------------------------------------------------------------- 1 | use fmodel_rust::saga::Saga; 2 | use fmodel_rust::saga_manager::{ActionPublisher, SagaManager}; 3 | 4 | use crate::api::{CreateShipmentCommand, OrderCreatedEvent, OrderEvent, ShipmentCommand}; 5 | use crate::application::SagaManagerError; 6 | 7 | mod api; 8 | mod application; 9 | 10 | fn saga<'a>() -> Saga<'a, OrderEvent, ShipmentCommand> { 11 | Saga { 12 | react: Box::new(|event| match event { 13 | OrderEvent::Created(evt) => { 14 | vec![ShipmentCommand::Create(CreateShipmentCommand { 15 | shipment_id: evt.order_id, 16 | order_id: evt.order_id, 17 | customer_name: evt.customer_name.to_owned(), 18 | items: evt.items.to_owned(), 19 | })] 20 | } 21 | OrderEvent::Updated(_) => { 22 | vec![] 23 | } 24 | OrderEvent::Cancelled(_) => { 25 | vec![] 26 | } 27 | }), 28 | } 29 | } 30 | 31 | /// Simple action publisher that just returns the action/command. 32 | /// It is used for testing. In real life, it would publish the action/command to some external system. or to an aggregate that is able to handel the action/command. 33 | struct SimpleActionPublisher; 34 | 35 | impl SimpleActionPublisher { 36 | fn new() -> Self { 37 | SimpleActionPublisher {} 38 | } 39 | } 40 | 41 | impl ActionPublisher for SimpleActionPublisher { 42 | async fn publish( 43 | &self, 44 | action: &[ShipmentCommand], 45 | ) -> Result, SagaManagerError> { 46 | Ok(Vec::from(action)) 47 | } 48 | } 49 | 50 | #[tokio::test] 51 | async fn test() { 52 | let saga: Saga = saga(); 53 | let order_created_event = OrderEvent::Created(OrderCreatedEvent { 54 | order_id: 1, 55 | customer_name: "John Doe".to_string(), 56 | items: vec!["Item 1".to_string(), "Item 2".to_string()], 57 | }); 58 | 59 | let saga_manager = SagaManager::new(SimpleActionPublisher::new(), saga); 60 | let result = saga_manager.handle(&order_created_event).await; 61 | assert!(result.is_ok()); 62 | assert_eq!( 63 | result.unwrap(), 64 | vec![ShipmentCommand::Create(CreateShipmentCommand { 65 | shipment_id: 1, 66 | order_id: 1, 67 | customer_name: "John Doe".to_string(), 68 | items: vec!["Item 1".to_string(), "Item 2".to_string()], 69 | })] 70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /tests/saga_test.rs: -------------------------------------------------------------------------------- 1 | use fmodel_rust::saga::{ActionComputation, Saga}; 2 | 3 | use crate::api::{ 4 | CreateShipmentCommand, OrderCommand, OrderCreatedEvent, OrderEvent, ShipmentCommand, 5 | UpdateOrderCommand, 6 | }; 7 | use crate::application::{sum_to_command, Command, Event}; 8 | 9 | mod api; 10 | mod application; 11 | 12 | fn order_saga<'a>() -> Saga<'a, OrderEvent, ShipmentCommand> { 13 | Saga { 14 | react: Box::new(|event| match event { 15 | OrderEvent::Created(evt) => { 16 | vec![ShipmentCommand::Create(CreateShipmentCommand { 17 | shipment_id: evt.order_id, 18 | order_id: evt.order_id, 19 | customer_name: evt.customer_name.to_owned(), 20 | items: evt.items.to_owned(), 21 | })] 22 | } 23 | OrderEvent::Updated(_) => { 24 | vec![] 25 | } 26 | OrderEvent::Cancelled(_) => { 27 | vec![] 28 | } 29 | }), 30 | } 31 | } 32 | 33 | fn order_saga_2<'a>() -> Saga<'a, Event, ShipmentCommand> { 34 | Saga { 35 | react: Box::new(|event| match event { 36 | Event::OrderCreated(evt) => { 37 | vec![ShipmentCommand::Create(CreateShipmentCommand { 38 | shipment_id: evt.order_id, 39 | order_id: evt.order_id, 40 | customer_name: evt.customer_name.to_owned(), 41 | items: evt.items.to_owned(), 42 | })] 43 | } 44 | Event::OrderUpdated(_) => { 45 | vec![] 46 | } 47 | Event::OrderCancelled(_) => { 48 | vec![] 49 | } 50 | Event::ShipmentCreated(_) => { 51 | vec![] 52 | } 53 | }), 54 | } 55 | } 56 | 57 | fn shipment_saga_2<'a>() -> Saga<'a, Event, OrderCommand> { 58 | Saga { 59 | react: Box::new(|event| match event { 60 | Event::ShipmentCreated(evt) => { 61 | vec![OrderCommand::Update(UpdateOrderCommand { 62 | order_id: evt.order_id, 63 | new_items: evt.items.to_owned(), 64 | })] 65 | } 66 | 67 | Event::OrderCreated(_) => { 68 | vec![] 69 | } 70 | Event::OrderUpdated(_) => { 71 | vec![] 72 | } 73 | Event::OrderCancelled(_) => { 74 | vec![] 75 | } 76 | }), 77 | } 78 | } 79 | 80 | #[test] 81 | fn test() { 82 | let order_saga: Saga = order_saga(); 83 | let order_saga_2: Saga = crate::order_saga_2(); 84 | let shipment_saga_2: Saga = crate::shipment_saga_2(); 85 | let merged_saga = order_saga_2 86 | .merge(shipment_saga_2) 87 | .map_action(sum_to_command); 88 | 89 | let order_created_event = OrderEvent::Created(OrderCreatedEvent { 90 | order_id: 1, 91 | customer_name: "John Doe".to_string(), 92 | items: vec!["Item 1".to_string(), "Item 2".to_string()], 93 | }); 94 | let commands = order_saga.compute_new_actions(&order_created_event); 95 | assert_eq!( 96 | commands, 97 | [ShipmentCommand::Create(CreateShipmentCommand { 98 | shipment_id: 1, 99 | order_id: 1, 100 | customer_name: "John Doe".to_string(), 101 | items: vec!["Item 1".to_string(), "Item 2".to_string()], 102 | })] 103 | ); 104 | let order_created_event2 = Event::OrderCreated(OrderCreatedEvent { 105 | order_id: 1, 106 | customer_name: "John Doe".to_string(), 107 | items: vec!["Item 1".to_string(), "Item 2".to_string()], 108 | }); 109 | 110 | let merged_commands = merged_saga.compute_new_actions(&order_created_event2); 111 | assert_eq!( 112 | merged_commands, 113 | [Command::ShipmentCreate(CreateShipmentCommand { 114 | shipment_id: 1, 115 | order_id: 1, 116 | customer_name: "John Doe".to_string(), 117 | items: vec!["Item 1".to_string(), "Item 2".to_string()], 118 | })] 119 | ); 120 | } 121 | -------------------------------------------------------------------------------- /tests/view_test.rs: -------------------------------------------------------------------------------- 1 | use fmodel_rust::specification::ViewTestSpecification; 2 | use fmodel_rust::view::View; 3 | 4 | use crate::api::{OrderCreatedEvent, OrderViewState, ShipmentCreatedEvent, ShipmentViewState}; 5 | 6 | use crate::application::Event; 7 | 8 | mod api; 9 | mod application; 10 | 11 | fn order_view<'a>() -> View<'a, OrderViewState, Event> { 12 | View { 13 | evolve: Box::new(|state, event| { 14 | let mut new_state = state.clone(); 15 | match event { 16 | Event::OrderCreated(evt) => { 17 | new_state.order_id = evt.order_id; 18 | new_state.customer_name = evt.customer_name.to_owned(); 19 | new_state.items = evt.items.to_owned(); 20 | } 21 | Event::OrderUpdated(evt) => { 22 | new_state.items = evt.updated_items.to_owned(); 23 | } 24 | Event::OrderCancelled(_) => { 25 | new_state.is_cancelled = true; 26 | } 27 | Event::ShipmentCreated(_) => {} 28 | } 29 | new_state 30 | }), 31 | initial_state: Box::new(|| OrderViewState { 32 | order_id: 0, 33 | customer_name: "".to_string(), 34 | items: Vec::new(), 35 | is_cancelled: false, 36 | }), 37 | } 38 | } 39 | 40 | fn shipment_view<'a>() -> View<'a, ShipmentViewState, Event> { 41 | View { 42 | evolve: Box::new(|state, event| { 43 | let mut new_state = state.clone(); 44 | match event { 45 | Event::ShipmentCreated(evt) => { 46 | new_state.shipment_id = evt.shipment_id; 47 | new_state.order_id = evt.order_id; 48 | new_state.customer_name = evt.customer_name.to_owned(); 49 | new_state.items = evt.items.to_owned(); 50 | } 51 | Event::OrderCreated(_) => {} 52 | Event::OrderUpdated(_) => {} 53 | Event::OrderCancelled(_) => {} 54 | } 55 | new_state 56 | }), 57 | initial_state: Box::new(|| ShipmentViewState { 58 | shipment_id: 0, 59 | order_id: 0, 60 | customer_name: "".to_string(), 61 | items: Vec::new(), 62 | }), 63 | } 64 | } 65 | 66 | fn merged_view<'a>() -> View<'a, (OrderViewState, ShipmentViewState), Event> { 67 | order_view().merge(self::shipment_view()) 68 | } 69 | 70 | #[test] 71 | fn order_created_view_test() { 72 | let order_created_event = Event::OrderCreated(OrderCreatedEvent { 73 | order_id: 1, 74 | customer_name: "John Doe".to_string(), 75 | items: vec!["Item 1".to_string(), "Item 2".to_string()], 76 | }); 77 | 78 | ViewTestSpecification::default() 79 | .for_view(self::order_view()) 80 | .given(vec![order_created_event.clone()]) 81 | .then(OrderViewState { 82 | order_id: 1, 83 | customer_name: "John Doe".to_string(), 84 | items: vec!["Item 1".to_string(), "Item 2".to_string()], 85 | is_cancelled: false, 86 | }); 87 | 88 | ViewTestSpecification::default() 89 | .for_view(merged_view()) 90 | .given(vec![order_created_event]) 91 | .then(( 92 | OrderViewState { 93 | order_id: 1, 94 | customer_name: "John Doe".to_string(), 95 | items: vec!["Item 1".to_string(), "Item 2".to_string()], 96 | is_cancelled: false, 97 | }, 98 | ShipmentViewState { 99 | shipment_id: 0, 100 | order_id: 0, 101 | customer_name: "".to_string(), 102 | items: Vec::new(), 103 | }, 104 | )); 105 | } 106 | #[test] 107 | 108 | fn shipment_created_view_test() { 109 | let shipment_created_event = Event::ShipmentCreated(ShipmentCreatedEvent { 110 | shipment_id: 1, 111 | order_id: 1, 112 | customer_name: "John Doe".to_string(), 113 | items: vec!["Item 1".to_string(), "Item 2".to_string()], 114 | }); 115 | 116 | ViewTestSpecification::default() 117 | .for_view(merged_view()) 118 | .given(vec![shipment_created_event.clone()]) 119 | .then(( 120 | OrderViewState { 121 | order_id: 0, 122 | customer_name: "".to_string(), 123 | items: Vec::new(), 124 | is_cancelled: false, 125 | }, 126 | ShipmentViewState { 127 | shipment_id: 1, 128 | order_id: 1, 129 | customer_name: "John Doe".to_string(), 130 | items: vec!["Item 1".to_string(), "Item 2".to_string()], 131 | }, 132 | )); 133 | } 134 | --------------------------------------------------------------------------------