ArtShowTools: Part 3

Adding Validation

In the previous post, I used event sourcing to handle commands and recreate the current state of the Artwork entity. In this post, I'll be adding validation to the value objects used to implement the command and event objects.

The ArtworkId Value Object

Let's start with the ArtworkId value object. This value object is currently implemented as a single case discriminated union.

type ArtworkId = ArtworkId of Guid

While this implementation avoids primitive obsession by making the ID of an artwork entity into a first-class type, there is no validation. For the ArtShowTools domain model, a valid ArtworkId is required to be non-empty. The first step is to expand the ArtworkError type to include an error representing an invalid ID.

type ArtworkError =
    | ArtworkAlreadyExists
    | ArtworkDoesNotExist
    | WrongArtwork
    | InvalidArtworkTitle
    | InvalidArtworkYear
    | InvalidArtworkId

Next, I make the ArtworkId constructor private and add some methods to construct and deconstruct ArtworkId instances.

type ArtworkId = private ArtworkId of Guid

module ArtworkId =
    let fromGuid (guid: Guid) : Result<ArtworkId, ArtworkError> =
        if (guid = Guid.Empty) then
            Error InvalidArtworkId
        else
            Ok(guid |> ArtworkId)

    let toGuid (id: ArtworkId) : Guid =
        let (ArtworkId guid) = id
        guid

The fromGuid method which constructs ArtworkId instance takes a Guid value and returns a Result containing the valid ArtworkId or InvalidArtworkId error.

To verify the behavior, I add some tests.

let guid = Guid("AD5BB7A9-4AC8-409E-9736-55E0CBB9EED0")

[<Test>]
let ``ArtworkId.fromGuid fails on empty Guid`` () =
    let result = Guid.Empty |> ArtworkId.fromGuid

    match result with
    | Ok _ -> Assert.Fail("method should fail")
    | Error error ->
        match error with
        | InvalidArtworkId -> ()
        | _ -> Assert.Fail("unexpected error type")

[<Test>]
let ``ArtworkId.fromGuid succeeds`` () =
    let result = guid |> ArtworkId.fromGuid

    match result with
    | Ok artworkId -> Assert.That(artworkId |> ArtworkId.toGuid, Is.EqualTo(guid))
    | Error _ -> Assert.Fail("method should succeed")

Refactoring the Existing Tests

Changing the scope of the ArtworkId constructor to private will break the existing domain model tests. For example, this declaration no longer compiles.

let id = Guid("350EA2A6-6316-44DE-9316-2D545E5CA2C5") |> ArtworkId

Refactoring it to use the new fromGuid method requires adding pattern matching so the test value is initialized properly or fails the test if the value is not valid.

let id =
    match Guid("350EA2A6-6316-44DE-9316-2D545E5CA2C5") |> ArtworkId.fromGuid with
    | Ok id -> id
    | Error _ -> failwith "invalid Id'

let wrongId =
    match Guid("D96CD8A5-E757-4798-8753-769E710AFA5A") |> ArtworkId.fromGuid with
    | OK id -> id
    | Error _ -> failwith "invalid id"

Unfortunately, this approach results in a lot of duplicated code just to initialize test values. Since I'll be applying this refactoring to all the value objects, I decided to implement a simple generic helper method to abstract test value creation.

let failOnError<'T, 'E> (message: string) (result: Result<'T, 'E>) : 'T =
    match result with
    | Ok v -> v
    | Error _ -> failwith message

This helper method reduces the complexity of declaration test values significantly.

let id =
    Guid("350EA2A6-6316-44DE-9316-2D545E5CA2C5")
    |> ArtworkId.fromGuid
    |> Testing.failOnError "invalid id"

let wrongId =
    Guid("D96CD8A5-E757-4798-8753-769E710AFA5A")
    |> ArtworkId.fromGuid
    |> Testing.failOnError "invalid id"

For completeness, I also added tests for the new helper method.

[<Test>]
let ``Testing.failOnError handles Error correctly`` () =
    let ex = Assert.Throws (fun x -> Error "error" |> failOnError "fail" |> ignore)
    Assert.That(ex.Message, Is.EqualTo("fail"))

[<Test>]
let ``Testing.failOnError handles Ok correctly`` () =
    let value = Ok "ok" |> failOnError "fail"
    Assert.That(value, Is.EqualTo("ok"))

I verify all the tests now pass and push the code changes to the prototype repository.

Next Steps

With a solid approach in place for validating value objects, I'll update the rest of the code in the prototype repository: ArtworkTitle and ArtworkYear. Once that validation is in place, I plan to add some persistence to the application by using EventStoreDb (https://www.eventstore.com/) to store my event streams.