diff --git a/src/Client/SAFE.fs b/src/Client/SAFE.fs index 6fdae2e..81dd708 100644 --- a/src/Client/SAFE.fs +++ b/src/Client/SAFE.fs @@ -185,4 +185,53 @@ module RemoteData = /// `Loaded x -> Loading x`; /// `NotStarted -> Loading None`; /// `Loading x -> Loading x`; - let startLoading (remote: RemoteData<'T>) = remote.StartLoading \ No newline at end of file + let startLoading (remote: RemoteData<'T>) = remote.StartLoading + +///A type which represents optimistic updates. +type Optimistic<'T> = + | NonExistant + | Exists of value:'T * prev:'T option + with + /// Retrieves the current value + member this.Value = + match this with + | NonExistant -> None + | Exists (v, pv) -> Some v + + /// Updates the current value, shifting the existing current value to previous. + member this.Update (value: 'T) = + match this with + | NonExistant -> NonExistant + | Exists (v, pv) -> Exists (value, Some v) + + /// Rolls back to the previous value, discarding the current one. + member this.Rollback () = + match this with + | NonExistant -> NonExistant + | Exists (_, Some pv) -> Exists (pv , None) + | Exists (_, None) -> NonExistant + + /// Maps the underlying optimistic value, when it exists, into another shape. + member this.Map (f: 'T -> 'U) = + match this with + | NonExistant -> NonExistant + | Exists (v, pv) -> Exists (f v, pv |> Option.map f) + +/// Module containing functions for working with Optimistic type +module Optimistic = + /// Creates a new Optimistic value with no history + let create value = + Exists (value, None) + + /// Creates an empty Optimistic value + let empty = + NonExistant + + /// Updates the current value, shifting existing value to previous + let update value (optimistic: Optimistic<'T>) = optimistic.Update value + + /// Rolls back to the previous value + let rollback (optimistic: Optimistic<'T>) = optimistic.Rollback() + + /// Maps both current and previous values + let map f (optimistic: Optimistic<'T>) = optimistic.Map f \ No newline at end of file diff --git a/test/Client/Program.fs b/test/Client/Program.fs index 3d04034..f3f636a 100644 --- a/test/Client/Program.fs +++ b/test/Client/Program.fs @@ -1,4 +1,4 @@ -module Client.Tests +module Client.Tests open Fable.Mocha open SAFE @@ -124,6 +124,112 @@ let remoteData = | RemoteDataCase.LoadingPopulated -> Loading (Some true) | RemoteDataCase.Loaded -> Loading (Some true)) ] +let optimistic = + testList "Optimistic" [ + testList "create" [ + testCase "creates new value with no history" <| fun _ -> + let opt = Optimistic.create 42 + match opt with + | Exists (value, prev) -> + Expect.equal value 42 "Current value should be set" + Expect.equal prev None "Previous value should be None" + | NonExistant -> + failtest "Should not be NonExistant" + ] + + testList "empty" [ + testCase "creates empty optimistic value" <| fun _ -> + let opt = Optimistic.empty + Expect.equal opt NonExistant "Should be NonExistant" + ] + + testList "Value property" [ + testCase "returns Some for existing value" <| fun _ -> + let opt = Optimistic.create 42 + Expect.equal opt.Value (Some 42) "Should return Some with current value" + + testCase "returns None for NonExistant" <| fun _ -> + let opt = Optimistic.empty + Expect.equal opt.Value None "Should return None for NonExistant" + ] + + testList "update" [ + testCase "updates value and shifts previous" <| fun _ -> + let opt = Optimistic.create 42 + let updated = opt.Update 84 + match updated with + | Exists (value, prev) -> + Expect.equal value 84 "Current value should be updated" + Expect.equal prev (Some 42) "Previous value should be old current" + | NonExistant -> + failtest "Should not be NonExistant" + + testCase "update on NonExistant remains NonExistant" <| fun _ -> + let opt = Optimistic.empty + let updated = opt.Update 42 + Expect.equal updated NonExistant "Should remain NonExistant" + ] + + testList "rollback" [ + testCase "rolls back to previous value" <| fun _ -> + let opt = Optimistic.create 42 |> fun o -> o.Update 84 + let rolled = opt.Rollback() + match rolled with + | Exists (value, prev) -> + Expect.equal value 42 "Current value should be previous" + Expect.equal prev None "Previous value should be None" + | NonExistant -> + failtest "Should not be NonExistant" + + testCase "rollback on NonExistant remains NonExistant" <| fun _ -> + let opt = Optimistic.empty + let rolled = opt.Rollback() + Expect.equal rolled NonExistant "Should remain NonExistant" + ] + + testList "map" [ + testCase "maps both current and previous values" <| fun _ -> + let opt = Optimistic.create 42 |> fun o -> o.Update 84 + let mapped = opt.Map string + match mapped with + | Exists (value, prev) -> + Expect.equal value "84" "Current value should be mapped" + Expect.equal prev (Some "42") "Previous value should be mapped" + | NonExistant -> + failtest "Should not be NonExistant" + + testCase "map on NonExistant remains NonExistant" <| fun _ -> + let opt = Optimistic.empty + let mapped = opt.Map string + Expect.equal mapped NonExistant "Should remain NonExistant" + ] + + testList "module functions" [ + testCase "update function matches member" <| fun _ -> + let opt = Optimistic.create 42 + let memberUpdate = opt.Update 84 + let moduleUpdate = Optimistic.update 84 opt + Expect.equal moduleUpdate memberUpdate "Module update should match member update" + + testCase "rollback function matches member" <| fun _ -> + let opt = Optimistic.create 42 |> fun o -> o.Update 84 + let memberRollback = opt.Rollback() + let moduleRollback = Optimistic.rollback opt + Expect.equal moduleRollback memberRollback "Module rollback should match member rollback" + + testCase "map function matches member" <| fun _ -> + let opt = Optimistic.create 42 + let memberMap = opt.Map string + let moduleMap = Optimistic.map string opt + Expect.equal moduleMap memberMap "Module map should match member map" + ] + ] + +let allTests = + testList "All Tests" [ + remoteData + optimistic + ] [] -let main _ = Mocha.runTests remoteData \ No newline at end of file +let main _ = Mocha.runTests allTests