Created
June 20, 2014 17:24
-
-
Save mausch/a8012001888e1427c2f1 to your computer and use it in GitHub Desktop.
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
#I @"g:\prg\Fleece\Fleece\bin\Release\" | |
#r @"FsControl.Core.dll" | |
#r @"FSharpPlus.dll" | |
#r @"System.Json.dll" | |
#r @"ReadOnlyCollectionsInterfaces.dll" | |
#r @"ReadOnlyCollectionsExtensions.dll" | |
#r @"Fleece.dll" | |
open System | |
open System.Collections.Generic | |
open System.Json | |
open Fleece | |
open Fleece.Operators | |
open FSharpPlus | |
open System.Globalization | |
let timeSpanJSONFormat = "d\.hh\:mm\:ss" | |
let timeSpanToJSON (x: TimeSpan) = | |
let s = x.ToString(timeSpanJSONFormat, CultureInfo.InvariantCulture) | |
JString s | |
let jsonToTimeSpan = | |
function | |
| JString s -> | |
if s = null | |
then Failure "Expected TimeSpan, got null" | |
else match TimeSpan.TryParseExact(s, timeSpanJSONFormat, CultureInfo.InvariantCulture) with | |
| true,ts -> Success ts | |
| _ -> failwithf "Invalid TimeSpan %s" s | |
| x -> Failure (sprintf "Expected JString, found %A" x) | |
(* | |
The following doesn't work because it's an "orphan instance" ( http://www.haskell.org/haskellwiki/Orphan_instance ) | |
TimeSpan is defined in mscorlib.dll, FromJSONClass is defined in Fleece.dll, and this extension is defined in a third library. | |
This breaks typeclass coherence and is usually considered a bad idea among haskellers ( http://stackoverflow.com/a/3079748/21239 ). | |
In Haskell you get a warning, and this encoding of typeclasses in F# doesn't even allow orphans. | |
Moreover, Fleece probably shouldn't include canonical instances for TimeSpan in particular as there are many non-standardized ways to encode this, e.g.: | |
* d.hh:mm:ss as you need | |
* ticks as a number | |
* ISO 8601 ( http://en.wikipedia.org/wiki/ISO_8601#Time_intervals ) | |
So it's best to be explicit about the encoding you need for each case. | |
*) | |
//type FromJSONClass with | |
// static member ToJSON(x: TimeSpan) = timeSpanToJSON x | |
// static member FromJSON(_: TimeSpan) = jsonToTimeSpan | |
// | |
//toJSON (TimeSpan(1,2,30)) | |
// Solution 1: simply use timeSpanToJSON / jsonToTimeSpan in the serialization of your types instead of trying to make TimeSpan an instance of ToJSON/FromJSON | |
module Solution1 = | |
type MyEvent = { | |
Id: int | |
Start: DateTime | |
Duration: TimeSpan | |
} | |
type MyEvent with | |
static member ToJSON(x: MyEvent) = | |
jobj [ | |
"id" .= x.Id | |
"start" .= x.Start | |
"duration", timeSpanToJSON x.Duration | |
] | |
static member FromJSON(_: MyEvent) = | |
function | |
| JObject s -> | |
monad { | |
let! id = s .@ "id" | |
let! start = s .@ "start" | |
let! duration = jsonToTimeSpan s.["duration"] | |
return { | |
Id = id | |
Start = start | |
Duration = duration | |
} | |
} | |
| x -> Failure (sprintf "Expected object, found %A" x) | |
// Solution 2: use the equivalent of a Haskell newtype wrapper, e.g. in F# a single-case discriminated union. | |
// Since the type definition is now under your control, there are no orphan issues. | |
// However your data structures will have to use this type instead of TimeSpan. | |
module Solution2 = | |
type TimeSpanJSON = TimeSpanJSON of TimeSpan | |
type TimeSpanJSON with | |
static member ToJSON(TimeSpanJSON x) = timeSpanToJSON x | |
static member FromJSON(_: TimeSpanJSON) = jsonToTimeSpan >> map TimeSpanJSON | |
type MyEvent = { | |
Id: int | |
Start: DateTime | |
Duration: Solution2.TimeSpanJSON | |
} | |
type MyEvent with | |
static member ToJSON(x: MyEvent) = | |
jobj [ | |
"id" .= x.Id | |
"start" .= x.Start | |
"duration" .= x.Duration | |
] | |
static member FromJSON(_: MyEvent) = | |
function | |
| JObject s -> | |
monad { | |
let! id = s .@ "id" | |
let! start = s .@ "start" | |
let! duration = s .@ "duration" | |
return { | |
Id = id | |
Start = start | |
Duration = duration | |
} | |
} | |
| x -> Failure (sprintf "Expected object, found %A" x) | |
// Solution 3: redefine/shadow toJSON/fromJSON for your project, including the instances you want: | |
module Solution3 = | |
type MyJSONClass = MyJSONClass with | |
static member ToJSON (x: TimeSpan) = timeSpanToJSON x | |
static member FromJSON(_: TimeSpan) = jsonToTimeSpan | |
let inline iToJSON (a: ^a, b: ^b, z: ^z) = ((^a or ^b or ^z) : (static member ToJSON: ^z -> JsonValue) z) | |
let inline toJSON (x: 'a) : JsonValue = iToJSON (ToJSONClass, MyJSONClass, x) // uses the instances from ToJSONClass (defined in Fleece) plus the ones defined in MyJSONClass | |
let inline (.=) key value = key, toJSON value | |
let inline iFromJSON (a: ^a, b: ^b, z: ^z ) = ((^a or ^b or ^z) : (static member FromJSON: ^z -> (JsonValue -> ^z ParseResult)) z) | |
let inline fromJSON (x: JsonValue) : 'a ParseResult = iFromJSON (FromJSONClass, MyJSONClass, Unchecked.defaultof<'a>) x | |
let inline (.@) (o: IReadOnlyDictionary<string, JsonValue>) key = | |
match o.TryGetValue key with | |
| true, value -> fromJSON value | |
| _ -> Failure ("Key '" + key + "' not found in " + JObject(o).ToString()) | |
type Solution1.MyEvent with | |
static member ToJSON (x: Solution1.MyEvent) = | |
jobj [ | |
"id" .= x.Id | |
"start" .= x.Start | |
"duration" .= x.Duration | |
] | |
static member FromJSON (_: Solution1.MyEvent) = | |
function | |
| JObject s -> | |
monad { | |
let! id = s .@ "id" | |
let! start = s .@ "start" | |
let! duration = s .@ "duration" | |
return { | |
Id = id | |
Start = start | |
Duration = duration | |
} | |
} | |
| x -> Failure (sprintf "Expected object, found %A" x) | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment