Skip to content

Instantly share code, notes, and snippets.

@absolutejam
Last active February 24, 2023 09:25
Show Gist options
  • Save absolutejam/4752248d1cb645ad1301906095d87920 to your computer and use it in GitHub Desktop.
Save absolutejam/4752248d1cb645ad1301906095d87920 to your computer and use it in GitHub Desktop.
ZPure-inspired domain decisions
//
// Inspired by the following talk: https://www.youtube.com/watch?v=TVYhFpqlgZ4
// And example repo: https://github.com/devsisters/ck-domain-logic-example
//
// Domain models
type AccountId = AccountId of string
type AccountNmae = AccountName of string
type AccountState = Opened | Closed
type Account = {
Id: AccountId
Name: AccountName
State: AccountState
}
module Account =
let init = {
Id = AccountId ""
Name = AccountName ""
State = Closed
}
type AccountEnv = {
IsAcceptingNewAccounts: bool
NumberOfCustomers: int
}
type AccountEvent =
| AccountOpened of id: AccountId * name: AccountName
| AccountClosed of reason: string
type AccountError =
| NotAceptingNewAccounts
| AccountAlreadyExists
| AccountNameTooLong of accountName: AccountName
// Domain decisions
module AccountDeciders =
let accountDecision<'t> = DecisionBuilder<AccountEvent, Account, AccountEnv, AccountError, 't>()
let assertAccountDoesNotExist = accountDecision {
let! account = get
do! assert (
fun () -> account <> Account.initialState,
AccountError.AccountAlreadyExists
)
}
let assertAcceptingNewAccounts = accountDecision {
let! acceptingNewAccounts = enquire (fun env -> env.IsAcceptingNewAccounts)
do! assertTrue (acceptingNewAccounts, AccountError.NotAcceptingNewAccounts)
}
let validateAccountName = accountDecision {
let! accountName = lookUp (fun account -> account.Name)
do! assert (
fun () -> accountName.Length < 10,
AccountError.AccountNameTooLong (accountName)
)
}
let openAccount (accountId: string, accountName: string) = accountDecision {
do! assertAccountDoesNotExist
do! assertAcceptingNewAccounts
let! validAccountId = validateAccountId
let! validAccountName = validateAccountName
do! event (AccountOpened (validAccountId, validAccountName))
// The CE's internal AccountState has been updated by evolving it with the above event
let! numberOfCustomers = enquire (fun env -> env.NumberOfCustomers)
if customerNumber + 1 = 1_000 then
do! event (AccountIsLuckyOneThousandthCustomer ())
}
// This would be built from prior (effectful) information, such as talking to DB
let env = {
IsAcceptingNewAccounts = true
NumberOfCustomers = 999
}
// TODO:
let evolve (accountState: AccountState) (event: AccountEvent) = accountState
// This would be built by folding previous events with the `evolve` function
let state = Account.init
let decision = AccountDecisions.openAccount (AccountId "123")
let result: Result<Event list, AccountError> =
decision
|> Decision.withEnv env
|> Decision.withState state
|> Decision.withEvolution evolve // this is so that the state can be evolved inside the decision too
|> Decision.run
// Ok [AccountOpened (...); AccountIsLuckyOneThousandthCustomer]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment