Last active
December 23, 2024 16:51
-
-
Save isaacabraham/3314300b3758cfca23df30e8dc3ca814 to your computer and use it in GitHub Desktop.
Experimenting with Paul Blasucci's ideas around Faults and Reports for F#, using the existing F# Result type.
This file contains hidden or 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
#if INTERACTIVE | |
#r "nuget: FsToolkit.ErrorHandling" | |
#endif | |
open System.Text.Json | |
open FsToolkit.ErrorHandling | |
/// All Error DUs should implement this to "take part" in generalised fault reporting. | |
[<Interface>] | |
type IFault = | |
abstract Description : string | |
/// An alias over Result that constrains the error type to some implementation of IFault. | |
type Report<'TOk, 'TError when 'TError :> IFault> = Result<'TOk, 'TError> | |
[<AutoOpen>] | |
module Report = | |
let generalise report = | |
report |> Result.mapError (fun error -> error :> IFault) | |
/// Just for fun | |
let (!!) = generalise | |
/// If you want to get to Description without upcasting, use this generalised pattenr | |
let (|GeneralError|GeneralOk|) (x:Report<'Pass, 'TError> when 'TError :> IFault) = | |
match x with | |
| Ok x -> GeneralOk x | |
| Error a -> GeneralError (a :> IFault) | |
// Two example error types that are not unified | |
type ValidationError = | |
| RequiredFieldMissing of string | |
| InvalidField of field:string * error:string | |
interface IFault with | |
member this.Description: string = | |
match this with | |
| RequiredFieldMissing missing -> $"Required field {missing} missing" | |
| InvalidField(field, error) -> $"Invalid field {field}: {error}" | |
type CustomerError = | |
| CustomerNotFound of customerId:int | |
| CustomerAlreadyExists of customerId:int | |
interface IFault with | |
member this.Description: string = | |
match this with | |
| CustomerNotFound customerId -> $"Customer not found: {customerId}" | |
| CustomerAlreadyExists customerId -> $"Customer already exists: {customerId}" | |
// Two example functions using those error types | |
let validateRequest (request : {| Id : string | null |}) : Report<int,_> = result { | |
let! customerId = request.Id |> Option.ofObj |> Result.requireSome (RequiredFieldMissing "ID") | |
return! customerId |> Option.tryParse |> Result.requireSome (InvalidField("ID", "Not a number")) | |
} | |
let loadCustomer customerId : Report<{| CustomerId : int; Name : string |},_> = | |
match customerId with | |
| 123 -> Ok {| CustomerId = 123; Name = "Alice" |} | |
| _ -> Error (CustomerNotFound customerId) | |
/// A top-level function that brings it all together | |
let handleRequest request = | |
let customerId = !! (validateRequest request) // using hardcore operator to go from custom error to IFault... going wild | |
// could also just do this of course! ;) | |
// let customerId = request |> validateRequest |> generalise | |
let customer = customerId |> Result.bind (loadCustomer >> generalise) // using more basicerer function composition to do the same here | |
// Here's the code that wants to treat all errors in the same way | |
match customer with | |
| Error error -> JsonSerializer.Serialize {| Error = error.Description |} | |
| Ok v -> JsonSerializer.Serialize v // HTTP 20x response | |
let x = handleRequest {| Id = "123" |} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment