Skip to content

Instantly share code, notes, and snippets.

@AlexeyRaga
Last active February 26, 2023 03:17
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save AlexeyRaga/59facd454e3e4521396e3276106dca7f to your computer and use it in GitHub Desktop.
Save AlexeyRaga/59facd454e3e4521396e3276106dca7f to your computer and use it in GitHub Desktop.
ChatGPT: F# Aggregate

Show me an implementation of an Organisation Aggregate in F# that can handle "Register", "Suspend" and "ChangeAddress" commands


Here is a simple implementation of an organization aggregate in F# that can handle the "Register", "Suspend", and "ChangeAddress" commands:

type Address = {
    Street: string
    City: string
    State: string
    ZipCode: string
}

type OrganizationState =
    | Registered of Address
    | Suspended of Address

type OrganizationId = string

type Organization = {
    Id: OrganizationId
    State: OrganizationState
}

type OrganizationCommand =
    | Register of Address
    | Suspend
    | ChangeAddress of Address

type OrganizationEvent =
    | Registered of OrganizationId * Address
    | Suspended of OrganizationId * Address
    | AddressChanged of OrganizationId * Address

let handleCommand (org: Organization) (cmd: OrganizationCommand) =
    match org.State, cmd with
    | Registered _, Register _ ->
        // Cannot register an already registered organization
        []
    | Suspended _, Register address ->
        [ Registered (org.Id, address) ]
    | Registered address, Suspend ->
        [ Suspended (org.Id, address) ]
    | Suspended _, Suspend ->
        // Cannot suspend an already suspended organization
        []
    | Registered address, ChangeAddress newAddress ->
        [ AddressChanged (org.Id, newAddress) ]
    | Suspended address, ChangeAddress newAddress ->
        [ AddressChanged (org.Id, newAddress) ]

let applyEvent (org: Organization) (evt: OrganizationEvent) =
    match evt with
    | Registered (_, address) ->
        { org with State = Registered address }
    | Suspended (_, address) ->
        { org with State = Suspended address }
    | AddressChanged (_, address) ->
        { org with State = Registered address }

In this implementation, we define an OrganizationState discriminated union with two cases: Registered and Suspended. We also define an OrganizationId and Organization type to represent the unique identifier and state of an organization, respectively. The OrganizationCommand type represents the possible commands that can be executed on an organization, while the OrganizationEvent type represents the events that result from executing these commands.

The handleCommand function takes an Organization and a OrganizationCommand as input and returns a list of OrganizationEvents, which represent the changes that result from executing the command. The applyEvent function takes an Organization and an OrganizationEvent as input and returns a new Organization with its state updated based on the event.


Now please write GWT unit tests for this aggregate


Here is an example of how you could write unit tests for the organization aggregate using the F# unit testing framework, FsUnit:

#r "FsUnit.dll"
open FsUnit

[<Test>]
let ``registering a new organization should produce a Registered event``() =
    let address = { Street = "1 Main St"; City = "San Francisco"; State = "CA"; ZipCode = "94111" }
    let org = { Id = "123"; State = Suspended address }
    let events = handleCommand org (Register address)
    events |> should equal [ Registered ("123", address) ]

[<Test>]
let ``suspending an already registered organization should produce a Suspended event``() =
    let address = { Street = "1 Main St"; City = "San Francisco"; State = "CA"; ZipCode = "94111" }
    let org = { Id = "123"; State = Registered address }
    let events = handleCommand org Suspend
    events |> should equal [ Suspended ("123", address) ]

[<Test>]
let ``changing the address of a registered organization should produce an AddressChanged event``() =
    let oldAddress = { Street = "1 Main St"; City = "San Francisco"; State = "CA"; ZipCode = "94111" }
    let newAddress = { Street = "2 Main St"; City = "San Francisco"; State = "CA"; ZipCode = "94111" }
    let org = { Id = "123"; State = Registered oldAddress }
    let events = handleCommand org (ChangeAddress newAddress)
    events |> should equal [ AddressChanged ("123", newAddress) ]

[<Test>]
let ``changing the address of a suspended organization should produce an AddressChanged event``() =
    let oldAddress = { Street = "1 Main St"; City = "San Francisco"; State = "CA"; ZipCode = "94111" }
    let newAddress = { Street = "2 Main St"; City = "San Francisco"; State = "CA"; ZipCode = "94111" }
    let org = { Id = "123"; State = Suspended oldAddress }
    let events = handleCommand org (ChangeAddress newAddress)
    events |> should equal [ AddressChanged ("123", newAddress) ]

[<Test>]
let ``applying a Registered event should change the state of the organization to Registered``() =
    let address = { Street = "1 Main St"; City = "San Francisco"; State = "CA"; ZipCode = "94111" }
    let org = { Id = "123"; State = Suspended address }
    let evt = Registered ("123", address)
    let updated = applyEvent org evt
    updated.State |> should equal (Registered address)

