├── src ├── demo │ ├── Procfile │ ├── Nuget.Config │ ├── server.js │ ├── src │ │ ├── Helper.fs │ │ ├── Examples.fs │ │ ├── App.fs │ │ └── Types.fs │ ├── Aptfile │ ├── package.json │ ├── webpack.config.js │ ├── demo.fsproj │ └── public │ │ ├── style.css │ │ └── index.html ├── FSharp.Domain.Validation │ ├── Properties │ │ ├── serviceDependencies.json │ │ ├── serviceDependencies.local.json │ │ └── launchSettings.json │ ├── Operators.fs │ ├── InternalResult.fs │ ├── Program.fs │ ├── Serialization.fs │ ├── Reflection.fs │ ├── FSharp.Domain.Validation.fsproj │ ├── Example │ │ └── Text.fs │ ├── _deprecated.fs │ └── Box.fs ├── FSharp.Domain.Validation.Common │ ├── AssemblyInfo.fs │ ├── IBox.fs │ ├── _deprecated.fs │ ├── Utils.fs │ └── FSharp.Domain.Validation.Common.fsproj ├── FSharp.Domain.Validation.Fable │ ├── Operators.fs │ ├── Thoth.fs │ ├── FSharp.Domain.Validation.Fable.fsproj │ ├── _deprecated.fs │ ├── _deprecated.Fable.fs │ └── Box.fs └── FSharp.Domain.Validation.sln ├── logo ├── fav.png ├── hd.png ├── cube.png ├── social.png └── square.png ├── assets ├── demo.gif ├── style-oo.svg ├── style-single-case.svg └── scroll.svg ├── docs ├── public │ ├── favicon.ico │ ├── syntax.css │ └── site.css ├── _config.yml ├── index.md ├── _layouts │ └── default.html └── demo.md ├── .github └── FUNDING.yml ├── LICENSE ├── .gitignore └── README.md /src/demo/Procfile: -------------------------------------------------------------------------------- 1 | # run before deploy: heroku ps:scale web=1 2 | web: npm start -------------------------------------------------------------------------------- /logo/fav.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lfr/FSharp.Domain.Validation/HEAD/logo/fav.png -------------------------------------------------------------------------------- /logo/hd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lfr/FSharp.Domain.Validation/HEAD/logo/hd.png -------------------------------------------------------------------------------- /logo/cube.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lfr/FSharp.Domain.Validation/HEAD/logo/cube.png -------------------------------------------------------------------------------- /assets/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lfr/FSharp.Domain.Validation/HEAD/assets/demo.gif -------------------------------------------------------------------------------- /logo/social.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lfr/FSharp.Domain.Validation/HEAD/logo/social.png -------------------------------------------------------------------------------- /logo/square.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lfr/FSharp.Domain.Validation/HEAD/logo/square.png -------------------------------------------------------------------------------- /docs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lfr/FSharp.Domain.Validation/HEAD/docs/public/favicon.ico -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-minimal 2 | url: https://impure.fun 3 | baseurl: '/FSharp.Domain.Validation/' -------------------------------------------------------------------------------- /src/FSharp.Domain.Validation/Properties/serviceDependencies.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "secrets1": { 4 | "type": "secrets" 5 | } 6 | } 7 | } -------------------------------------------------------------------------------- /src/FSharp.Domain.Validation/Properties/serviceDependencies.local.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "secrets1": { 4 | "type": "secrets.user" 5 | } 6 | } 7 | } -------------------------------------------------------------------------------- /src/FSharp.Domain.Validation.Common/AssemblyInfo.fs: -------------------------------------------------------------------------------- 1 | module AssemblyInfo 2 | 3 | open System.Runtime.CompilerServices 4 | 5 | [] 6 | do() -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | ## Demos 4 | 5 | The links below contain demos of validation blocks in action! 6 | 7 | ### Links 8 | 9 | - [FSharp.ValidationBlocks.Fable](/docs/demo) 10 | -------------------------------------------------------------------------------- /src/FSharp.Domain.Validation/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "FSharp.ValidationBlocks": { 4 | "commandName": "Project", 5 | "environmentVariables": { 6 | "Key": "Value" 7 | } 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /src/demo/Nuget.Config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/demo/server.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var express = require('express'); 3 | 4 | var app = express(); 5 | 6 | app.use(express.static(path.join(__dirname, 'public'))); 7 | app.set('port', process.env.PORT); 8 | app.set('host', '0.0.0.0'); 9 | 10 | var server = app.listen(app.get('port'), app.get('host'), function () { 11 | console.log('listening on port ', server.address().port); 12 | }); 13 | -------------------------------------------------------------------------------- /assets/style-oo.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 7 | // Object-oriented style 8 | 9 | 10 | -------------------------------------------------------------------------------- /assets/style-single-case.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 7 | // Single-case union style 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/demo/src/Helper.fs: -------------------------------------------------------------------------------- 1 | module Result 2 | 3 | open FSharp.Domain.Validation 4 | open FSharp.Domain.Validation.Utils 5 | 6 | // Helper function to translate Result`2 objects into readable text 7 | let toText = function 8 | | Ok block -> 9 | Box.value block 10 | |> sprintf "👍 \"%A\"" 11 | |> fun s -> s.Replace("\n", "◄┘") // display new lines as '◄┘' 12 | | Error e -> (sprintf "😢 %A" e |> depascalize).Replace("[", "[input ") 13 | 14 | -------------------------------------------------------------------------------- /assets/scroll.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 7 | scroll right in mobile » 8 | 9 | -------------------------------------------------------------------------------- /src/demo/Aptfile: -------------------------------------------------------------------------------- 1 | # This must be used with https://github.com/heroku/heroku-buildpack-apt 2 | # Add Microsoft's source found inside packages.microsoft.com/ubuntu/20.04/packages-microsoft-prod.deb 3 | # WARNING: Any control characters here will break apt deployment, edit file in GitHub to see them 4 | :repo:deb [trusted=yes arch=amd64] https://packages.microsoft.com/ubuntu/20.04/prod focal main 5 | # List packages as per dotnet install guide for 20.04 6 | apt-transport-https 7 | dotnet-sdk-5.0 8 | -------------------------------------------------------------------------------- /src/FSharp.Domain.Validation.Common/IBox.fs: -------------------------------------------------------------------------------- 1 | namespace FSharp.Domain.Validation 2 | 3 | type IBox = interface end 4 | type IBoxOf<'baseType> = inherit IBox 5 | 6 | type IBox<'baseType, 'error> = 7 | inherit IBoxOf<'baseType> 8 | abstract member Validate: ('baseType -> 'error list) 9 | 10 | type internal NoValidation<'baseType, 'error> = private NewBox of 'baseType with 11 | static member Box (src:'baseType) = NewBox src 12 | interface IBox<'baseType, 'error> with 13 | member _.Validate = fun _ -> [] -------------------------------------------------------------------------------- /src/demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "engines": { 4 | "node": "14.13.0", 5 | "npm": "6.14.8" 6 | }, 7 | "scripts": { 8 | "heroku-postbuild": "dotnet new tool-manifest && dotnet tool install fable && dotnet fable . --run webpack", 9 | "start": "node server.js", 10 | "dev": "dotnet fable watch . --run webpack-dev-server" 11 | }, 12 | "dependencies": { 13 | "@babel/core": "^7.12.10", 14 | "react": "^16.14.0", 15 | "react-dom": "^16.14.0", 16 | "webpack": "^4.44.2", 17 | "webpack-cli": "^3.3.12", 18 | "webpack-dev-server": "^3.11.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/demo/webpack.config.js: -------------------------------------------------------------------------------- 1 | // Note this only includes basic configuration for development mode. 2 | // For a more comprehensive configuration check: 3 | // https://github.com/fable-compiler/webpack-config-template 4 | 5 | var path = require("path"); 6 | 7 | module.exports = { 8 | mode: "development", 9 | entry: "./src/App.fs.js", 10 | output: { 11 | path: path.join(__dirname, "./public"), 12 | filename: "bundle.js", 13 | }, 14 | devServer: { 15 | publicPath: "/", 16 | contentBase: "./public", 17 | port: 8080, 18 | }, 19 | module: { 20 | rules: [{ 21 | test: /\.fs(x|proj)?$/, 22 | use: "fable-loader" 23 | }] 24 | } 25 | } -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: lfr # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /src/FSharp.Domain.Validation/Operators.fs: -------------------------------------------------------------------------------- 1 | namespace FSharp.Domain.Validation 2 | 3 | [] 4 | module NamespaceOperators = 5 | 6 | /// Same as Box.value 7 | let inline (~%) box = Box.value box 8 | 9 | module Operators = 10 | 11 | /// Turns predicates into errors, useful when there's a predicate available to use. 12 | /// Use: String.IsNullOrWhitespace ==> MyError 13 | let (==>) (condition:'a -> bool) (error:'e) (inp:'a) = 14 | if condition inp then [error] else [] 15 | 16 | /// Turns conditions into errors. Better for conditions made of lambdas. 17 | /// Use: fun s -> s.Length > 280 => MyError 18 | let (=>) (condition:bool) (error:'e) = 19 | if condition then [error] else [] 20 | 21 | /// Flattens a list of error lists. Use: fun s -> !? [ condition1 s => MyError; condition2 s => MyError; ] 22 | let (!?) (errors:'e list list) : 'e list = errors |> List.concat -------------------------------------------------------------------------------- /src/FSharp.Domain.Validation.Fable/Operators.fs: -------------------------------------------------------------------------------- 1 | namespace FSharp.Domain.Validation 2 | 3 | [] 4 | module NamespaceOperators = 5 | 6 | /// Same as Box.value 7 | let inline (~%) box = Box.value box 8 | 9 | module Operators = 10 | 11 | /// Turns predicates into errors, useful when there's a predicate available to use. 12 | /// Use: String.IsNullOrWhitespace ==> MyError 13 | let (==>) (condition:'a -> bool) (error:'e) (inp:'a) = 14 | if condition inp then [error] else [] 15 | 16 | /// Turns conditions into errors. Better for conditions made of lambdas. 17 | /// Use: fun s -> s.Length > 280 => MyError 18 | let (=>) (condition:bool) (error:'e) = 19 | if condition then [error] else [] 20 | 21 | /// Flattens a list of error lists. Use: fun s -> !? [ condition1 s => MyError; condition2 s => MyError; ] 22 | let (!?) (errors:'e list list) : 'e list = errors |> List.concat -------------------------------------------------------------------------------- /src/demo/src/Examples.fs: -------------------------------------------------------------------------------- 1 | module Examples 2 | 3 | open Browser.Dom 4 | open Thoth.Json 5 | open FSharp.Domain.Validation 6 | open FSharp.Domain.Validation.Thoth 7 | 8 | let pre = document.querySelector("pre") :?> Browser.Types.HTMLPreElement 9 | let box = document.querySelector("#inp") :?> Browser.Types.HTMLTextAreaElement 10 | 11 | let sandbox () = 12 | 13 | let thothCustomEncoders = 14 | Extra.empty 15 | |> Extra.withCustom Codec.Encode Codec.Decode 16 | 17 | pre.textContent <- 18 | let value = 19 | {| 20 | Block = typeof.Name 21 | Value = 22 | Box.validate box.value 23 | |> function Ok x -> x | _ -> failwith "invalid" 24 | |} 25 | 26 | sprintf "Toth serialized content:\n%s" <| 27 | Encode.Auto.toString (4, value, extra = thothCustomEncoders) 28 | -------------------------------------------------------------------------------- /src/demo/src/App.fs: -------------------------------------------------------------------------------- 1 | module App 2 | 3 | 4 | open type FSharp.Domain.Validation.Box 5 | open Browser.Dom 6 | 7 | // Get bindings for the text area and 3 result contains 8 | let inp = document.querySelector("#inp") :?> Browser.Types.HTMLTextAreaElement 9 | let result1 = document.querySelector("#resultBox1") :?> Browser.Types.HTMLSpanElement 10 | let result2 = document.querySelector("#resultBox2") :?> Browser.Types.HTMLSpanElement 11 | let result3 = document.querySelector("#resultBox3") :?> Browser.Types.HTMLSpanElement 12 | 13 | // Register our listener 14 | inp.oninput <- fun _ -> 15 | 16 | result1.textContent <- 17 | validate inp.value 18 | |> Result.toText 19 | 20 | result2.textContent <- 21 | validate inp.value 22 | |> Result.toText 23 | 24 | result3.textContent <- 25 | validate inp.value 26 | |> Result.toText 27 | 28 | 29 | 30 | // Enable sandbox in dev server 31 | #if DEBUG 32 | Examples.sandbox () 33 | #endif -------------------------------------------------------------------------------- /src/FSharp.Domain.Validation/InternalResult.fs: -------------------------------------------------------------------------------- 1 | namespace FSharp.Domain.Validation 2 | 3 | 4 | 5 | /// A runtime result containing either a valid box or a list of errors 6 | type IBoxResult = 7 | 8 | /// Get the result's box if valid, otherwise throw exception 9 | abstract member Unbox : unit -> 'result 10 | 11 | 12 | 13 | /// This helper type should not be used outside of this library 14 | type internal Result<'error>(value:obj, errors:'error list) = 15 | 16 | 17 | member x.IsOk = errors |> List.isEmpty 18 | member x.Errors = 19 | if x.IsOk then 20 | failwith "Attempt to access errors of valid result." else errors 21 | member x.Value = 22 | if x.IsOk then value else 23 | sprintf "Validation of the given input returned errors: %A" errors 24 | |> failwith 25 | 26 | member x.ToResult<'box> () = 27 | if x.IsOk then x.Value :?> 'box |> Ok 28 | else Error x.Errors 29 | 30 | interface IBoxResult with 31 | member x.Unbox () = x.Value |> box |> unbox -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Luis Ferrao 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/FSharp.Domain.Validation/Program.fs: -------------------------------------------------------------------------------- 1 | #if FABLE_COMPILER 2 | module Main 3 | [] 4 | let main argv = 0 5 | #else 6 | 7 | open System 8 | open FSharp.ValidationBlocks 9 | open FSharp.ValidationBlocks.Example 10 | open FSharp.ValidationBlocks.Serialization 11 | open System.Text.Json 12 | [] 13 | let main argv = 14 | 15 | // TODO make actual tests out of these 16 | let msg = "If all goes well these should be equal" 17 | let s = "te\0st\t" 18 | 19 | // check that Block.value = initial value 20 | let sb = 21 | Block.validate s 22 | |> function Ok b -> b | _ -> failwith "error" 23 | printfn "%s: %A = %A (canonicalized)" msg s (%sb) 24 | 25 | let options = System.Text.Json.JsonSerializerOptions() 26 | options.Converters.Add(ValidationBlockJsonConverterFactory()) 27 | 28 | // check serialized 29 | let serialized = JsonSerializer.Serialize(sb, options) 30 | printfn "%s: %A = %A" msg serialized (s |> Utils.canonicalize |> JsonSerializer.Serialize) 31 | 32 | // check deserialized 33 | let deserialized = 34 | JsonSerializer.Deserialize(serialized, typeof, options) 35 | printfn "%s: %A = %A" msg sb deserialized 36 | 37 | // check non-valid text 38 | let test = Block.validate "This is \not valid text." 39 | printfn "%s: %A" msg test 40 | 41 | 0 // exit 42 | 43 | #endif -------------------------------------------------------------------------------- /src/demo/demo.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net5.0 4 | 5 | 6 | TRACE;FABLE_COMPILER 7 | 8 | 9 | TRACE;;FABLE_COMPILER; 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/FSharp.Domain.Validation.Fable/Thoth.fs: -------------------------------------------------------------------------------- 1 | namespace FSharp.Domain.Validation.Thoth 2 | 3 | open FSharp.Domain.Validation 4 | open Thoth.Json 5 | 6 | /// Contains custom encoder and decoder to property serialize boxes as their 7 | /// inner type, use both encoder and decoder together for each box type: 8 | /// Extra.empty |> Extra.withCustome Codec.Encode Codec.Decode 9 | type Codec<'a, 'e> private () = class end with 10 | 11 | /// Custom encoder for validation boxes to be properly serialized in json 12 | /// as their inner type, use along with decoder: 13 | /// Extra.empty |> Extra.withCustom Codec.Encode Codec.Decode 14 | static member inline Encode<'box when 'box :> IBox<'a,'e>> (box:'box) : JsonValue = 15 | Encode.Auto.toString(4, Box.value box) :> _ 16 | 17 | /// Custom decoder for validation boxes to be properly serialized in json 18 | /// as their inner type, use along with encoder: 19 | /// Extra.empty |> Extra.withCustom Codec.Encode Codec.Decode 20 | static member inline Decode<'box when 'box :> IBox<'a,'e>> (path:string) (value:JsonValue) : Result<'box, DecoderError> = 21 | Decode.Auto.fromString<'a>(value :?> string) 22 | |> Result.map (fun x -> box x :?> 'a) 23 | |> function 24 | | Ok x -> 25 | Box.validate x 26 | |> Result.mapError 27 | (fun e -> path, e |> List.map (sprintf "%A") |> BadOneOf) 28 | | Error e -> Error (path, FailMessage e) -------------------------------------------------------------------------------- /src/FSharp.Domain.Validation.Common/_deprecated.fs: -------------------------------------------------------------------------------- 1 | namespace FSharp 2 | 3 | [] 4 | module ValidationBlocks = 5 | 6 | [] 7 | type IBlock = FSharp.Domain.Validation.IBox 8 | [] 9 | type IBlockOf<'baseType> = FSharp.Domain.Validation.IBoxOf<'baseType> 10 | [] 11 | type IBlock<'baseType, 'error> = FSharp.Domain.Validation.IBox<'baseType, 'error> 12 | 13 | open FSharp.Domain.Validation.Utils 14 | 15 | [] 16 | module Utils = 17 | 18 | /// Removes leading and trailing whitespace/control characters as well as 19 | /// any occurrences of the null (0x0000) character 20 | [] 21 | let canonicalize = canonicalize 22 | 23 | /// Converts 'PascalCase' to 'lower case'. 24 | /// This can be useful to convert error union cases to error messages. 25 | [] 26 | let depascalize = depascalize 27 | 28 | /// Returnes true if string contains control characters 29 | [] 30 | let containsControlCharacters = containsControlCharacters 31 | -------------------------------------------------------------------------------- /src/FSharp.Domain.Validation/Serialization.fs: -------------------------------------------------------------------------------- 1 | namespace FSharp.Domain.Validation 2 | 3 | open System.Text.Json 4 | open System.Text.Json.Serialization 5 | open FSharp.Domain.Validation.Reflection 6 | 7 | module Serialization = 8 | 9 | type private ValidationJsonConverter<'box> () = 10 | inherit JsonConverter<'box>() 11 | 12 | override _.CanConvert t = typeof<'box>.IsAssignableFrom(t) 13 | 14 | override _.Read(reader, t, options) = 15 | let bi = t |> boxinfo 16 | let value = JsonSerializer.Deserialize(&reader, bi.BaseType, options) 17 | let result = Runtime.verbatim t value 18 | result.Unbox() 19 | 20 | override _.Write (writer, value, options) = 21 | let bi = value.GetType() |> boxinfo 22 | JsonSerializer.Serialize(writer, value |> Box.unwrap, bi.BaseType, options) 23 | 24 | 25 | type ValidationJsonConverterFactory() = 26 | inherit JsonConverterFactory() 27 | 28 | let typedef = typedefof> 29 | 30 | let implementsIBox (t:System.Type) = 31 | t.GetInterfaces() |> Array.exists (fun i -> i.IsGenericType && 32 | i.GetGenericTypeDefinition() = typedefof>) 33 | 34 | override _.CanConvert t = 35 | t |> typeof.IsAssignableFrom && t |> implementsIBox 36 | 37 | override _.CreateConverter (t, _) = 38 | let converterType = typedef.MakeGenericType([|t|]) 39 | System.Activator.CreateInstance converterType :?> JsonConverter -------------------------------------------------------------------------------- /src/demo/public/style.css: -------------------------------------------------------------------------------- 1 | html { 2 | padding: 1px; 3 | } 4 | 5 | body { 6 | margin: 0; 7 | box-sizing: border-box; 8 | overflow: hidden; 9 | } 10 | 11 | table { 12 | width: 100%; 13 | border-spacing: 0; 14 | } 15 | 16 | textarea { 17 | --margin: 0rem; 18 | margin: var(--margin); 19 | width: calc(100% - 2 * var(--margin)); 20 | display: blocK; 21 | box-sizing: border-box; 22 | resize: none; 23 | background: Yellow; 24 | } 25 | 26 | table { 27 | font-family: 'Inconsolata', monospace; 28 | font-size: smaller; 29 | background-color: #111; 30 | border: 1px solid black; 31 | } 32 | 33 | div { 34 | font-family: monospace; 35 | text-align: right; 36 | color: dimgray; 37 | font-size: smaller; 38 | } 39 | 40 | th { 41 | background: dimgray; 42 | color: black; 43 | font-weight: normal; 44 | font-size: smaller; 45 | padding: .2rem; 46 | } 47 | 48 | td { 49 | font-size: smaller; 50 | color: white; 51 | } 52 | 53 | tbody tr td:first-child { 54 | width: 11.7rem; 55 | } 56 | 57 | tbody tr td:last-child { 58 | color: limegreen; 59 | } 60 | 61 | .comment { 62 | color: limegreen; 63 | } 64 | 65 | .type { 66 | color: #0cc; 67 | } 68 | 69 | .keyword { 70 | color: cornflowerblue; 71 | } 72 | 73 | .symbol { 74 | color: #f6f; /* lighter magenta */ 75 | } 76 | 77 | .result { 78 | color: yellow; 79 | font-size: smaller; 80 | font-family: sans-serif; 81 | } 82 | 83 | a { 84 | color: gray; 85 | text-decoration: none; 86 | } 87 | 88 | a:hover { 89 | text-decoration: underline; 90 | } -------------------------------------------------------------------------------- /docs/_layouts/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | {{ content }} 40 | 41 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/FSharp.Domain.Validation.Common/Utils.fs: -------------------------------------------------------------------------------- 1 | namespace FSharp.Domain.Validation 2 | 3 | open System 4 | 5 | module Utils = 6 | 7 | /// All control or whitespace characters 8 | let ctrlWhitespaceChars = 9 | [int System.Char.MinValue .. int System.Char.MaxValue] 10 | |> List.map char 11 | |> List.filter 12 | (fun c -> System.Char.IsControl c || System.Char.IsWhiteSpace c) 13 | |> List.toArray 14 | 15 | /// All control characters 16 | let ctrlChars = 17 | ctrlWhitespaceChars 18 | |> Array.filter 19 | (fun c -> System.Char.IsControl c) 20 | 21 | /// Removes leading and trailing whitespace/control characters as well as 22 | /// any occurrences of the null (0x0000) character 23 | let 24 | #if FABLE_COMPILER 25 | inline 26 | #endif 27 | canonicalize (inp:'a) (inpType:System.Type) : 'a = 28 | match box inp with 29 | | null -> null 30 | | x when inpType <> typeof -> x 31 | | x -> (box x :?> string).Trim(ctrlWhitespaceChars).Replace("\0","") |> box 32 | |> unbox 33 | 34 | /// Converts 'PascalCase' to 'lower case'. 35 | /// This can be useful to convert error union cases to error messages. 36 | let depascalize (s:string) = 37 | s.ToCharArray () 38 | |> Array.mapi 39 | (fun i c -> 40 | match c with 41 | | c when i = 0 -> [|Char.ToLowerInvariant c|] 42 | | c when Char.IsUpper c -> [|' '; Char.ToLowerInvariant c|] 43 | | c -> [|c|]) 44 | |> Array.concat 45 | |> String 46 | 47 | /// Returnes true if string contains control characters 48 | let containsControlCharacters (s:string) = 49 | ctrlChars |> Array.exists s.Contains 50 | -------------------------------------------------------------------------------- /src/FSharp.Domain.Validation.Common/FSharp.Domain.Validation.Common.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net5.0 4 | true 5 | 0.9.78 6 | Luis Ferrao 7 | Luis Ferrao 8 | FSharp.Domain.Validation 9 | This package is a dependency and should only be referenced for library development, for normal projects you want FSharp.Domain.Validation for non-Fable development, or FSharp.Domain.Validation.Fable for Fable projects 10 | Copyright © 2021 Luis Ferrao 11 | MIT 12 | https://github.com/lfr/FSharp.Domain.Validation 13 | square.png 14 | validation blocks boxes fsharp ddd domain 15 | true 16 | true 17 | 18 | 19 | ..\.package-bin\ 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | .\fable\ 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | True 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/demo/src/Types.fs: -------------------------------------------------------------------------------- 1 | [] 2 | module Types 3 | 4 | open FSharp.Domain.Validation 5 | open FSharp.Domain.Validation.Utils 6 | open System 7 | 8 | 9 | // 1. Define all the possible errors that the boxes can yield 10 | type TextError = 11 | | ContainsControlCharacters 12 | | IsMissingOrBlank 13 | | IsNotAValidInteger 14 | 15 | 16 | // 2. This interface in not strictly necessary but it makes 17 | // type declarations and function signatures a more readable 18 | type TextBlock = inherit IBox 19 | 20 | 21 | // 3. Define your custom types 22 | /// Single or multi-line text without additional validation 23 | type FreeText = private FreeText of string with 24 | interface TextBlock with 25 | member _.Validate = 26 | fun s -> 27 | [if String.IsNullOrWhiteSpace s then IsMissingOrBlank] 28 | 29 | // can be simplified using FSharp.Domain.Validation.Operators: 30 | // member _.Validate = 31 | // String.IsNullOrWhiteSpace ==> ``Is missing or blank`` 32 | 33 | /// Single line of text (no control characters) 34 | type Text = private Text of FreeText with 35 | interface TextBlock with 36 | member _.Validate = 37 | fun s -> 38 | [if containsControlCharacters s then ContainsControlCharacters] 39 | 40 | /// String representation of an integer 41 | type Integer = private Integer of FreeText with 42 | interface TextBlock with 43 | member _.Validate = 44 | fun s -> 45 | [if Int32.TryParse(s) |> fst |> not then IsNotAValidInteger] 46 | 47 | 48 | 49 | 50 | /// This type can be used to test that Fable's FSharp.Domain.Validation throws 51 | /// exceptions with boxes of types that are discriminated unions, 52 | /// test with: Block.validate (Ok "some text") 53 | [] 54 | type TestDu = private TestDu of Result with 55 | interface IBox, TextError> with 56 | member _.Validate = fun _ -> [] 57 | 58 | -------------------------------------------------------------------------------- /src/FSharp.Domain.Validation/Reflection.fs: -------------------------------------------------------------------------------- 1 | namespace FSharp.Domain.Validation 2 | 3 | type BoxInfo = 4 | { 5 | BaseType : System.Type 6 | ErrorType : System.Type 7 | ValidateMethod : System.Reflection.MethodInfo 8 | } 9 | 10 | module Reflection = 11 | 12 | type Flags = System.Reflection.BindingFlags 13 | 14 | let private noBox = Unchecked.defaultof> 15 | let private typeError (t:System.Type) = 16 | sprintf "'%s' is not a Validation box." t.Name 17 | let internal isBox (t:System.Type) = 18 | t.GetInterfaces() |> Array.exists ((=) typeof) 19 | 20 | let mutable private biCache : Map = Map.empty 21 | /// Gets reflection information about the specified box type 22 | let boxinfo (boxType:System.Type) = 23 | biCache 24 | |> Map.tryFind boxType.GUID 25 | |> Option.defaultWith 26 | (fun () -> 27 | if isBox boxType then 28 | let validateMi = 29 | boxType.GetMethods(Flags.NonPublic ||| Flags.Instance) 30 | |> Array.find 31 | (fun mi -> nameof noBox.Validate |> mi.Name.EndsWith) 32 | let bi = 33 | boxType.GetInterfaces() 34 | |> Array.find 35 | (fun i -> 36 | i.IsGenericType && 37 | i.GetGenericTypeDefinition() = typedefof>) 38 | |> fun i -> i.GetGenericArguments() 39 | |> fun x -> 40 | { 41 | BaseType = x.[0] 42 | ErrorType = x.[1] 43 | ValidateMethod = validateMi 44 | 45 | } 46 | biCache <- biCache |> Map.add boxType.GUID bi 47 | bi 48 | else typeError boxType |> invalidArg (nameof boxType)) -------------------------------------------------------------------------------- /src/FSharp.Domain.Validation/FSharp.Domain.Validation.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Library 5 | net5.0 6 | true 7 | true 8 | 0.9.78 9 | Luis Ferrao 10 | 11 | FSharp.Domain.Validation 12 | Designing with types requires a lot of code - this library fixes that 13 | Copyright © 2021 Luis Ferrao 14 | MIT 15 | https://github.com/lfr/FSharp.Domain.Validation 16 | validation blocks boxes fsharp domain ddd 17 | square.png 18 | 19 | FSharp.Domain.Validation 20 | 21 | 22 | 23 | ..\.package-bin\ 24 | 25 | 26 | 27 | ..\.package-bin\ 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | True 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/FSharp.Domain.Validation.Fable/FSharp.Domain.Validation.Fable.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | true 6 | true 7 | 0.9.78 8 | Luis Ferrao 9 | 10 | FSharp.Domain.Validation 11 | Designing with types requires a lot of code - this library fixes that for Fable 12 | Copyright © 2021 Luis Ferrao 13 | MIT 14 | https://github.com/lfr/FSharp.Domain.Validation 15 | validation blocks boxes fsharp ddd domain fable javascript 16 | square.png 17 | 18 | FSharp.Domain.Validation.Fable 19 | 20 | 21 | 22 | TRACE;FABLE_COMPILER; 23 | true 24 | true 25 | ..\.package-bin\ 26 | 27 | 28 | 29 | ..\.package-bin\ 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | .\fable\ 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | True 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /src/FSharp.Domain.Validation.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29609.76 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FSharp.Domain.Validation", "FSharp.Domain.Validation\FSharp.Domain.Validation.fsproj", "{E788CE37-23C0-4573-AB4D-253F4796DAF4}" 7 | EndProject 8 | Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FSharp.Domain.Validation.Fable", "FSharp.Domain.Validation.Fable\FSharp.Domain.Validation.Fable.fsproj", "{5593AAFC-6A39-4F5A-8C09-F0DE95051573}" 9 | EndProject 10 | Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "demo", "demo\demo.fsproj", "{D7EE84D3-7323-46B8-9E52-70CD7E955C3C}" 11 | EndProject 12 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FSharp.Domain.Validation.Common", "FSharp.Domain.Validation.Common\FSharp.Domain.Validation.Common.fsproj", "{4E55EFFA-89E8-4040-8000-3254BC3BD013}" 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Release|Any CPU = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {E788CE37-23C0-4573-AB4D-253F4796DAF4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {E788CE37-23C0-4573-AB4D-253F4796DAF4}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {E788CE37-23C0-4573-AB4D-253F4796DAF4}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {E788CE37-23C0-4573-AB4D-253F4796DAF4}.Release|Any CPU.Build.0 = Release|Any CPU 24 | {5593AAFC-6A39-4F5A-8C09-F0DE95051573}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {5593AAFC-6A39-4F5A-8C09-F0DE95051573}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {5593AAFC-6A39-4F5A-8C09-F0DE95051573}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {5593AAFC-6A39-4F5A-8C09-F0DE95051573}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {D7EE84D3-7323-46B8-9E52-70CD7E955C3C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {D7EE84D3-7323-46B8-9E52-70CD7E955C3C}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {D7EE84D3-7323-46B8-9E52-70CD7E955C3C}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {D7EE84D3-7323-46B8-9E52-70CD7E955C3C}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {4E55EFFA-89E8-4040-8000-3254BC3BD013}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {4E55EFFA-89E8-4040-8000-3254BC3BD013}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {4E55EFFA-89E8-4040-8000-3254BC3BD013}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {4E55EFFA-89E8-4040-8000-3254BC3BD013}.Release|Any CPU.Build.0 = Release|Any CPU 36 | EndGlobalSection 37 | GlobalSection(SolutionProperties) = preSolution 38 | HideSolutionNode = FALSE 39 | EndGlobalSection 40 | GlobalSection(ExtensibilityGlobals) = postSolution 41 | SolutionGuid = {99A8B369-D75A-4B57-8D73-2E303CE1B94A} 42 | EndGlobalSection 43 | EndGlobal 44 | -------------------------------------------------------------------------------- /src/FSharp.Domain.Validation/Example/Text.fs: -------------------------------------------------------------------------------- 1 | namespace FSharp.Domain.Validation.Example 2 | 3 | 4 | open FSharp.Domain.Validation 5 | open FSharp.Domain.Validation.Operators 6 | open FSharp.Domain.Validation.Utils 7 | 8 | /// Define all the possible errors that the boxes can yield 9 | type TextError = 10 | | ContainsControlCharacters 11 | | ContainsTabs 12 | | ExceedsMaximumLength of int 13 | | IsMissingOrBlank 14 | 15 | #if !FABLE_COMPILER 16 | 17 | /// This interface in not strictly necessary but it makes type declarations 18 | /// and function signatures a lot more readable 19 | type TextBox = inherit IBox 20 | 21 | /// This is a good place to define IText-specific functions 22 | module Text = 23 | 24 | /// Validates the given trimmed string treating null/blank as a valid result of None 25 | /// Use: Text.optional "hello!" or Text.optional "hello!" 26 | let optional<'box when 'box :> TextBox> s : Result<'box option, TextError list> = 27 | if System.String.IsNullOrWhiteSpace s then Ok None 28 | else Box.validate<'box> s |> Result.map Some 29 | 30 | /// Creates a box option from the given string if valid, otherwise throws an exception 31 | /// Use: Text.uncheckedOptional "hello!" or Text.uncheckedOptional "hello!" 32 | let uncheckedOptional<'box when 'box :> TextBox> s : 'box option = 33 | if System.String.IsNullOrWhiteSpace s then None else 34 | Unchecked.boxof s |> Some 35 | 36 | 37 | /// Single or multi-line text without any validation 38 | type FreeText = private FreeText of string with 39 | interface TextBox with 40 | member _.Validate = 41 | // System.String.IsNullOrWhiteSpace => IsMissingOrBlank 42 | fun s -> 43 | [if s |> System.String.IsNullOrWhiteSpace then IsMissingOrBlank] 44 | 45 | 46 | /// Single line of text (no control characters) 47 | type Text = private Text of FreeText with 48 | interface TextBox with 49 | member _.Validate = 50 | fun s -> 51 | [if containsControlCharacters s then ContainsControlCharacters] 52 | 53 | 54 | /// Text restricted to 280 characters at most when trimmed 55 | /// (the current maximum length of a tweeet), without tabs 56 | type Tweet = private Tweet of FreeText with 57 | interface TextBox with 58 | member _.Validate = 59 | fun s -> 60 | [ 61 | if s.Contains("\t") then ContainsTabs 62 | if s.Length > 280 then ExceedsMaximumLength 280 63 | ] 64 | 65 | 66 | 67 | // Alternative type definition using composing operator and a single condition 68 | type FreeText' = private FreeText' of string with 69 | interface TextBox with 70 | member _.Validate = 71 | System.String.IsNullOrWhiteSpace ==> IsMissingOrBlank 72 | 73 | // Alternative type definition using non-composing operators and multiple conditions 74 | type Tweet' = private Tweet' of FreeText with 75 | interface TextBox with 76 | member _.Validate = 77 | fun s -> !? [ 78 | s.Contains("\t") => ContainsTabs 79 | s.Length > 280 => ExceedsMaximumLength 280 80 | ] 81 | 82 | #endif -------------------------------------------------------------------------------- /docs/public/syntax.css: -------------------------------------------------------------------------------- 1 | .highlight .hll { background-color: #ffc; } 2 | .highlight .c { color: #999; } /* Comment */ 3 | .highlight .err { color: #a00; background-color: #faa } /* Error */ 4 | .highlight .k { color: #069; } /* Keyword */ 5 | .highlight .o { color: #555 } /* Operator */ 6 | .highlight .cm { color: #09f; font-style: italic } /* Comment.Multiline */ 7 | .highlight .cp { color: #099 } /* Comment.Preproc */ 8 | .highlight .c1 {color: olive;} /* Comment.Single */ 9 | .highlight .cs { color: #999; } /* Comment.Special */ 10 | .highlight .gd { background-color: #fcc; border: 1px solid #c00 } /* Generic.Deleted */ 11 | .highlight .ge { font-style: italic } /* Generic.Emph */ 12 | .highlight .gr { color: #f00 } /* Generic.Error */ 13 | .highlight .gh { color: #030; } /* Generic.Heading */ 14 | .highlight .gi { background-color: #cfc; border: 1px solid #0c0 } /* Generic.Inserted */ 15 | .highlight .go { color: #aaa } /* Generic.Output */ 16 | .highlight .gp { color: #009; } /* Generic.Prompt */ 17 | .highlight .gs { } /* Generic.Strong */ 18 | .highlight .gu { color: #030; } /* Generic.Subheading */ 19 | .highlight .gt { color: #9c6 } /* Generic.Traceback */ 20 | .highlight .kc { color: #069; } /* Keyword.Constant */ 21 | .highlight .kd { color: #069; } /* Keyword.Declaration */ 22 | .highlight .kn { color: #069; } /* Keyword.Namespace */ 23 | .highlight .kp { color: #069 } /* Keyword.Pseudo */ 24 | .highlight .kr { color: #069; } /* Keyword.Reserved */ 25 | .highlight .kt { color: #078; } /* Keyword.Type */ 26 | .highlight .m { color: #f60 } /* Literal.Number */ 27 | .highlight .s { color: #d44950 } /* Literal.String */ 28 | .highlight .na { color: #4f9fcf } /* Name.Attribute */ 29 | .highlight .nb { color: #366 } /* Name.Builtin */ 30 | .highlight .nc { color: #0a8; } /* Name.Class */ 31 | .highlight .no { color: #360 } /* Name.Constant */ 32 | .highlight .nd { color: #99f } /* Name.Decorator */ 33 | .highlight .ni { color: #999; } /* Name.Entity */ 34 | .highlight .ne { color: #c00; } /* Name.Exception */ 35 | .highlight .nf { color: #c0f } /* Name.Function */ 36 | .highlight .nl { color: #99f } /* Name.Label */ 37 | .highlight .nn { color: #0cf; } /* Name.Namespace */ 38 | .highlight .nt { color: #2f6f9f; } /* Name.Tag */ 39 | .highlight .nv { color: #033 } /* Name.Variable */ 40 | .highlight .ow { color: #000; } /* Operator.Word */ 41 | .highlight .w { color: #bbb } /* Text.Whitespace */ 42 | .highlight .mf { color: #f60 } /* Literal.Number.Float */ 43 | .highlight .mh { color: #f60 } /* Literal.Number.Hex */ 44 | .highlight .mi { color: #f60 } /* Literal.Number.Integer */ 45 | .highlight .mo { color: #f60 } /* Literal.Number.Oct */ 46 | .highlight .sb { color: #c30 } /* Literal.String.Backtick */ 47 | .highlight .sc { color: #c30 } /* Literal.String.Char */ 48 | .highlight .sd { color: #c30; font-style: italic } /* Literal.String.Doc */ 49 | .highlight .s2 { color: green } /* Literal.String.Double */ 50 | .highlight .se { color: #c30; } /* Literal.String.Escape */ 51 | .highlight .sh { color: #c30 } /* Literal.String.Heredoc */ 52 | .highlight .si { color: #a00 } /* Literal.String.Interpol */ 53 | .highlight .sx { color: #c30 } /* Literal.String.Other */ 54 | .highlight .sr { color: #3aa } /* Literal.String.Regex */ 55 | .highlight .s1 { color: #c30 } /* Literal.String.Single */ 56 | .highlight .ss { color: #fc3 } /* Literal.String.Symbol */ 57 | .highlight .bp { color: #366 } /* Name.Builtin.Pseudo */ 58 | .highlight .vc { color: #033 } /* Name.Variable.Class */ 59 | .highlight .vg { color: #033 } /* Name.Variable.Global */ 60 | .highlight .vi { color: #033 } /* Name.Variable.Instance */ 61 | .highlight .il { color: #f60 } /* Literal.Number.Integer.Long */ 62 | 63 | .css .o, 64 | .css .o + .nt, 65 | .css .nt + .nt { color: #999; } -------------------------------------------------------------------------------- /src/FSharp.Domain.Validation.Fable/_deprecated.fs: -------------------------------------------------------------------------------- 1 | namespace FSharp.ValidationBlocks 2 | 3 | [] 4 | module Fable = 5 | 6 | type Type = System.Type 7 | 8 | [] 9 | type IBlock = FSharp.ValidationBlocks.IBlock 10 | [] 11 | type IBlock<'baseType, 'error> = FSharp.ValidationBlocks.IBlock<'baseType, 'error> 12 | [] 13 | type IBlockOf<'baseType> = FSharp.ValidationBlocks.IBlockOf<'baseType> 14 | 15 | open FSharp.Domain.Validation 16 | 17 | [] 18 | module Block = 19 | 20 | /// Returns true if an object is a block 21 | [] 22 | let isBlock (inp:obj) = Box.isBox inp 23 | 24 | /// This is the primary way to get a value out of a box 25 | [] 26 | let value (src:'block when 'block :> IBlock<'baseType,'error>) : 'baseType = 27 | Box.value src 28 | 29 | [] 30 | type Block<'a, 'e> private () = class end with 31 | 32 | /// Creates a block from the given input if valid, otherwise returns an Error (strings are NOT canonicalized), 33 | /// use Block.verbatim when return type can be inferred, otherwise Block.verbatim<'block> 34 | [] 35 | static member inline verbatim<'block when 'block :> IBlock<'a,'e>> (inp:'a) : Result<'block, 'e list> = 36 | Box<'a, 'e>.verbatim<'block> inp 37 | 38 | /// Main function to create boxes, creates a block from the given input if valid, 39 | /// otherwise returns an Error, use Block.validate when return type can be 40 | /// inferred, otherwise Block.validate<'block> 41 | [] 42 | static member inline validate<'block when 'block :> IBlock<'a,'e>> (inp:'a) : Result<'block, 'e list> = 43 | Box<'a, 'e>.validate<'block> inp 44 | 45 | [] 46 | [] 47 | module NamespaceOperators = 48 | 49 | /// Same as Block.value 50 | let inline (~%) block = FSharp.Domain.Validation.NamespaceOperators.(~%) block 51 | 52 | [] 53 | module Operators = 54 | 55 | /// Turns predicates into errors, useful when there's a predicate available to use. 56 | /// Use: String.IsNullOrWhitespace ==> MyError 57 | [] 58 | let (==>) = FSharp.Domain.Validation.Operators.(==>) 59 | 60 | /// Turns conditions into errors. Better for conditions made of lambdas. 61 | /// Use: fun s -> s.Length > 280 => MyError 62 | [] 63 | let (=>) = FSharp.Domain.Validation.Operators.(=>) 64 | 65 | /// Flattens a list of error lists. Use: fun s -> !? [ condition1 s => MyError; condition2 s => MyError; ] 66 | [] 67 | let (!?) = FSharp.Domain.Validation.Operators.(!?) 68 | 69 | module Thoth = 70 | 71 | [] 72 | type Codec<'a, 'e> = FSharp.Domain.Validation.Thoth.Codec<'a, 'e> -------------------------------------------------------------------------------- /src/FSharp.Domain.Validation.Fable/_deprecated.Fable.fs: -------------------------------------------------------------------------------- 1 | namespace FSharp.Domain.Validation 2 | 3 | [] 4 | module Fable = 5 | 6 | type Type = System.Type 7 | 8 | [] 9 | type IBlock = FSharp.ValidationBlocks.IBlock 10 | [] 11 | type IBlock<'baseType, 'error> = FSharp.ValidationBlocks.IBlock<'baseType, 'error> 12 | [] 13 | type IBlockOf<'baseType> = FSharp.ValidationBlocks.IBlockOf<'baseType> 14 | 15 | open FSharp.Domain.Validation 16 | 17 | [] 18 | module Block = 19 | 20 | /// Returns true if an object is a block 21 | [] 22 | let isBlock (inp:obj) = Box.isBox inp 23 | 24 | /// This is the primary way to get a value out of a box 25 | [] 26 | let value (src:'block when 'block :> IBlock<'baseType,'error>) : 'baseType = 27 | Box.value src 28 | 29 | [] 30 | type Block<'a, 'e> private () = class end with 31 | 32 | /// Creates a block from the given input if valid, otherwise returns an Error (strings are NOT canonicalized), 33 | /// use Block.verbatim when return type can be inferred, otherwise Block.verbatim<'block> 34 | [] 35 | static member inline verbatim<'block when 'block :> IBlock<'a,'e>> (inp:'a) : Result<'block, 'e list> = 36 | Box<'a, 'e>.verbatim<'block> inp 37 | 38 | /// Main function to create boxes, creates a block from the given input if valid, 39 | /// otherwise returns an Error, use Block.validate when return type can be 40 | /// inferred, otherwise Block.validate<'block> 41 | [] 42 | static member inline validate<'block when 'block :> IBlock<'a,'e>> (inp:'a) : Result<'block, 'e list> = 43 | Box<'a, 'e>.validate<'block> inp 44 | 45 | [] 46 | [] 47 | module NamespaceOperators = 48 | 49 | /// Same as Block.value 50 | let inline (~%) block = FSharp.Domain.Validation.NamespaceOperators.(~%) block 51 | 52 | [] 53 | module Operators = 54 | 55 | /// Turns predicates into errors, useful when there's a predicate available to use. 56 | /// Use: String.IsNullOrWhitespace ==> MyError 57 | [] 58 | let (==>) = FSharp.Domain.Validation.Operators.(==>) 59 | 60 | /// Turns conditions into errors. Better for conditions made of lambdas. 61 | /// Use: fun s -> s.Length > 280 => MyError 62 | [] 63 | let (=>) = FSharp.Domain.Validation.Operators.(=>) 64 | 65 | /// Flattens a list of error lists. Use: fun s -> !? [ condition1 s => MyError; condition2 s => MyError; ] 66 | [] 67 | let (!?) = FSharp.Domain.Validation.Operators.(!?) 68 | 69 | module Thoth = 70 | 71 | [] 72 | type Codec<'a, 'e> = FSharp.Domain.Validation.Thoth.Codec<'a, 'e> -------------------------------------------------------------------------------- /src/FSharp.Domain.Validation/_deprecated.fs: -------------------------------------------------------------------------------- 1 | namespace FSharp 2 | 3 | [] 4 | module ValidationBlocks = 5 | 6 | open FSharp.Domain.Validation 7 | 8 | [] 9 | module Block = 10 | 11 | /// This is the primary way to get a value out of a block 12 | let value = Box.value 13 | 14 | [] 15 | let ``return`` = Box.``return`` 16 | 17 | [] 18 | let apply = Box.apply 19 | 20 | // Equality and comparison 21 | [] 22 | let equals = Box.equals 23 | 24 | [] 25 | let differs = Box.differs 26 | 27 | [] 28 | let compareTo = Box.compareTo 29 | 30 | [] 31 | type Block<'a, 'e> private () = class end with 32 | 33 | /// Creates a block from the given input if valid, otherwise returns an Error (strings are NOT canonicalized), 34 | /// use Block.verbatim when return type can be inferred, otherwise Block.verbatim<'block> 35 | static member verbatim<'block when 'block :> IBox<'a,'e>> (inp:'a) : Result<'block, 'e list> = 36 | Box<'a, 'e>.verbatim<'block> inp 37 | 38 | /// Main function to create boxes, creates a block from the given input if valid, 39 | /// otherwise returns an Error, use Block.validate when return type can be 40 | /// inferred, otherwise Block.validate<'block> 41 | static member validate (inp:'a) : Result<'block, 'e list> = 42 | Box<'a, 'e>.validate inp 43 | 44 | module Runtime = 45 | 46 | /// Non-generic version of Block.verbatim, mostly meant for serialization 47 | let verbatim = Runtime.verbatim 48 | 49 | type Unchecked<'a> private () = class end with 50 | 51 | /// Creates a block from the given input if valid, otherwise throws an exception, 52 | /// use Unchecked.blockof when return type can be inferred, otherwise Unchecked.blockof<'block> 53 | static member blockof<'block when 'block :> IBoxOf<'a>> (inp:'a) : 'block = 54 | FSharp.Domain.Validation.Unchecked<'a>.boxof<'block> inp 55 | 56 | 57 | [] 58 | [] 59 | module NamespaceOperators = 60 | 61 | /// Same as Block.value 62 | let inline (~%) block = FSharp.Domain.Validation.NamespaceOperators.(~%) block 63 | 64 | [] 65 | module Operators = 66 | 67 | open FSharp.Domain.Validation.Operators 68 | 69 | /// Turns predicates into errors, useful when there's a predicate available to use. 70 | /// Use: String.IsNullOrWhitespace ==> MyError 71 | [] 72 | let (==>) = (==>) 73 | 74 | /// Turns conditions into errors. Better for conditions made of lambdas. 75 | /// Use: fun s -> s.Length > 280 => MyError 76 | [] 77 | let (=>) = (=>) 78 | 79 | /// Flattens a list of error lists. Use: fun s -> !? [ condition1 s => MyError; condition2 s => MyError; ] 80 | [] 81 | let (!?) = (!?) 82 | 83 | [] 84 | module Serialization = 85 | 86 | [] 87 | type ValidationBlockJsonConverterFactory = 88 | FSharp.Domain.Validation.Serialization.ValidationJsonConverterFactory -------------------------------------------------------------------------------- /src/demo/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Demo 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |

