Skip to content

Commit 56b5a3b

Browse files
[fix]: Opted for DU over record type to prevent illegal states.
what changed?: opt in for a DU instead of a record type for Optimistic helper type why?: make illegal states unrepresentable effect?: n/a
1 parent d4386c9 commit 56b5a3b

File tree

2 files changed

+102
-78
lines changed

2 files changed

+102
-78
lines changed

src/SAFE.Client/SAFE.fs

Lines changed: 32 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -188,50 +188,44 @@ module RemoteData =
188188
let startLoading (remote: RemoteData<'T>) = remote.StartLoading
189189

190190
///A type which represents optimistic updates.
191-
type Optimistic<'T> = {
192-
/// The previous value, if any
193-
Prev: 'T option
194-
/// The current value, if any
195-
Value: 'T option
196-
}with
197-
198-
/// Updates the current value, shifting the existing current value to previous.
199-
member this.Update value =
200-
{
201-
Value = Some value
202-
Prev = this.Value
203-
}
204-
205-
/// Rolls back to the previous value, discarding the current one.
206-
member this.Rollback () =
207-
{
208-
Value = this.Prev
209-
Prev = None
210-
}
211-
212-
/// Maps the underlying optimistic value, when it exists, into another shape.
213-
member this.Map (f: 'T -> 'U) =
214-
{
215-
Value = Option.map f this.Value
216-
Prev = Option.map f this.Prev
217-
}
218-
219-
/// Binds both current and previous values using the provided function
220-
member this.Bind (f: 'T -> Optimistic<'U>) =
221-
match this.Value with
222-
| Some v -> f v // Just use the result directly
223-
| None -> { Value = None; Prev = None }
224-
225-
/// Returns the current value as an option
226-
member this.AsOption = this.Value
191+
type Optimistic<'T> =
192+
| NonExistant
193+
| Exists of value:'T * prev:'T option
194+
with
195+
/// Retrieves the current value
196+
member this.Value =
197+
match this with
198+
| NonExistant -> None
199+
| Exists (v, pv) -> Some v
200+
201+
/// Updates the current value, shifting the existing current value to previous.
202+
member this.Update (value: 'T) =
203+
match this with
204+
| NonExistant -> NonExistant
205+
| Exists (v, pv) -> Exists (value, Some v)
206+
207+
/// Rolls back to the previous value, discarding the current one.
208+
member this.Rollback () =
209+
match this with
210+
| NonExistant -> NonExistant
211+
| Exists (_, Some pv) -> Exists (pv , None)
212+
| Exists (_, None) -> NonExistant
213+
214+
/// Maps the underlying optimistic value, when it exists, into another shape.
215+
member this.Map (f: 'T -> 'U) =
216+
match this with
217+
| NonExistant -> NonExistant
218+
| Exists (v, pv) -> Exists (f v, pv |> Option.map f)
227219

228220
/// Module containing functions for working with Optimistic type
229221
module Optimistic =
230222
/// Creates a new Optimistic value with no history
231-
let create value = { Value = Some value; Prev = None }
223+
let create value =
224+
Exists (value, None)
232225

233226
/// Creates an empty Optimistic value
234-
let empty = { Value = None; Prev = None }
227+
let empty =
228+
NonExistant
235229

236230
/// Updates the current value, shifting existing value to previous
237231
let update value (optimistic: Optimistic<'T>) = optimistic.Update value
@@ -241,12 +235,3 @@ module Optimistic =
241235

242236
/// Maps both current and previous values
243237
let map f (optimistic: Optimistic<'T>) = optimistic.Map f
244-
245-
/// Binds both current and previous values
246-
let bind f (optimistic: Optimistic<'T>) = optimistic.Bind f
247-
248-
/// Returns the current value as an option
249-
let asOption (optimistic: Optimistic<'T>) = optimistic.AsOption
250-
251-
/// Returns the previous value as an option
252-
let asPrevOption optimistic = optimistic.Prev

test/SAFE.Client.Tests/Program.fs

Lines changed: 70 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -124,65 +124,104 @@ let remoteData =
124124
| RemoteDataCase.LoadingPopulated -> Loading (Some true)
125125
| RemoteDataCase.Loaded -> Loading (Some true))
126126
]
127-
128127
let optimistic =
129128
testList "Optimistic" [
130129
testList "create" [
131130
testCase "creates new value with no history" <| fun _ ->
132131
let opt = Optimistic.create 42
133-
Expect.equal opt.Value (Some 42) "Current value should be set"
134-
Expect.equal opt.Prev None "Previous value should be None"
132+
match opt with
133+
| Exists (value, prev) ->
134+
Expect.equal value 42 "Current value should be set"
135+
Expect.equal prev None "Previous value should be None"
136+
| NonExistant ->
137+
failtest "Should not be NonExistant"
135138
]
136139

137140
testList "empty" [
138141
testCase "creates empty optimistic value" <| fun _ ->
139142
let opt = Optimistic.empty
140-
Expect.equal opt.Value None "Current value should be None"
141-
Expect.equal opt.Prev None "Previous value should be None"
143+
Expect.equal opt NonExistant "Should be NonExistant"
144+
]
145+
146+
testList "Value property" [
147+
testCase "returns Some for existing value" <| fun _ ->
148+
let opt = Optimistic.create 42
149+
Expect.equal opt.Value (Some 42) "Should return Some with current value"
150+
151+
testCase "returns None for NonExistant" <| fun _ ->
152+
let opt = Optimistic.empty
153+
Expect.equal opt.Value None "Should return None for NonExistant"
142154
]
143155

144156
testList "update" [
145157
testCase "updates value and shifts previous" <| fun _ ->
146158
let opt = Optimistic.create 42
147159
let updated = opt.Update 84
148-
Expect.equal updated.Value (Some 84) "Current value should be updated"
149-
Expect.equal updated.Prev (Some 42) "Previous value should be old current"
160+
match updated with
161+
| Exists (value, prev) ->
162+
Expect.equal value 84 "Current value should be updated"
163+
Expect.equal prev (Some 42) "Previous value should be old current"
164+
| NonExistant ->
165+
failtest "Should not be NonExistant"
166+
167+
testCase "update on NonExistant remains NonExistant" <| fun _ ->
168+
let opt = Optimistic.empty
169+
let updated = opt.Update 42
170+
Expect.equal updated NonExistant "Should remain NonExistant"
150171
]
151172

152173
testList "rollback" [
153174
testCase "rolls back to previous value" <| fun _ ->
154-
let opt = Optimistic.create 42 |> Optimistic.update 84
175+
let opt = Optimistic.create 42 |> fun o -> o.Update 84
176+
let rolled = opt.Rollback()
177+
match rolled with
178+
| Exists (value, prev) ->
179+
Expect.equal value 42 "Current value should be previous"
180+
Expect.equal prev None "Previous value should be None"
181+
| NonExistant ->
182+
failtest "Should not be NonExistant"
183+
184+
testCase "rollback on NonExistant remains NonExistant" <| fun _ ->
185+
let opt = Optimistic.empty
155186
let rolled = opt.Rollback()
156-
Expect.equal rolled.Value (Some 42) "Current value should be previous"
157-
Expect.equal rolled.Prev None "Previous value should be cleared"
187+
Expect.equal rolled NonExistant "Should remain NonExistant"
158188
]
159189

160190
testList "map" [
161-
testCase "maps both values" <| fun _ ->
162-
let opt = { Value = Some 42; Prev = Some 21 }
191+
testCase "maps both current and previous values" <| fun _ ->
192+
let opt = Optimistic.create 42 |> fun o -> o.Update 84
163193
let mapped = opt.Map string
164-
Expect.equal mapped.Value (Some "42") "Current value should be mapped"
165-
Expect.equal mapped.Prev (Some "21") "Previous value should be mapped"
166-
]
167-
168-
testList "bind" [
169-
testCase "binds value with history" <| fun _ ->
170-
let opt = { Value = Some 42; Prev = Some 21 }
171-
let bound = opt.Bind (fun x -> { Value = Some (string x); Prev = None})
172-
Expect.equal bound.Value (Some "42") "Current value should be bound"
173-
Expect.equal bound.Prev (None) "Previous value should be bound"
194+
match mapped with
195+
| Exists (value, prev) ->
196+
Expect.equal value "84" "Current value should be mapped"
197+
Expect.equal prev (Some "42") "Previous value should be mapped"
198+
| NonExistant ->
199+
failtest "Should not be NonExistant"
200+
201+
testCase "map on NonExistant remains NonExistant" <| fun _ ->
202+
let opt = Optimistic.empty
203+
let mapped = opt.Map string
204+
Expect.equal mapped NonExistant "Should remain NonExistant"
174205
]
175206

176-
testList "asOption" [
177-
testCase "returns current value as option" <| fun _ ->
207+
testList "module functions" [
208+
testCase "update function matches member" <| fun _ ->
178209
let opt = Optimistic.create 42
179-
Expect.equal (Optimistic.asOption opt) (Some 42) "Should return current value"
180-
]
181-
182-
testList "asPrevOption" [
183-
testCase "returns previous value as option" <| fun _ ->
184-
let opt = Optimistic.create 42 |> Optimistic.update 84
185-
Expect.equal (Optimistic.asPrevOption opt) (Some 42) "Should return previous value"
210+
let memberUpdate = opt.Update 84
211+
let moduleUpdate = Optimistic.update 84 opt
212+
Expect.equal moduleUpdate memberUpdate "Module update should match member update"
213+
214+
testCase "rollback function matches member" <| fun _ ->
215+
let opt = Optimistic.create 42 |> fun o -> o.Update 84
216+
let memberRollback = opt.Rollback()
217+
let moduleRollback = Optimistic.rollback opt
218+
Expect.equal moduleRollback memberRollback "Module rollback should match member rollback"
219+
220+
testCase "map function matches member" <| fun _ ->
221+
let opt = Optimistic.create 42
222+
let memberMap = opt.Map string
223+
let moduleMap = Optimistic.map string opt
224+
Expect.equal moduleMap memberMap "Module map should match member map"
186225
]
187226
]
188227

0 commit comments

Comments
 (0)