[<Test>]
let ``applying a Suspended event should change the state of the organization to Suspended``() =
    let address = { Street = "1 Main St"; City = "San Francisco"; State = "CA"; ZipCode = "94111" }
    let org = { Id = "123"; State = Registered address }
    let evt = Suspended ("123", address)
    let updated = applyEvent org evt
    updated.State |> should equal (Suspended address)

Can you make them property-based tests? For property-based tests auto-generate input data


Here is an updated version of the previous code, where the input data is automatically generated using FsCheck:

#r "FsCheck.dll"
open FsCheck
open FsCheck.Xunit

[<Property(QuietOnSuccess=true)>]
let ``registering a new organization should produce a Registered event``() =
    let address = Arb.generate<Address>()
    let org = { Id = "123"; State = Suspended address }
    let events = handleCommand org (Register address)
    events = [ Registered ("123", address) ]
    events |> Prop.forAll

[<Property(QuietOnSuccess=true)>]
let ``suspending an already registered organization should produce a Suspended event``() =
    let address = Arb.generate<Address>()
    let org = { Id = "123"; State = Registered address }
    let events = handleCommand org Suspend
    events = [ Suspended ("123", address) ]
    events |> Prop.forAll

[<Property(QuietOnSuccess=true)>]
let ``changing the address of a registered organization should produce an AddressChanged event``() =
    let oldAddress = Arb.generate<Address>()
    let newAddress = Arb.generate<Address>()
    let org = { Id = "123"; State = Registered oldAddress }
    let events = handleCommand org (ChangeAddress newAddress)
    events = [ AddressChanged ("123", newAddress) ]
    events |> Prop.forAll

[<Property(QuietOnSuccess=true)>]
let ``changing the address of a suspended organization should produce an AddressChanged event``() =
    let oldAddress = Arb.generate<Address>()
    let newAddress = Arb.generate<Address>()
    let org = { Id = "123"; State = Suspended oldAddress }
    let events = handleCommand org (ChangeAddress newAddress)
    events = [ AddressChanged ("123", newAddress) ]
    events |> Prop.forAll

[<Property(QuietOnSuccess=true)>]
let ``applying a Registered event should change the state of the organization to Registered``() =
    let address = Arb.generate<Address>()
    let org = { Id = "123"; State = Suspended address }
    let evt = Registered ("123", address)
    let updated = applyEvent org evt
    updated.State = Registered address
    updated.State |> Prop.forAll

[<Property(QuietOnSuccess=true)>]
let ``applying a Suspended event should change the state of the organization to Suspended``() =
    let address = Arb.generate<Address>()
    let org = { Id = "123"; State = Registered address }
    let evt = Suspended ("123", address)
    let updated = applyEvent org evt
    updated.State = Suspended address
    updated.State |> Prop.forAll

Please use Hedgehog property-based testing framework


Here's an example of how you could write property-based tests for the organization aggregate using the Hedgehog property-based testing framework in F#:

#r "Hedgehog.dll"
open Hedgehog
open Hedgehog.Gen
open Hedgehog.Range

let registeredProp = property {
    let! address = genAddress
    let org = { Id = "123"; State = Suspended address }
    let events = handleCommand org (Register address)
    return events = [ Registered ("123", address) ]
}

let suspendedProp = property {
    let! address = genAddress
    let org = { Id = "123"; State = Registered address }
    let events = handleCommand org Suspend
    return events = [ Suspended ("123", address) ]
}

let addressChangedProp = property {
    let! oldAddress = genAddress
    let! newAddress = genAddress
    let org = { Id = "123"; State = Registered oldAddress }
    let events = handleCommand org (ChangeAddress newAddress)
    return events = [ AddressChanged ("123", newAddress) ]
}

let registeredEventProp = property {
    let! address = genAddress
    let org = { Id = "123"; State = Suspended address }
    let evt = Registered ("123", address)
    let updated = applyEvent org evt
    return updated.State = Registered address
}

let suspendedEventProp = property {
    let! address = genAddress
    let org = { Id = "123"; State = Registered address }
    let evt = Suspended ("123", address)
    let updated = applyEvent org evt
    return updated.State = Suspended address
}

let addressChangedEventProp = property {
    let! oldAddress = genAddress
    let! newAddress = genAddress
    let org = { Id = "123"; State = Registered oldAddress }
    let evt = AddressChanged ("123", newAddress)
    let updated = applyEvent org evt
    return updated.State = Registered newAddress
}

[<Test>]
let ``registering a new organization should produce a Registered event``() =
    Check.Quick registeredProp

[<Test>]
let ``suspending an already registered organization should produce a Suspended event``() =
    Check.Quick suspendedProp

[<Test>]
let ``changing the address of a registered organization should produce an AddressChanged event``() =
    Check.Quick addressChangedProp

[<Test>]
let ``applying a Registered event should change the state of the organization to Registered``() =
    Check.Quick registeredEventProp

[<Test>]
let ``applying a Suspended event should change the state of the organization to Suspended``() =
    Check.Quick suspendedEventProp

[<Test>]
let ``applying an AddressChanged event should change the address of the organization``() =
    Check.Quick addressChangedEventProp
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment