Last active
February 9, 2020 09:13
-
-
Save iblazhko/9767a3e38d9d73abb0fde6ceacbc8ddb to your computer and use it in GitHub Desktop.
F# ROP Domain Validation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(* | |
Railway-Oriented Programming | |
https://github.com/swlaschin/Railway-Oriented-Programming-Example/blob/master/src/FsRopExample/Rop.fs | |
*) | |
module ROP = | |
/// A Result is a success or failure | |
/// The Success case has a success value, plus a list of messages | |
/// The Failure case has just a list of messages | |
type RopResult<'TSuccess, 'TMessage> = | |
| Success of 'TSuccess * 'TMessage list | |
| Failure of 'TMessage list | |
/// create a Success with no messages | |
let succeed x = | |
Success (x,[]) | |
/// create a Success with a message | |
let succeedWithMsg x msg = | |
Success (x,[msg]) | |
/// create a Failure with a message | |
let fail msg = | |
Failure [msg] | |
/// A function that applies either fSuccess or fFailure | |
/// depending on the case. | |
let either fSuccess fFailure = function | |
| Success (x,msgs) -> fSuccess (x,msgs) | |
| Failure errors -> fFailure errors | |
/// merge messages with a result | |
let mergeMessages msgs result = | |
let fSuccess (x,msgs2) = | |
Success (x, msgs @ msgs2) | |
let fFailure errs = | |
Failure (errs @ msgs) | |
either fSuccess fFailure result | |
/// given a function that generates a new RopResult | |
/// apply it only if the result is on the Success branch | |
/// merge any existing messages with the new result | |
let bindR f result = | |
let fSuccess (x,msgs) = | |
f x |> mergeMessages msgs | |
let fFailure errs = | |
Failure errs | |
either fSuccess fFailure result | |
/// given a function wrapped in a result | |
/// and a value wrapped in a result | |
/// apply the function to the value only if both are Success | |
let applyR f result = | |
match f,result with | |
| Success (f,msgs1), Success (x,msgs2) -> | |
(f x, msgs1@msgs2) |> Success | |
| Failure errs, Success (_,msgs) | |
| Success (_,msgs), Failure errs -> | |
errs @ msgs |> Failure | |
| Failure errs1, Failure errs2 -> | |
errs1 @ errs2 |> Failure | |
/// infix version of apply | |
let (<*>) = applyR | |
/// given a function that transforms a value | |
/// apply it only if the result is on the Success branch | |
let liftR f result = | |
let f' = f |> succeed | |
applyR f' result | |
/// given two values wrapped in results apply a function to both | |
let lift2R f result1 result2 = | |
let f' = liftR f result1 | |
applyR f' result2 | |
/// given three values wrapped in results apply a function to all | |
let lift3R f result1 result2 result3 = | |
let f' = lift2R f result1 result2 | |
applyR f' result3 | |
/// given four values wrapped in results apply a function to all | |
let lift4R f result1 result2 result3 result4 = | |
let f' = lift3R f result1 result2 result3 | |
applyR f' result4 | |
/// infix version of liftR | |
let (<!>) = liftR | |
/// synonym for liftR | |
let mapR = liftR | |
/// given an RopResult, call a unit function on the success branch | |
/// and pass thru the result | |
let successTee f result = | |
let fSuccess (x,msgs) = | |
f (x,msgs) | |
Success (x,msgs) | |
let fFailure errs = Failure errs | |
either fSuccess fFailure result | |
/// given an RopResult, call a unit function on the failure branch | |
/// and pass thru the result | |
let failureTee f result = | |
let fSuccess (x,msgs) = Success (x,msgs) | |
let fFailure errs = | |
f errs | |
Failure errs | |
either fSuccess fFailure result | |
/// given an RopResult, map the messages to a different error type | |
let mapMessagesR f result = | |
match result with | |
| Success (x,msgs) -> | |
let msgs' = List.map f msgs | |
Success (x, msgs') | |
| Failure errors -> | |
let errors' = List.map f errors | |
Failure errors' | |
/// given an RopResult, in the success case, return the value. | |
/// In the failure case, determine the value to return by | |
/// applying a function to the errors in the failure case | |
let valueOrDefault f result = | |
match result with | |
| Success (x,_) -> x | |
| Failure errors -> f errors | |
/// lift an option to a RopResult. | |
/// Return Success if Some | |
/// or the given message if None | |
let failIfNone message = function | |
| Some x -> succeed x | |
| None -> fail message | |
/// given an RopResult option, return it | |
/// or the given message if None | |
let failIfNoneR message = function | |
| Some rop -> rop | |
| None -> fail message | |
(* | |
Building blocks for domain model | |
*) | |
module ValueTypes = | |
type StringError = | |
| Missing | |
| MustNotBeShorterThan of int | |
| MustNotBeLongerThan of int | |
| MustBeBetween of int * int | |
| DoesNotMatchPattern of string | |
type AgeError = | |
| TooOld | |
| TooYoung | |
module String100 = | |
type T = String100 of string | |
let create (s: string) = | |
match s with | |
| null -> fail StringError.Missing | |
| _ when s.Length = 0 -> fail StringError.Missing | |
| _ when s.Length > 100 -> fail (MustNotBeLongerThan 100) | |
| _ -> succeed (String100 s) | |
let apply f (String100 s) = f s | |
let value (String100 s) = s | |
module BirthDate = | |
type T = BirthDate of DateTime | |
let create (d: DateTime) = | |
match d with | |
| _ when d.Year < 1910 -> fail AgeError.TooOld | |
| _ when d.Year >= 2010 -> fail AgeError.TooYoung | |
| _ -> succeed (BirthDate d) | |
let apply f (BirthDate d) = f d | |
let value (BirthDate d) = d | |
(* | |
Domain model | |
*) | |
module Domain = | |
open ValueTypes | |
type Person = | |
{ FirstName: String100.T | |
LastName: String100.T | |
BirthDate: BirthDate.T } | |
let createPerson f l b = | |
{ FirstName = f | |
LastName = l | |
BirthDate = b } | |
(* | |
DTOs | |
Meant to be interoperable with outside world hence '*DTO' are nullable classes with nullable properties | |
*) | |
module DTO = | |
open System | |
open Domain | |
open ValueTypes | |
open ROP | |
module PersonDTO = | |
[<AllowNullLiteralAttribute>] | |
type Person() = | |
member val FirstName : string = null with get, set | |
member val LastName : string = null with get, set | |
member val BirthDate : DateTime = DateTime.MinValue with get, set | |
type PersonSerializationError = | |
| PersonIsRequired | |
| FirstNameNotValid of StringError | |
| LastNameNotValid of StringError | |
| BirthDateNotValid of AgeError | |
let fromDomain (person:Domain.Person) = | |
let dto = Person() | |
dto.FirstName <- person.FirstName |> String100.value | |
dto.LastName <- person.LastName |> String100.value | |
dto.BirthDate <- person.BirthDate |> BirthDate.value | |
dto | |
let toDomain (dto: Person) = | |
if isNull dto then fail PersonIsRequired | |
else | |
let firstOrError = | |
match (dto.FirstName |> String100.create) with | |
| (String100.T s) -> succed s | |
| e -> fail (FirstNameNotValid e) | |
let lastOrError = | |
match (dto.LastName |> String100.create) with | |
| (String100.T s) -> succed s | |
| e -> fail (LastNameNotValid e) | |
let bdateOrError = | |
match (dto.BirthDate |> BirthDate.create) with | |
| (BirthDate.T d) -> succed d | |
| e -> fail (BirthDateNotValid e) | |
createPerson | |
<!> firstOrError | |
<*> lastOrError | |
<*> bdateOrError |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment