https://github.com/lfr/fsharp.domain.validation
Designing with types requires a lot of code - this library fixes that
https://github.com/lfr/fsharp.domain.validation
ddd dotnet fsharp tiny
Last synced: 20 days ago
JSON representation
Designing with types requires a lot of code - this library fixes that
- Host: GitHub
- URL: https://github.com/lfr/fsharp.domain.validation
- Owner: lfr
- License: mit
- Created: 2020-02-13T13:23:04.000Z (about 5 years ago)
- Default Branch: master
- Last Pushed: 2024-06-25T18:43:03.000Z (10 months ago)
- Last Synced: 2025-04-02T16:51:51.241Z (28 days ago)
- Topics: ddd, dotnet, fsharp, tiny
- Language: F#
- Homepage:
- Size: 10.5 MB
- Stars: 141
- Watchers: 9
- Forks: 6
- Open Issues: 23
-
Metadata Files:
- Readme: README.md
- Funding: .github/FUNDING.yml
- License: LICENSE
Awesome Lists containing this project
README
[](https://www.nuget.org/packages/FSharp.Domain.Validation/)
[](https://www.nuget.org/packages/FSharp.Domain.Validation.Fable/)
![]()
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 string
module Tweet =
let validate = function
| s when String.IsNullOrWhitespace s β
IsMissingOrBlank |> Error
| s when s.Length > 280 β
IsTooLong 280 |> Error
| s β Tweet s |> Ok
let value (Tweet s) = x in s
type Tweet private (s) = class end with
static member Validate = function
| s when String.IsNullOrWhitespace s β
IsMissingOrBlank |> Error
| s when s.Length > 280 β
IsTooLong 280 |> Error
| s β Tweet s |> Ok
interface IConstrained<string> with
member x.Value = s|
type Tweet = private Tweet of Text withΒ Β Β Β Β Β Β Β Β β‘Β [See the live demo](https://impure.fun/FSharp.Domain.Validation/demo/)
interface TextBox with
member _.Validate =
fun s -> s.Length > 280 => IsTooLong 280You 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!
## Interface? Really?
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 rulesThere's no other mentions of interfaces in the code that creates or uses Validation boxes, only when defining new types.
## 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 `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### 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.
```fsharp
// 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.
```fsharp
// all string-based types can now interface TextBox instead of IBox
type TextBox = inherit IBox
```### Declaring your types
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.
```fsharp
/// 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]
```### Simpler validation rules with validation operators
The type declaration above can be simplified further using the provided `=>` and `==>` operators that here combine a predicate of `string` with the appropriate error.```fsharp
/// 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](/src/FSharp.Domain.Validation/Example/Text.fs) for more examples of validation operators.### Creating and using boxes in your code
Using Validation boxes is easy, let's say you have a box called `email`, you can simply access its value using the following:
```fsharp
// 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.
```fsharp
// experimental β same as Box.value
%email // β string
```Creating a box is just as simple:
```fsharp
// 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:
```fsharp
// create a box when its type can't be inferred
Box.validate s // β Ok Tweet | Error e
```β Do **not** force type inference using type annotations as it's unnecessarily verbose:
```fsharp
// incorrect example, do *not* copy/paste
let result : Result = // :(
Box.validate "[email protected]"// correct alternative when type inference isn't available
let result =
Box.validate "[email protected]" // :)
```In both cases `result` is of type `Result`.
## Exceptions instead of Error
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:```fsharp
// 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 "this better be valid 2" // β Text
```## Serialization
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.
## 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. It's 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:
- [x] 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.
## Ready to try it?
There are two packages, make sure you only reference the one you need:
| Project type | Package |
|---|:--|
|Standard|[](https://www.nuget.org/packages/FSharp.Domain.Validation/)|
|Fable|[](https://www.nuget.org/packages/FSharp.Domain.Validation.Fable/)|You can check the [project source code](https://github.com/lfr/FSharp.Domain.Validation/tree/master/src/demo) behind the live demo. You can also look into [Text.fs](/src/FSharp.Domain.Validation/Example/Text.fs) for an example of string boxes which are the by far the most common type of boxes.
## Conclusion
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](https://fsharpforfunandprofit.com/rop/) almost for free, and while there is a case to be made [against ROP](https://fsharpforfunandprofit.com/posts/against-railway-oriented-programming/), it's definitely a perfect match for content validation, especially content that may be entered by a user.