19 | 20 |

21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 |
The very complicated F# code behind this page
  
// Register our listener
inp.oninput <- fun _ ->
 
  result1.textContent <-
    validate<Text> inp.value// Defined as non-multiline text
    |> Result.toText// → waiting for input...
 
  result2.textContent <-
    validate<FreeText> inp.value// Defined as any non-blank text
    |> Result.toText// → waiting for input...
 
  result3.textContent <-
    validate<Integer> inp.value// Defined as parseable to int
    |> Result.toText// → waiting for input...
 
73 |
Powered by FSharp.Domain.Validation.Fable
74 | 75 | 76 |
.
77 | 78 | 79 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /docs/public/site.css: -------------------------------------------------------------------------------- 1 | html { 2 | /* the following 3 lines center the body */ 3 | align-items: center; 4 | display: flex; 5 | justify-content: center; 6 | } 7 | 8 | * { 9 | border-color: #555; 10 | } 11 | 12 | body { 13 | background-color: #222; 14 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEYAAABGCAAAAABURb1YAAAFoUlEQVR4AWXWCY7kMJcDYd9DiCD4jLr/EWezJOT8vbMXy8ZngpVPlLUWbWABJFVRZyqqdqIq9O9V1gJS14KFfYzASvgS1UTF1jSqE9qgNjYVoNxLfIhCBFKhOTdPNR11KpmKk4W2LhowESMPKHHBItCg2tqoGm0VbWnXAmxIAFALPIAtCxY4VVX7jqrSGRVlJnwqfRsAIG+BZ0EmHy7Jm3NMo6KzdWXGc8ycY2zyHdOrW3hHhcQ0+uvcYlz3LQgQVyrPp2t+dUncjzHVTNFNuvhwK2Jgkfh856/INm05pjEVtWEKC6IfrmmtfPnpYZkRVTPjFztVUDsDLLCvR2WmX6RPXV/KRBQ7E0GZN5w+5+rKd2J7Ev1sFtVGlYY2oq2tgtN1SakCTkwiaxkebLZuJtjCd/OvyY1OQXXrGnAKfE67xbGq/NzemNviTWC2LjHjl6ruFucdUTTT4/wO7tQtkPd0LtO1ncs+ZrVvUXXeHue3Xt1zTD1vJlkbt8rigWhH3EUJ6HT31JYE1lp15XY1BSDaFh4asRNt0U5gqraB+UyFZJNagXh/qTxV1TY90VSVSLMX0W7dGA/4SfEZj+R7l6Hs9O6Orx5ne51Tj3Oev5E9MFWReefovrI73lzdnTziy8mTRjU1jZ99GtT52We2bmVfvqvKovqYqZ6N2GlGbRZYWM3CRKzX2bVQoMqjnbSopmlBTe3vMix227dzvqRJVR9+dT0LfJZhTyzbeaffIg6qz+muKJ2KoJ249yD3ku5kI/sJm31Mj41TrIoNVlgrolc33nTK06qPrdpRW6UTnZwnj6BuXdy6EMCqtPqcPUhS9OoC6mnYr+7p7tY10zyfLv7ovrMxfLs2y9trusGZMyW+80zU7z9Wz5D//8YusxMkP845i/Qk21Sru3W3u+aI5OtbWK3bpVVtk8c0OnuLpYVlXWQrYgFui0nAouTMy6OZtKKk5+EN9+GPs6e7aOpuewv6eL++gfnrIbiNdX7+bu0+j4rCvKo+aKdVxenV7dGtO3F1aRrP00R9sN26TNa+JsLpKYmLtZ1ZWJxGSc1UfDy7q/1Ms9ZdbuICFXJ1I5puXRt8WlXTpAsWhrrb4v7K7e3N7a5pZ4f0ed3mfy8L1iLzr3PmpDllv0nnfaaeZcj185/d/U3qj662fWwFpnsSqYvb3eN1+ky0Vclom50eTncXxA9x2cDu/F6GZcSAGrWqCe698be7qTv529jjXEVtT9GSGbktZp3G7nScj+4CTnflfXN1VXzU1uyLG88KnBSPblVBnUZFm91inYBsXW/zbG4SltE0YKtp1I5po4/tbWxYZ83PFldYiWcZCpkvaUc11Weu7iYwhltZfu3VnlGhnRHUmT6zABb+mv4u8Np/p1xTVd9XYG9x3RNybm39cWYnGkXTO9kn0fG5y7BWXKexp7v085JOxI62OyWN0pEHE/yEEgx7LSyf7rJbklTVakak3e18wHicTVlrRzeV9WzjiGrP7NqZAmt/Lv5nd518uitvVNH56W5Qwfd1AQ+YxP/UjXqfJgpm2tyhUpE0suBZFqzHNFm7uzl/l7sHnajtdf68Fs/ZWMm4gHpb/OliqjfRUf2bmBGYLJ66u5vJrpqRf5cORdupKvl7D2n06VdiMl3/sbvrdBfnL6ri+yX07933pfP801huIiaoOLm6J82c2xkfb2NPAs1Zhmva6XWuOm/W1P0+Hmz4XRnXTWpGs/cgN2nvZwsiPEBSd3Ud2XFjJNPtPCd1Zlys2zmef3Z3rU8tomK3rs5N73v3+ZXd4uY6E2GB9SzD7MbaprkJYN1rn1/dSCJrWc1U6GjbLZIp2nFZf67g4e6ucbH40Y1WwebsbrbuUjCfxGdzG/uldlA1naJI5p3j/FaARdxvZtKHfz49dIKizl9hL3Cvruf/3Sum/w07RUTLccAlwgAAAABJRU5ErkJggg==); 15 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; 16 | color: rgb(204, 204, 204); 17 | width: 100%; 18 | max-width: 60rem; 19 | margin: 1%; 20 | } 21 | 22 | object { 23 | height: 22rem; 24 | width: 100%; 25 | } 26 | 27 | .object-container { 28 | text-align: center; 29 | } 30 | 31 | h1, h2, h3, h4, h5, h6 { 32 | font-weight: normal; 33 | padding-bottom: .5rem; 34 | margin-bottom: 1rem; 35 | margin-top: 1.5rem; 36 | text-align: center; 37 | } 38 | 39 | @media (min-width: 48em) { 40 | body {width: 80%;} 41 | object {width: 80%;} 42 | h1, h2, h3, h4, h5, h6 { 43 | text-align: left; 44 | border-bottom-width: 1px; 45 | border-bottom-style: solid; 46 | } 47 | } 48 | 49 | p { 50 | padding-left: .6rem; 51 | padding-right: .6rem; 52 | line-height: 1.5; 53 | } 54 | 55 | a { 56 | color: steelblue; 57 | text-decoration: none; 58 | } 59 | 60 | a:hover { 61 | text-decoration: underline; 62 | } 63 | 64 | .footer { 65 | text-align: center; 66 | font-size: smaller; 67 | margin-top: 3rem; 68 | border-top: 1px; 69 | padding-top: 1rem; 70 | } 71 | 72 | .footer a { 73 | color: gray; 74 | padding: 1rem; 75 | } 76 | 77 | pre.highlight { 78 | background: #111; 79 | border: 1px solid black; 80 | padding: .5rem; 81 | } 82 | 83 | code.language-plaintext { 84 | background: #222; 85 | border: 1px solid black; 86 | padding: 1px; 87 | border: 1px solid rgba(255, 255, 255, .1); 88 | border-radius: 6px; 89 | padding: .2rem .4rem; 90 | } 91 | 92 | .twitter-share-button { 93 | width: 80px !important; 94 | vertical-align: middle; 95 | margin-top: .01rem; 96 | } 97 | 98 | @media (max-width: 48em) { 99 | .twitter-share-button { 100 | display: block; 101 | margin-left: auto; 102 | margin-right: auto; 103 | margin-top: 1rem; 104 | } 105 | } -------------------------------------------------------------------------------- /docs/demo.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Fable validation demo 💙 4 | permalink: /demo/ 5 | description: >- 6 | Sometimes you have to choose between easy and reliable. Not this time. 7 | --- 8 | 9 | # Fable validation demo 💙 10 | 11 | Type something below and try to guess what `validate` does, knowing that the helper `Result.toText` is just a one-liner that renders `Result<>` using different emojis for success and error. 12 |
13 | 14 |
15 | 16 | Perhaps you have something like this in mind: 17 | 18 | ``` 19 | match 'type with 20 | | Text -> check that it's 1 line & not null 21 | | FreeText -> check that it's not null 22 | | Integer -> check that it can be parsed to int 23 | ``` 24 | 25 | Turns out it does nothing of the sort, in fact `validate` is not even defined anywhere in the code! It's a generic function from `FSharp.Domain.Validation`, one that has no awareness of our own custom types `Text`, `FreeText`, and `Integer`. 26 | 27 | ## 100% Object-free ✔ 28 | 29 | You may be thinking "*ok then `FreeText` is an object with a constructor that validates a string*", but it's in fact much simpler than that, it's just a combination of a **validation rule** with a **type name**. The interface below is only used to identify it as a Validation box, and conveniently enforce the definition of a validation rule with the appropriate signature: 30 | 31 | ```fsharp 32 | type FreeText = private FreeText of string with 33 | interface TextBox with 34 | member _.Validate = 35 | String.IsNullOrWhiteSpace ==> IsMissingOrBlank 36 | // 🤯 37 | ``` 38 | 39 | This simplicity is not just a nicety, if you're going to replace all your validated strings with similar types, [and you definitely should](https://impure.fun/fun/2020/03/04/these-arent-the-types/), it's important that these can be defined with minimal code. 40 | 41 | ## Certified DRY™ ✔ 42 | 43 | Our other custom type `Text` also rejects empty strings, but its definition doesn't even declare that rule: 44 | 45 | ```fsharp 46 | type Text = private Text of FreeText with 47 | interface TextBox with 48 | member _.Validate = 49 | containsControlChars ==> ContainsCtrlChars 50 | // 🤯🤯 51 | ``` 52 | 53 | ## Certified KISS™ ✔ 54 | 55 | So declaring types requires very little code, but so does validating! Most of the time the function `validate` doesn't even have to specify a type like in the code below: 56 | 57 | ```fsharp 58 | open type FSharp.Domain.Validation.Box 59 | open FsToolkit.ErrorHandling 60 | 61 | // this creates a valid(ated) record using 62 | // a validation CE from FsToolkit.ErrorHandling 63 | validation { 64 | 65 | let! text = validate inp1 66 | and! freeText = validate inp2 67 | //🤯🤯🤯 68 | 69 | return 70 | { 71 | TextProp = text 72 | FreeTextProp = freeText 73 | } 74 | } 75 | ``` 76 | 77 | ## May contain traces of RTFM 📖 78 | 79 | I know it all sounds super easy but do me and yourself a favor, [read the project's README](https://github.com/lfr/FSharp.Domain.Validation) before trying this at home. Not only it's more up-to-date than this demo, but it also uses examples that don't make the use of `FSharp.Domain.Validation.Operators` making them more stock F# and easier to follow. 80 | 81 | For instance here it's not immediately obvious that the `_.Validate` function returns a list of errors. 82 | 83 | ## Share the love 💙 84 | 85 | Excited about this? Spread the word to your fellow dev!  86 | 93 | 94 | ## Fable users read this 🚨 95 | 96 | * With Fable you'll have reference only the package `FSharp.Domain.Validation`👉`.Fable` 97 | * Records like `MyDomain` above are worthless unless they can be used in javascript, in order to properly serialize them with [Thoth.Json](https://thoth-org.github.io/Thoth.Json/) use extra encoders for each box type: 98 | ```fsharp 99 | open FSharp.Domain.Validation.Thoth 100 | 101 | let myExtraCoders = 102 | Extra.empty 103 | |> Extra.withCustom 104 | Codec.Encode 105 | Codec.Decode 106 | |> Extra.withCustom 107 | Codec.Encode 108 | Codec.Decode 109 | // etc… 110 | ``` 111 | 112 | 113 | 114 | * The function `Unchecked.boxof` won't be available in Fable until [Fable#2321](https://github.com/fable-compiler/Fable/issues/2321) is closed, so for now the only way to quickly skip `Result<_,_>` is with something like: 115 | ```fsharp 116 | |> function Ok x -> x | _ -> failwith "💣" 117 | ``` 118 | 119 | This demo code first appeared in [this article](). 120 | -------------------------------------------------------------------------------- /src/FSharp.Domain.Validation.Fable/Box.fs: -------------------------------------------------------------------------------- 1 | namespace FSharp.Domain.Validation 2 | 3 | open Fable.Core.JsInterop 4 | open Microsoft.FSharp.Reflection 5 | open FSharp.Domain.Validation 6 | open System.Reflection 7 | 8 | type Type = System.Type 9 | 10 | // we'll use this marker to get the namespace 11 | type __ = interface end 12 | 13 | module Box = 14 | 15 | // we'll use this to safely get the name of the validate method 16 | let private __ = {new IBox<_,_> with member _.Validate = fun _ -> []} 17 | 18 | // TODO: get rid of this as soon as proper nameof is supported 19 | [] 20 | module _nameof = 21 | let Validate = "Validate" 22 | let wrap = "wrap" 23 | let IBox = typedefof>.Name 24 | 25 | /// Returns true if an object is a box 26 | let isBox (inp:obj) = inp?(_nameof.Validate) <> None 27 | 28 | /// This is the primary way to get a value out of a box 29 | let rec value (src:'box when 'box :> IBox<'baseType,'error>) : 'baseType = 30 | 31 | let typeError s = sprintf "%A is not a %s" src s 32 | //let typeError s = $"%A{src$ is not a %s{s}" ← replace above with this in net5.0 33 | 34 | match src?fields with 35 | 36 | // union case has only one field that has no inner fields 37 | | Some x when x?(1) = None && not<|isBox x?(0) -> x?(0) |> unbox 38 | 39 | // union case has only one field that does have inner fields 40 | | Some x when x?(1) = None -> x?(0) |> value 41 | 42 | // union case has more than one field 43 | | Some _ -> "single-case single-field union" |> typeError |> failwith 44 | 45 | // union case has no fields 46 | | _ -> "single-case union" |> typeError |> failwith 47 | 48 | type Box<'a, 'e> private () = class end with 49 | 50 | [] 51 | // this method cannot be made private because the calling method has to be inline 52 | static member wrap (originalInput:'a) (boxType:Type) = 53 | 54 | match FSharpType.GetUnionCases (boxType, true) with 55 | | [|uci|] -> 56 | match uci.GetFields() with 57 | 58 | | [|pi|] when not<|FSharpType.IsUnion pi.PropertyType -> 59 | let primitiveBox = 60 | FSharpValue.MakeUnion(uci, [|box originalInput|], true) 61 | :?> IBox<'a, 'e> 62 | primitiveBox.Validate originalInput, primitiveBox 63 | 64 | | [|pi|] -> 65 | let errors, innerBox = 66 | Box<'a,'e>.wrap originalInput pi.PropertyType 67 | 68 | let box = 69 | FSharpValue.MakeUnion(uci, [|box innerBox|], true) 70 | :?> IBox<'a, 'e> 71 | 72 | match errors with 73 | | [] -> box.Validate originalInput, innerBox 74 | | e -> e, innerBox 75 | 76 | | x -> sprintf "Expected a single-field union case, but %s contains %i fields." uci.Name x.Length |> failwith 77 | | x -> sprintf "Expected a single-case union, but %s contains %i cases." boxType.Name x.Length |> failwith 78 | 79 | /// Creates a box from the given input if valid, otherwise returns an Error (strings are NOT canonicalized), 80 | /// use Box.verbatim when return type can be inferred, otherwise Box.verbatim<'box> 81 | static member inline verbatim<'box when 'box :> IBox<'a,'e>> (inp:'a) : Result<'box, 'e list> = 82 | 83 | if FSharpType.IsUnion typeof<'a> then 84 | typeof<__>.Namespace 85 | |> failwithf "%s doesn't currently support boxes of union types (except for Option<'a>)" 86 | 87 | match Box<'a,'e>.wrap inp typeof<'box> with 88 | | [], box -> box :?> _ |> Ok 89 | | errors, _ -> Error errors 90 | 91 | /// Main function to create boxes, creates a box from the given input if valid, 92 | /// otherwise returns an Error, use Box.validate when return type can be 93 | /// inferred, otherwise Box.validate<'box> 94 | static member inline validate<'box when 'box :> IBox<'a,'e>> (inp:'a) : Result<'box, 'e list> = 95 | FSharp.Domain.Validation.Utils.canonicalize 96 | inp typeof<'a> |> Box<'a,'e>.verbatim 97 | 98 | 99 | open Box 100 | 101 | type Unchecked<'a> private () = class end with 102 | 103 | /// Creates a box from the given input if valid, otherwise throws an exception, 104 | /// use Unchecked.Boxof when return type can be inferred, otherwise Unchecked.Boxof<'box> 105 | [] 106 | // Missing reflection support: 107 | // 🔲 Type.GetInterface 108 | // ✅ Type.MakeGenericType 109 | // ✅ GetGenericArguments 110 | // ✅ Type.IsGenericType 111 | // ✅ Type.GetGenericTypeDefinition 112 | // 🔲 Type.GetMethods 113 | // 🔲 MethodBase.Invoke 114 | // https://github.com/fable-compiler/Fable/issues/2321 115 | static member inline boxof<'box when 'box :> IBoxOf<'a>> (inp:'a) = 116 | 117 | let errorType = 118 | typeof<'box> 119 | .GetInterface(_nameof.IBox) 120 | .GetGenericArguments().[1] // 0 is base type, 1 is error type 121 | 122 | let wrapMi = 123 | typedefof> 124 | .MakeGenericType(typeof<'a>, errorType) 125 | .GetMethod(_nameof.wrap, 126 | BindingFlags.Static ||| BindingFlags.Public) 127 | 128 | wrapMi.Invoke(null, [|box inp|]) 129 | :?> (obj list * 'box) 130 | |> function 131 | | [], x -> x 132 | | _ -> failwithf "Cannot create box with invalid input '%A'" inp -------------------------------------------------------------------------------- /src/FSharp.Domain.Validation/Box.fs: -------------------------------------------------------------------------------- 1 | namespace FSharp.Domain.Validation 2 | 3 | open Microsoft.FSharp.Reflection 4 | open FSharp.Domain.Validation.Reflection 5 | 6 | module Box = 7 | 8 | type Module = interface end 9 | 10 | // This enables invoking instance methods on null instances 11 | let private genericDelegate = 12 | typedefof>.MakeGenericType([|typeof|]) 13 | let private nullDelegate t = 14 | let mi = (Reflection.boxinfo t).ValidateMethod 15 | System.Delegate 16 | .CreateDelegate(genericDelegate, null, mi).DynamicInvoke() 17 | |> fun x -> x, x.GetType().GetMethod("Invoke") 18 | 19 | 20 | /// Extracts a value from the box 21 | let rec internal unwrap (x:obj) = 22 | 23 | let typeError = sprintf "'%A'" x |> sprintf "%s is not a %s" 24 | 25 | match FSharpType.GetUnionCases (x.GetType(), true) with 26 | | [|uci|] -> 27 | match uci.GetFields() with 28 | | [|pi|] when FSharpType.IsUnion (pi.PropertyType, true) |> not -> pi.GetValue(x) 29 | | [|pi|] -> pi.GetValue(x) |> unwrap 30 | | _ -> "single-case single-field union" |> typeError |> failwith 31 | | _ -> "single-case union" |> typeError |> failwith 32 | 33 | 34 | /// Tail recursive validation of the top box along with the other boxes it's built upon 35 | let rec internal wrap<'e> 36 | (src:obj, rollingCtor:UnionCaseInfo list) 37 | (boxType:System.Type) : Result<'e> = 38 | 39 | let isPrimitive (pi:System.Reflection.PropertyInfo) = 40 | pi.PropertyType |> typeof.IsAssignableFrom |> not 41 | 42 | let validate (value:obj) boxType = 43 | let mi, nullDelegate = nullDelegate boxType 44 | nullDelegate.Invoke(mi, [|value|]) :?> 'e list 45 | 46 | let folder (state:Result<'e>) (uci:UnionCaseInfo) = 47 | if state.IsOk then 48 | let newValue = FSharpValue.MakeUnion(uci, [|state.Value|], true) 49 | let errors = validate src uci.DeclaringType 50 | Result<'e>(newValue, errors) 51 | else state 52 | 53 | match FSharpType.GetUnionCases (boxType, true) with 54 | | [|uci|] -> 55 | match uci.GetFields() with 56 | 57 | | [|pi|] when pi |> isPrimitive -> 58 | uci::rollingCtor 59 | |> List.fold folder (Result<'e>(src, [])) 60 | 61 | | [|pi|] -> 62 | wrap<'e> (src, uci :: rollingCtor) pi.PropertyType 63 | 64 | | x -> sprintf "Expected a single-field union case, but %s contains %i fields." uci.Name x.Length |> failwith 65 | | x -> sprintf "Expected a single-case union, but %s contains %i cases." boxType.Name x.Length |> failwith 66 | 67 | 68 | /// Internal validation method, just a wrapper with some type checks, not meant to be used outside of this library 69 | let internal validateInternal<'box, 'a, 'err when 'box :> IBox<'a,'err>> (src:'a) : Result<'box, 'err list> = 70 | 71 | let t = typeof<'box> 72 | 73 | // ideally this would be a type constraint on 'box, but sum type constrains don't exist yet 74 | let typeConstraintError = 75 | sprintf "%s, more anotations may help determine more accurately this method's return type." >> failwith 76 | if not<|FSharpType.IsUnion(t, true) then 77 | t.Name |> sprintf "%s is not a Discriminated Union" |> typeConstraintError 78 | elif FSharpType.GetUnionCases(t, Flags.NonPublic ||| Flags.Instance).Length <> 1 then 79 | t.Name |> sprintf "%s is not a single-case signe-value union with private constructor" |> typeConstraintError 80 | 81 | //let result = src |> wrap<'error> t 82 | let result = wrap<'err> (src, []) t 83 | 84 | result.ToResult<'box>() 85 | 86 | /// This is the primary way to get a value out of a box 87 | let rec value (src:IBox<'baseType,'error>) = 88 | src |> unwrap :?> 'baseType 89 | 90 | 91 | [] 92 | let ``return``<'baseType, 'error> (inp:'baseType) : IBox<'baseType, 'error> = 93 | NoValidation<'baseType, 'error>.Box inp :> _ 94 | 95 | [] 96 | let apply (f:IBox<'baseType -> 'a, 'error>) (x:IBox<'baseType,'error>) = 97 | value x |> value f |> ``return``<'a, 'error> 98 | 99 | // Equality and comparison 100 | [] 101 | let equals<'baseType, 'e when 'baseType : equality> 102 | (left:IBox<'baseType, 'e>) (right:IBox<'baseType, 'e>) = 103 | value left = value right 104 | 105 | [] 106 | let differs t1 t2 = equals t1 t2 |> not 107 | 108 | [] 109 | let compareTo<'baseType, 'e when 'baseType :> System.IComparable<'baseType>> 110 | (left:IBox<'baseType, 'e>) (right:IBox<'baseType, 'e>) = 111 | (value left).CompareTo(value right) 112 | 113 | type Box<'a, 'e> private () = class end with 114 | 115 | /// Creates a box from the given input if valid, otherwise returns an Error (strings are NOT canonicalized), 116 | /// use Box.verbatim when return type can be inferred, otherwise Box.verbatim<'box> 117 | static member verbatim<'box when 'box :> IBox<'a,'e>> (inp:'a) : Result<'box, 'e list> = 118 | Box.validateInternal<'box, 'a, 'e> inp 119 | 120 | /// Main function to create boxes, creates a box from the given input if valid, 121 | /// otherwise returns an Error, use Box.validate when return type can be 122 | /// inferred, otherwise Box.validate<'box> 123 | static member validate (inp:'a) : Result<'box, 'e list> = 124 | Utils.canonicalize inp typeof<'a> |> Box<'a, 'e>.verbatim 125 | 126 | module Runtime = 127 | 128 | let private wrapMi = 129 | (nameof Box.wrap, Flags.NonPublic ||| Flags.Static) 130 | |> typeof.DeclaringType.GetMethod 131 | 132 | /// Non-generic version of Box.verbatim, mostly meant for serialization 133 | let verbatim (boxType:System.Type) (input:obj) : IBoxResult = 134 | let bi = Reflection.boxinfo boxType 135 | let mi = wrapMi.MakeGenericMethod([|bi.ErrorType|]) 136 | mi.Invoke(null, [|input; List.empty; boxType|]) :?> _ 137 | 138 | type Unchecked<'a> private () = class end with 139 | 140 | /// Creates a box from the given input if valid, otherwise throws an exception, 141 | /// use Unchecked.boxof when return type can be inferred, otherwise Unchecked.boxof<'box> 142 | static member boxof<'box when 'box :> IBoxOf<'a>> (inp:'a) : 'box = 143 | let result = Runtime.verbatim typeof<'box> inp 144 | result.Unbox() -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Visual Studio code coverage results 141 | *.coverage 142 | *.coveragexml 143 | 144 | # NCrunch 145 | _NCrunch_* 146 | .*crunch*.local.xml 147 | nCrunchTemp_* 148 | 149 | # MightyMoose 150 | *.mm.* 151 | AutoTest.Net/ 152 | 153 | # Web workbench (sass) 154 | .sass-cache/ 155 | 156 | # Installshield output folder 157 | [Ee]xpress/ 158 | 159 | # DocProject is a documentation generator add-in 160 | DocProject/buildhelp/ 161 | DocProject/Help/*.HxT 162 | DocProject/Help/*.HxC 163 | DocProject/Help/*.hhc 164 | DocProject/Help/*.hhk 165 | DocProject/Help/*.hhp 166 | DocProject/Help/Html2 167 | DocProject/Help/html 168 | 169 | # Click-Once directory 170 | publish/ 171 | 172 | # Publish Web Output 173 | *.[Pp]ublish.xml 174 | *.azurePubxml 175 | # Note: Comment the next line if you want to checkin your web deploy settings, 176 | # but database connection strings (with potential passwords) will be unencrypted 177 | *.pubxml 178 | *.publishproj 179 | 180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 181 | # checkin your Azure Web App publish settings, but sensitive information contained 182 | # in these scripts will be unencrypted 183 | PublishScripts/ 184 | 185 | # NuGet Packages 186 | *.nupkg 187 | # NuGet Symbol Packages 188 | *.snupkg 189 | # The packages folder can be ignored because of Package Restore 190 | **/[Pp]ackages/* 191 | # except build/, which is used as an MSBuild target. 192 | !**/[Pp]ackages/build/ 193 | # Uncomment if necessary however generally it will be regenerated when needed 194 | #!**/[Pp]ackages/repositories.config 195 | # NuGet v3's project.json files produces more ignorable files 196 | *.nuget.props 197 | *.nuget.targets 198 | 199 | # Microsoft Azure Build Output 200 | csx/ 201 | *.build.csdef 202 | 203 | # Microsoft Azure Emulator 204 | ecf/ 205 | rcf/ 206 | 207 | # Windows Store app package directories and files 208 | AppPackages/ 209 | BundleArtifacts/ 210 | Package.StoreAssociation.xml 211 | _pkginfo.txt 212 | *.appx 213 | *.appxbundle 214 | *.appxupload 215 | 216 | # Visual Studio cache files 217 | # files ending in .cache can be ignored 218 | *.[Cc]ache 219 | # but keep track of directories ending in .cache 220 | !?*.[Cc]ache/ 221 | 222 | # Others 223 | ClientBin/ 224 | ~$* 225 | *~ 226 | *.dbmdl 227 | *.dbproj.schemaview 228 | *.jfm 229 | *.pfx 230 | *.publishsettings 231 | orleans.codegen.cs 232 | 233 | # Including strong name files can present a security risk 234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 235 | #*.snk 236 | 237 | # Since there are multiple workflows, uncomment next line to ignore bower_components 238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 239 | #bower_components/ 240 | 241 | # RIA/Silverlight projects 242 | Generated_Code/ 243 | 244 | # Backup & report files from converting an old project file 245 | # to a newer Visual Studio version. Backup files are not needed, 246 | # because we have git ;-) 247 | _UpgradeReport_Files/ 248 | Backup*/ 249 | UpgradeLog*.XML 250 | UpgradeLog*.htm 251 | ServiceFabricBackup/ 252 | *.rptproj.bak 253 | 254 | # SQL Server files 255 | *.mdf 256 | *.ldf 257 | *.ndf 258 | 259 | # Business Intelligence projects 260 | *.rdl.data 261 | *.bim.layout 262 | *.bim_*.settings 263 | *.rptproj.rsuser 264 | *- [Bb]ackup.rdl 265 | *- [Bb]ackup ([0-9]).rdl 266 | *- [Bb]ackup ([0-9][0-9]).rdl 267 | 268 | # Microsoft Fakes 269 | FakesAssemblies/ 270 | 271 | # GhostDoc plugin setting file 272 | *.GhostDoc.xml 273 | 274 | # Node.js Tools for Visual Studio 275 | .ntvs_analysis.dat 276 | node_modules/ 277 | 278 | # Visual Studio 6 build log 279 | *.plg 280 | 281 | # Visual Studio 6 workspace options file 282 | *.opt 283 | 284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 285 | *.vbw 286 | 287 | # Visual Studio LightSwitch build output 288 | **/*.HTMLClient/GeneratedArtifacts 289 | **/*.DesktopClient/GeneratedArtifacts 290 | **/*.DesktopClient/ModelManifest.xml 291 | **/*.Server/GeneratedArtifacts 292 | **/*.Server/ModelManifest.xml 293 | _Pvt_Extensions 294 | 295 | # Paket dependency manager 296 | .paket/paket.exe 297 | paket-files/ 298 | 299 | # FAKE - F# Make 300 | .fake/ 301 | 302 | # CodeRush personal settings 303 | .cr/personal 304 | 305 | # Python Tools for Visual Studio (PTVS) 306 | __pycache__/ 307 | *.pyc 308 | 309 | # Cake - Uncomment if you are using it 310 | # tools/** 311 | # !tools/packages.config 312 | 313 | # Tabs Studio 314 | *.tss 315 | 316 | # Telerik's JustMock configuration file 317 | *.jmconfig 318 | 319 | # BizTalk build output 320 | *.btp.cs 321 | *.btm.cs 322 | *.odx.cs 323 | *.xsd.cs 324 | 325 | # OpenCover UI analysis results 326 | OpenCover/ 327 | 328 | # Azure Stream Analytics local run output 329 | ASALocalRun/ 330 | 331 | # MSBuild Binary and Structured Log 332 | *.binlog 333 | 334 | # NVidia Nsight GPU debugger configuration file 335 | *.nvuser 336 | 337 | # MFractors (Xamarin productivity tool) working folder 338 | .mfractor/ 339 | 340 | # Local History for Visual Studio 341 | .localhistory/ 342 | 343 | # BeatPulse healthcheck temp database 344 | healthchecksdb 345 | 346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 347 | MigrationBackup/ 348 | 349 | # Ionide (cross platform F# VS Code tools) working folder 350 | .ionide/ 351 | 352 | .fake 353 | .ionide 354 | 355 | # Custom 356 | src/Properties 357 | .package-bin/ 358 | /src/demo/.config 359 | *.fs.js 360 | /src/ConsoleApplication1 361 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![nuget](https://img.shields.io/nuget/v/FSharp.Domain.Validation.svg?style=badge&logo=nuget&color=brightgreen&cacheSeconds=21600&label=Standard)](https://www.nuget.org/packages/FSharp.Domain.Validation/) 2 | [![nuget](https://img.shields.io/nuget/v/FSharp.Domain.Validation.Fable.svg?style=badge&logo=nuget&color=brightgreen&cacheSeconds=21600&label=Fable)](https://www.nuget.org/packages/FSharp.Domain.Validation.Fable/) 3 |
4 | 5 |

6 | 7 |

8 | 9 | A tiny F# library with huge potential to simplify your domain design, as you can see from the examples below: 10 |
11 | |
Without this package 👎
|
Using this package 👍
| 12 | |---|---| 13 | |
// Single-case union style
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

// Object-oriented style
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
interface TextBox with
member _.Validate =
fun s -> s.Length > 280 => IsTooLong 280
         ➡ [See the live demo](https://impure.fun/FSharp.Domain.Validation/demo/) 14 | 15 | 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! 16 | 17 | 18 | 19 | 20 | ## Interface? Really? 21 | 22 | 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: 23 | 24 | * Identify a type as a Validation box 25 | * Enforce the definition of validation rules 26 | 27 | There's no other mentions of interfaces in the code that creates or uses Validation boxes, only when defining new types. 28 | 29 | ## How it works 30 | 31 | 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! 32 | 33 |

34 | 35 | demo 36 | 37 |
Older version of the live demo for future DDD paleontologists
38 |

39 | 40 | ### Declaring your errors 41 | 42 | 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. 43 | 44 | ```fsharp 45 | // These are just an example, create whatever errors 46 | // you need to return from your own validation rules 47 | type TextError = 48 | | ContainsControlCharacters 49 | | ContainsTabs 50 | | IsTooLong of int 51 | | IsMissingOrBlank 52 | // ... 53 | ``` 54 | 55 | 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. 56 | 57 | ```fsharp 58 | // all string-based types can now interface TextBox instead of IBox 59 | type TextBox = inherit IBox 60 | ``` 61 | 62 | ### Declaring your types 63 | 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. 64 | 65 | 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. 66 | 67 | ```fsharp 68 | /// Single or multi-line non-null non-blank text without any additional validation 69 | type FreeText = private FreeText of string with 70 | interface TextBox with 71 | member _.Validate = 72 | // validation ruleᵴ (only one) 73 | fun s -> 74 | [if s |> String.IsNullOrWhiteSpace then IsMissingOrBlank] 75 | ``` 76 | 77 | ### Simpler validation rules with validation operators 78 | The type declaration above can be simplified further using the provided `=>` and `==>` operators that here combine a predicate of `string` with the appropriate error. 79 | 80 | ```fsharp 81 | /// Alternative type declaration using the ==> operator 82 | type FreeText = private FreeText of string with 83 | interface TextBox with 84 | member _.Validate = 85 | // same validation rule using validation operators 86 | String.IsNullOrWhiteSpace ==> IsMissingOrBlank 87 | ``` 88 | 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. 89 | 90 | ### Creating and using boxes in your code 91 | 92 | Using Validation boxes is easy, let's say you have a box called `email`, you can simply access its value using the following: 93 | 94 | ```fsharp 95 | // get the primitive value from the box 96 | Box.value email // → string 97 | ``` 98 | 99 | 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. 100 | 101 | ```fsharp 102 | // experimental — same as Box.value 103 | %email // → string 104 | ``` 105 | 106 | Creating a box is just as simple: 107 | 108 | ```fsharp 109 | // create a box, canonicalizing (i.e. trimming) the input if it's a string 110 | Box.validate s // → Ok 'box | Error e 111 | ``` 112 | 113 | `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. 114 | 115 | When type inference isn't possible, specify the box type using the generic parameter: 116 | 117 | ```fsharp 118 | // create a box when its type can't be inferred 119 | Box.validate s // → Ok Tweet | Error e 120 | ``` 121 | 122 | ⚠ Do **not** force type inference using type annotations as it's unnecessarily verbose: 123 | 124 | ```fsharp 125 | // incorrect example, do *not* copy/paste 126 | let result : Result = // :( 127 | Box.validate "incorrect@dont.do" 128 | 129 | // correct alternative when type inference isn't available 130 | let result = 131 | Box.validate "dev@fsharp.lang" // :) 132 | ``` 133 | 134 | In both cases `result` is of type `Result`. 135 | 136 | ## Exceptions instead of Error 137 | 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: 138 | 139 | ```fsharp 140 | // throws an exception if not valid 141 | Unchecked.boxof "this better be valid" // → 'box (inferred) 142 | 143 | // same as above, when type inference is not available 144 | Unchecked.boxof "this better be valid 2" // → Text 145 | ``` 146 | 147 | ## Serialization 148 | 149 | 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. 150 | 151 | ## Not just strings 152 | 153 | 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. 154 | 155 | ## Ok looks good, but I'm still not sure 156 | 157 | I've created a checklist to help you decide whether this library is a good match for your project: 158 | 159 | - [x] My project contains domain objects/records 160 | 161 | If your project satisfies all of the above this library is for you! 162 | 163 | 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. 164 | 165 | ## Ready to try it? 166 | 167 | There are two packages, make sure you only reference the one you need: 168 | 169 | | Project type |
Package
| 170 | |---|:--| 171 | |Standard|[![nuget](https://img.shields.io/nuget/v/FSharp.Domain.Validation.svg?style=badge&logo=nuget&color=brightgreen&cacheSeconds=21600&label=FSharp.Domain.Validation)](https://www.nuget.org/packages/FSharp.Domain.Validation/)| 172 | |Fable|[![nuget](https://img.shields.io/nuget/v/FSharp.Domain.Validation.Fable.svg?style=badge&logo=nuget&color=brightgreen&cacheSeconds=21600&label=FSharp.Domain.Validation.Fable)](https://www.nuget.org/packages/FSharp.Domain.Validation.Fable/)| 173 | 174 | 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. 175 | 176 | ## Conclusion 177 | 178 | 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. 179 | --------------------------------------------------------------------------------