Rust's type system allows implementing state machine in a straightforward way. In our application we might use it to define a sale's state in a specific point in time. We can define 4 different states for a sale:
Draft: An user can edit a sale, this can be used as some form of budget or presale, it does not affect inventory or accounting, you can only approve a draft sale.
Approved: The sale can't be edited, now it's an invoice that should be delivered to the client. You can cancel, pay or partially pay an approved sale.
Pay: An user payed the sale, now you can generate a collection receipt for the sale. You can only cancel a payed sale.
Partially Pay: The user received a part of the total payment, this could be used if you want to sell a product by parts using some form of credit, then you generate a collection receipt. You can cancel and pay a partially payed sale.
Cancel: This is an annulled invoice, if you commit a mistake with a sale, you need to generate a credit/debit note afterwards. This is the final state, once you cancel a sale, you can't change its state.
Let's see it in code. Take a look at src/models/sale_state.rs
:
use crate::models::sale::Sale; #[derive(DbEnum, Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(juniper::GraphQLEnum)] pub enum SaleState { Draft, Approved, PartiallyPayed, Payed, Cancelled } #[derive(Debug)] pub enum Event { Approve, Cancel, PartiallyPay, Pay, } impl SaleState { pub fn next(self, event: Event) -> Result<SaleState, String> { match (self, event) { (SaleState::Draft, Event::Approve) => Ok(SaleState::Approved), (SaleState::Approved, Event::Pay) => Ok(SaleState::Payed), (SaleState::Approved, Event::PartiallyPay) => Ok(SaleState::PartiallyPayed), (SaleState::Approved, Event::Cancel) => Ok(SaleState::Cancelled), (SaleState::Payed, Event::Cancel) => Ok(SaleState::Cancelled), (SaleState::PartiallyPayed, Event::Cancel) => Ok(SaleState::Cancelled), (SaleState::PartiallyPayed, Event::Pay) => Ok(SaleState::Payed), (sale_state, sale_event) => Err(format!("You can't {:#?} from {:#?} state", sale_event, sale_state)) } } }
The enum SaleState
is used as a database enum, let's take a look at the migration migrations/2019-09-25-114234_add_state_to_sales/up.sql
:
CREATE TYPE sale_state AS ENUM ('draft', 'approved', 'partially_payed', 'payed', 'cancelled'); ALTER TABLE sales ADD COLUMN state sale_state; UPDATE sales SET state = 'approved'; ALTER TABLE sales ALTER COLUMN state SET NOT NULL;
Thanks to diesel-derive-enum
crate we can map a Rust enum to db enum.
In order to make sure we are respecting the rules we already defined, we might add a function in src/models/sale.rs
:
fn set_state(context: &Context, sale_id: i32, event: Event) -> FieldResult<bool> { use crate::schema::sales::dsl; use diesel::ExpressionMethods; use diesel::QueryDsl; use diesel::RunQueryDsl; let conn: &PgConnection = &context.conn; let sale_query_builder = dsl::sales .filter(dsl::user_id.eq(context.user_id)) .find(sale_id); let sale = sale_query_builder.first::<Sale>(conn)?; let sale_state = sale.state.next(event)?; diesel::update(sale_query_builder) .set(dsl::state.eq(sale_state)) .get_result::<Sale>(conn)?; Ok(true) }
We can add a filter to updateSale
function, to make sure we only edit draft sales:
let sale = diesel::update( dsl::sales .filter( dsl::user_id .eq(context.user_id) .and(dsl::state.eq(SaleState::Draft)), ) .find(sale_id), ) .set(¶m_sale) .get_result::<Sale>(conn)?;
Then we add the approve, pay, partially pay and cancel functions:
fn approveSale(context: &Context, sale_id: i32) -> FieldResult<bool> { Sale::set_state(context, sale_id, Event::Approve) } fn cancelSale(context: &Context, sale_id: i32) -> FieldResult<bool> { //TODO: perform credit note or debit note Sale::set_state(context, sale_id, Event::Cancel) } fn paySale(context: &Context, sale_id: i32) -> FieldResult<bool> { //TODO: perform collection Sale::set_state(context, sale_id, Event::Pay) } fn partiallyPaySale(context: &Context, sale_id: i32) -> FieldResult<bool> { //TODO: perform collection Sale::set_state(context, sale_id, Event::PartiallyPay) }
Full source code here
Top comments (1)
Thank you