Skip to content

Instantly share code, notes, and snippets.

@mausch
Created June 20, 2014 17:24
Show Gist options
  • Save mausch/a8012001888e1427c2f1 to your computer and use it in GitHub Desktop.
Save mausch/a8012001888e1427c2f1 to your computer and use it in GitHub Desktop.
#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