A tiny library with huge potential to simplify your domain design, as you can see from the examples below:
|Without ValidationBlocks||With ValidationBlocks|
// Single-case union style
type Tweet = private Tweet of Text with
You may have noticed that the examples on the left have an additional validation case. On the right this validation is implicit in the statement that a
Tweet is a
Tweet of Text. Since validation blocks are built on top of each other, the only rules that need to be explicitly declared are the rules specific to the block itself. One could imagine a similar behavior with OO-style types, but there's no simple way to achieve that with private constructors.
F# is a multi-paradigm language. Whether you think it's a good thing or a bad thing (it's both), with the right discipline certain OO concepts can be harnessed 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 ValidationBlock
- Enforce the definition of validation rules
There's no other mentions of interfaces in the code that uses or creates validation blocks, only when you declare the block in your domain definition file.
How it works
First you declare your error types, then you declare your actual domain types (i.e.
Tweet), and finally you use them with the provided
Declaring your errors
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.
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 simply by abbreviating the
IBlock<_,_> interface for a specific primitive type.
// all string blocks can now interface IText instead of IBlock<string, TextError> type IText = inherit IBlock<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 ValidationBlock 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 IText with member _.Validate = // validation rule fun s -> [if s |> String.IsNullOrWhiteSpace then IsMissingOrBlank]
Simpler validation rules with validation operators
The type declaration above can be simplified further using the provided
==> 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 IText with member _.Validate = // validation rule using validation operators String.IsNullOrWhiteSpace ==> IsMissingOrBlank
To use validation operators make sure to open
FSharp.ValidationBlocks.Operators in the file(s) where you declare your ValidationBlocks. See Text.fs for more examples of validation operators.
Creating and using blocks in your code
Using validation blocks is easy, let's say you have a block binding called
// get the primitive value from the block Block.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.ValidationBlocks, so to avoid operator pollution this is marked as experimental until the final operator characters are decided.
// experimental — same as Block.value %email // → string
Creating a block is just as simple:
// create a block when block type can be inferred Block.validate s // → Ok 'block | Error e
When type inference isn't possible, specify block type using the generic parameter:
// create a block when block type can be inferred Block.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 email : Result<Email, TextError list> = // :( Block.validate "email@example.com" // correct alternative let email = Block.validate<Email> "firstname.lastname@example.org" // :)
In both cases the resulting
Result<Email, TextError list>.
Exceptions instead of Error
Block.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.blockof "this better be valid" // → 'block (inferred) // same as above without type inference Unchecked.blockof<Text> "this better be valid 2" // → Text
System.Text.Json.Serialization.JsonConverter included, if you add it to your serialization options all blocks 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 ValidationBlocks.
Not just strings
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. They're 100% generic so the sky is the limit.
Ok looks good, but I'm still not sure
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
Block.value or just make the single case constructors public.
In addition to the above, if you use the provided JsonConverter, your blocks will be serialized as their primitive type (i.e. string) and not as ValidationBlocks, so you're not adding any indirect dependency between this library and whatever is on the other side of your serialization.
Using validation blocks you can create airtight domain objects guaranteed to never have invalid content. Not only you're writing less code, but your domain code 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.
Tweet @fishyrock to contribute or give feedback!
Full working example
You can find a full working example in the file Text.fs