F#: Domain Modeling a Workflow (vol. 5)

Intro

I’m practicing Domain Modeling Made Functional and have implemented the Profile domain context of Nikeza.Mobile.

Here’s the birds-eye view of the workflows I constructed:

Untitled2

Workflow

The workflows that I wrote reflect operations that rely on I/O on the edges and domain logic at the core. All workflows take a command and emit domain events as defined upfront via function type declarations. These function types are RegistrationWorkflow, SessionWorkflow, and EditWorkflow.

module Workflows

open Commands
open Events
open Logic

type private RegistrationWorkflow = RegistrationCommand -> RegistrationEvent list
type private SessionWorkflow =      SessionCommand      -> SessionEvent      list
type private EditWorkflow =         EditCommand         -> ProfileEvent      list

let handleRegistration : RegistrationWorkflow =
    fun command -> command |> function
    | RegistrationCommand.Validate form ->
                                   form |> Registration.validate
                                        |> ResultOf.Registration.Validate
                                        |> Registration.handle
    | RegistrationCommand.Submit   form ->
                                   form |> IO.trySubmit
                                        |> ResultOf.Registration.Submit
                                        |> Registration.handle
let handleSession : SessionWorkflow =
    fun command -> command |> function
    | SessionCommand.Login credentials ->
                           credentials |> IO.tryLogin
                                       |> ResultOf.Login
                                       |> Session.handle

    | SessionCommand.Logout -> IO.tryLogout()
                                 |> ResultOf.Logout
                                 |> Session.handle
let handleEdit : EditWorkflow =
    fun command -> command |> function
    | EditCommand.Validate profile ->
                           profile |> Edit.validate
                                   |> ResultOf.Editor.Validate
                                   |> Edit.handle
    | EditCommand.Save     profile ->
                           profile |> IO.trySave
                                   |> ResultOf.Editor.Save
                                   |> Edit.handle

Commands

Commands are partitioned via responsibilities. Hence, I have command types for supporting registration, user-session, and editing.

The result of command execution is reflected by the ResultOf module that’s defined under the command types. I used similar naming conventions to enhance readability specifically for the Workflows module.

module Commands

open Nikeza.DataTransfer
open Registration

type RegistrationCommand =
    | Validate of Registration.UnvalidatedForm
    | Submit   of Registration.ValidatedForm

type SessionCommand =
    | Login  of Credentials
    | Logout 

type EditCommand =
    | Validate of EditedProfile
    | Save     of ValidatedProfile

    module ResultOf =
        type Editor =
            | Validate of Result<ValidatedProfile, EditedProfile>
            | Save     of Result<Profile, ValidatedProfile>

        type Session =
            | Login  of Result<Provider, Credentials>
            | Logout of Result<unit, unit>

        type Registration =
            | Submit   of Result<Nikeza.DataTransfer.Profile, Registration.ValidatedForm>
            | Validate of Result<ValidatedForm, UnvalidatedForm>

Events

Domain events are emitted from a workflow. These events are organized under different domain responsibilities (i.e. Registration, Session, Edit).

module Events

open Nikeza.Common
open Nikeza.DataTransfer

type RegistrationEvent =
    | FormValidated         of Registration.ValidatedForm
    | FormNotValidated      of Registration.UnvalidatedForm
    | RegistrationSucceeded of Profile
    | RegistrationFailed    of Registration.ValidatedForm
    | LoginRequested        of ProfileId

type SessionEvent =
    | LoggedIn    of Provider
    | LoginFailed of Credentials
    | LoggedOut
    | LogoutFailed

type ProfileEvent =
    | ProfileValidated    of ValidatedProfile
    | ProfileNotValidated of EditedProfile
    | ProfileRequested    of ProfileId
    | ProfileSaved        of Nikeza.DataTransfer.Profile
    | ProfileSaveFailed   of ValidatedProfile
    | Subscribed          of ProviderId
    | Unsubscribed        of ProviderId

