Last active
February 24, 2023 09:25
-
-
Save absolutejam/4752248d1cb645ad1301906095d87920 to your computer and use it in GitHub Desktop.
ZPure-inspired domain decisions
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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