A tiny F# library with huge potential to simplify your domain design, as you can see from the examples below:
Without this package 👎 | Using this package 👍 |
---|---|
|
type Tweet = private Tweet of Text with➡ See the live demo |
You may have noticed that the examples on the left have an additional not null or empty validation case. On the right this validation is implicit in the statement that a Tweet
is a Tweet of Text
. Since Validation boxes can have inner boxes, the only rules that need to be explicitly declared are the rules specific to the type being defined!
F# is a multi-paradigm language, so there's nothing preventing us from harnessing (hijacking?) OP concepts for their expressiveness without any of the baggage. For instance here we use interface
as an elegant way to both:
- Identify a type as a Validation box
- Enforce the definition of validation rules
There's no other mentions of interfaces in the code that creates or uses Validation boxes, only when defining new types.
First you declare your error types, then you declare your actual domain types (i.e. Tweet
), and finally you use them with the provided Box.value
and Box.validate
functions. These 3 simple steps are enough to ensure at compilation time that your entire domain is always valid!
Older version of the live demo for future DDD paleontologists
Before declaring types like the one above, you do need define your error type. This can be a brand new validation-specific discriminated union or part of an existing one.
// These are just an example, create whatever errors
// you need to return from your own validation rules
type TextError =
| ContainsControlCharacters
| ContainsTabs
| IsTooLong of int
| IsMissingOrBlank
// ...
While not strictly necessary, the next single line of code greatly improves the readability of your type declarations by abbreviating the IBox<_,_>
interface for a specific primitive type.
// all string-based types can now interface TextBox instead of IBox<string, TextError>
type TextBox = inherit IBox<string, TextError>
Type declaration is reduced to the absolute minimum. A type is given a name, a private constructor, and the interface above that essentially makes it a Validation box and ensures that you define the validation rule.
The validation rule is a function of the primitive type (string
here) that returns a list of one or more errors depending on the stated conditions.
/// Single or multi-line non-null non-blank text without any additional validation
type FreeText = private FreeText of string with
interface TextBox with
member _.Validate =
// validation ruleᵴ (only one)
fun s ->
[if s |> String.IsNullOrWhiteSpace then IsMissingOrBlank]
The type declaration above can be simplified further using the provided =>
and ==>
operators that here combine a predicate of string
with the appropriate error.
/// Alternative type declaration using the ==> operator
type FreeText = private FreeText of string with
interface TextBox with
member _.Validate =
// same validation rule using validation operators
String.IsNullOrWhiteSpace ==> IsMissingOrBlank
To use validation operators make sure to open FSharp.Domain.Validation.Operators
in the file(s) where you declare your Validation types. See Text.fs for more examples of validation operators.
Using Validation boxes is easy, let's say you have a box called email
, you can simply access its value using the following:
// get the primitive value from the box
Box.value email // → string
There's also an experimental operator %
that essentially does the same thing. Note that this operator is opened automatically along with the namespace FSharp.Domain.Validation
. To avoid operator pollution this is advertised as experimental until the final operator characters are decided.
// experimental — same as Box.value
%email // → string
Creating a box is just as simple:
// create a box, canonicalizing (i.e. trimming) the input if it's a string
Box.validate s // → Ok 'box | Error e
Box.validate
canonicalization consists of trimming both whitespace and control characters, as well as removing occurrences of the null character. While this should be the preferred way of creating boxes, it's possible to skip canonicalization by using Box.verbatim
instead.
When type inference isn't possible, specify the box type using the generic parameter:
// create a box when its type can't be inferred
Box.validate<Tweet> s // → Ok Tweet | Error e
⚠ Do not force type inference using type annotations as it's unnecessarily verbose:
// incorrect example, do *not* copy/paste
let result : Result<Email, TextError list> = // :(
Box.validate "incorrect@dont.do"
// correct alternative when type inference isn't available
let result =
Box.validate<Email> "dev@fsharp.lang" // :)
In both cases result
is of type Result<Email, TextError list>
.
The Box.validate
method returns a Result
, which may not always be necessary, for instance when de-serializing values that are guaranteed to be valid, you can just use:
// throws an exception if not valid
Unchecked.boxof "this better be valid" // → 'box (inferred)
// same as above, when type inference is not available
Unchecked.boxof<Text> "this better be valid 2" // → Text
There's a System.Text.Json.Serialization.JsonConverter
included, if you add it to your serialization options all boxes are serialized to (and de-serialized from) their primitive type. It is good practice to keep your serialized content independent from implementation considerations such as Validation boxes.
Strings are the perfect example as it's usually the first type for which developers stitch together validation logic, but this library works with anything, you can create a PositiveInt
that's guaranteed to be greater than zero, or a FutureDate
that's guaranteed to not be in the past. Lists, vectors, any type of object really, if you can write a predicate against it, you can validate it. It's 100% generic so the sky is the limit.
I've created a checklist to help you decide whether this library is a good match for your project:
- My project contains domain objects/records
If your project satisfies all of the above this library is for you!
It dramatically reduces the amount of code necessary to make illegal states unrepresentable while being tiny and built only with FSharp.Core
. It uses F# concepts in the way they're meant to be used, so if one day you decide to no longer use it, you can simply get rid of it and still keep all the single-case unions that you've defined. All you'll need to do is create your own implementation of Box.validate
and Box.value
or just make the single case constructors public.
There are two packages, make sure you only reference the one you need:
Project type | Package |
---|---|
Standard | |
Fable |
You can check the project source code behind the live demo. You can also look into Text.fs for an example of string boxes which are the by far the most common type of boxes.
Using this library you can create airtight domain objects guaranteed to never have invalid content. Not only you're writing less code, but your domain definition files are much smaller and nicer to work with. You'll also get ROP almost for free, and while there is a case to be made against ROP, it's definitely a perfect match for content validation, especially content that may be entered by a user.