I/O

The IO module is responsible for executing impure functions that have a dependency on the outside world. Thus, I use a Result type to reflect the status of the I/O operations.

module internal IO

open Registration
open Nikeza.DataTransfer

type private TrySubmit = ValidatedForm    -> Result<Profile, ValidatedForm>
type private TryLogin =  Credentials      -> Result<Provider, Credentials>
type private TryLogout = unit             -> Result<unit, unit>
type private TrySave =   ValidatedProfile -> Result<Profile, ValidatedProfile>

let trySubmit : TrySubmit =
    fun form -> Error form

let tryLogout : TryLogout =
    fun () -> Error ()

let tryLogin : TryLogin =
    fun credentials -> Error credentials

let trySave : TrySave =
    fun profile -> Error profile

Logic.editor.fs

Here’s the domain logic for editing a user’s profile:

module internal Logic.Edit

open Nikeza.DataTransfer
open Events
open Commands

type private Validate = EditedProfile   -> Result<ValidatedProfile, EditedProfile>
type private Handle =   ResultOf.Editor -> ProfileEvent list

let validate : Validate =
    fun edit ->
        let validEmail =     not <| System.String.IsNullOrEmpty(edit.Profile.Email)
        let validFirstName = not <| System.String.IsNullOrEmpty(edit.Profile.FirstName)
        let validLastName =  not <| System.String.IsNullOrEmpty(edit.Profile.LastName)         if validEmail && validFirstName && validLastName            then Ok    { Profile= edit.Profile }            else Error { Profile= edit.Profile } let handle : Handle =     fun response ->
        response |> function
                    | ResultOf.Editor.Validate result ->
                                               result |> function
                                                         | Ok    profile -> [ProfileValidated    profile]
                                                         | Error profile -> [ProfileNotValidated profile]
                    | ResultOf.Editor.Save     result ->
                                               result |> function
                                                         | Ok    profile -> [ProfileSaved      profile]
                                                         | Error profile -> [ProfileSaveFailed profile]

Logic.session.fs

Here’s the domain logic for managing a user’s session:

module internal Logic.Session

open Commands
open Events

type private HandleLogin = ResultOf.Session -> SessionEvent list

let handle : HandleLogin =
    fun result ->
        result |> function
                  | ResultOf.Session.Login result ->
                                           result |> function
                                                     | Ok    info -> [LoggedIn    info]
                                                     | Error info -> [LoginFailed info]
                  | ResultOf.Session.Logout result ->
                                            result |> function
                                                      | Ok    _ -> [LoggedOut]
                                                      | Error _ -> [LogoutFailed]

Logic.registration.fs

Here’s the domain logic for managing a user’s registration:

module internal Logic.Registration

open Nikeza.Common
open Events
open Registration
open Commands

type private Registration = ResultOf.Registration -> RegistrationEvent list

let handle : Registration =
    fun resultOf -> resultOf |> function
        | ResultOf.Registration.Submit   result ->
                                         result |> function
                                                   | Ok    profile -> [RegistrationSucceeded profile]
                                                   | Error form    -> [RegistrationFailed    form]
        | ResultOf.Registration.Validate result ->
                                         result |> function
                                                   | Ok    form -> [FormValidated    form]
                                                   | Error form -> [FormNotValidated form]

let validate (unvalidatedForm:UnvalidatedForm) : Result<ValidatedForm, UnvalidatedForm> =

    let isValidEmail email = false

    let form = unvalidatedForm.Form

    if   not (form.Email |> isValidEmail) then
          Error unvalidatedForm

    elif form.Password <> form.Confirm then
          Error unvalidatedForm

    else  Ok { Form= form }

let isValid (credentials:LogInRequest) =
    let validEmail =    not <| System.String.IsNullOrEmpty(credentials.Email)
    let validPassword = not <| System.String.IsNullOrEmpty(credentials.Password)

    validEmail && validPassword

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: