Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle CartId, SkuId conversions #33

Merged
merged 1 commit into from
Oct 4, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 11 additions & 8 deletions src/Foldunk.Serialization/Serialization.fs
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,11 @@ module Converters =

/// Paralells F# behavior wrt how it generates a DU's underlyiong .NET Type
let inline isInlinedIntoUnionItem (t : Type) =
t = typeof<string>
t = typeof<string>
|| t.IsValueType
|| (t.IsGenericType // None :> obj / Nullable()
|| (t.IsGenericType
&& (typedefof<Option<_>> = t.GetGenericTypeDefinition()
|| typedefof<Nullable<_>> = t.GetGenericTypeDefinition()))
|| t.GetGenericTypeDefinition().IsValueType)) // Nullable<T>

/// For Some 1 generates "1", for None generates "null"
type OptionConverter() =
Expand Down Expand Up @@ -136,6 +136,8 @@ module Converters =
if value = null then FSharpValue.MakeUnion(cases.[0], Array.empty)
else FSharpValue.MakeUnion(cases.[1], [|value|])

let typeHasJsonConverterAttribute = memoize (fun (t : Type) -> t.IsDefined(typeof<JsonConverterAttribute>))

(* Serializes a discriminated union case with a single field that is a record by flattening the
record fields to the same level as the discriminator *)
type UnionConverter private (discriminator : string, ?catchAllCase) =
Expand Down Expand Up @@ -188,7 +190,7 @@ module Converters =
if token.Type <> JTokenType.Object then raise <| new FormatException(sprintf "Expected object reading JSON, got %O" token.Type)
let obj = token :?> JObject

let caseName = obj.Item(discriminator) |> string
let caseName = obj.[discriminator] |> string
let foundTag = cases |> Array.tryFindIndex (fun case -> case.Name = caseName)
let tag =
match foundTag, catchAllCase with
Expand All @@ -202,7 +204,9 @@ module Converters =
let fieldInfos = case.GetFields()

let fieldValues =
if fieldInfos.Length = 1 && not (Union.isInlinedIntoUnionItem fieldInfos.[0].PropertyType) then
if fieldInfos.Length = 1
&& not (Union.isInlinedIntoUnionItem fieldInfos.[0].PropertyType)
&& not (typeHasJsonConverterAttribute fieldInfos.[0].PropertyType) then
let fieldInfo = fieldInfos.[0]
// strip out the discriminator property as we're preparing args for a constructor
let obj' =
Expand All @@ -217,9 +221,8 @@ module Converters =
else
let simpleFieldValue (fieldInfo: PropertyInfo) =
let itemValue = obj.[fieldInfo.Name]
let fieldType = fieldInfo.PropertyType
if itemValue = null && Union.isInlinedIntoUnionItem fieldType then null
else itemValue.ToObject(fieldType, jsonSerializer)
if itemValue = null then null
else itemValue.ToObject(fieldInfo.PropertyType, jsonSerializer)
fieldInfos |> Array.map simpleFieldValue

union.caseConstructor.[tag] fieldValues
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@
<ItemGroup>
<None Include="paket.references" />
<Content Include="App.config" />
<Compile Include="..\..\Samples\Store\Domain\Infrastructure.fs">
<Link>Infrastructure.fs</Link>
</Compile>
<Compile Include="SerializationTests.fs" />
</ItemGroup>
<ItemGroup>
Expand Down
135 changes: 59 additions & 76 deletions tests/Foldunk.Serialization.Tests/SerializationTests.fs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
module Foldunk.Serialization.Tests

open Domain
open Foldunk.Serialization
open FsCheck
open Newtonsoft.Json
open Swensen.Unquote.Assertions
open System
open System.IO
open System.Text.RegularExpressions
open Xunit
open global.Xunit

let normalizeJsonString (json : string) =
let str1 = Regex.Replace(json, @"{\s*}", "{}")
Expand Down Expand Up @@ -43,57 +45,48 @@ type TestDU =
| CaseM of a: int option
| CaseN of a: int * b: int option
| CaseO of a: int option * b: int option
| CaseP of CartId
| CaseQ of SkuId
| CaseR of a: CartId
| CaseS of a: SkuId
| CaseT of a: SkuId option * b: CartId

// no camel case, because I want to test "Item" as a record property
let settings = Settings.CreateDefault(camelCase = false)

[<Fact>]
let ``UnionConverter produces expected output`` () =
let serialize (x : obj) = JsonConvert.SerializeObject(box x, settings)
let a = CaseA {test = "hi"}
let aJson = JsonConvert.SerializeObject(a, settings)

test <@ """{"case":"CaseA","test":"hi"}""" = aJson @>
test <@ """{"case":"CaseA","test":"hi"}""" = serialize a @>

let b = CaseB
let bJson = JsonConvert.SerializeObject(b, settings)

test <@ """{"case":"CaseB"}""" = bJson @>
test <@ """{"case":"CaseB"}""" = serialize b @>

let c = CaseC "hi"
let cJson = JsonConvert.SerializeObject(c, settings)

test <@ """{"case":"CaseC","Item":"hi"}""" = cJson @>
test <@ """{"case":"CaseC","Item":"hi"}""" = serialize c @>

let d = CaseD "hi"
let dJson = JsonConvert.SerializeObject(d, settings)

test <@ """{"case":"CaseD","a":"hi"}""" = dJson @>
test <@ """{"case":"CaseD","a":"hi"}""" = serialize d @>

let e = CaseE ("hi", 0)
let eJson = JsonConvert.SerializeObject(e, settings)

test <@ """{"case":"CaseE","Item1":"hi","Item2":0}""" = eJson @>
test <@ """{"case":"CaseE","Item1":"hi","Item2":0}""" = serialize e @>

let f = CaseF ("hi", 0)
let fJson = JsonConvert.SerializeObject(f, settings)

test <@ """{"case":"CaseF","a":"hi","b":0}""" = fJson @>
test <@ """{"case":"CaseF","a":"hi","b":0}""" = serialize f @>

let g = CaseG {Item = "hi"}
let gJson = JsonConvert.SerializeObject(g, settings)

test <@ """{"case":"CaseG","Item":"hi"}""" = gJson @>
test <@ """{"case":"CaseG","Item":"hi"}""" = serialize g @>

// this may not be expected, but I don't itend changing it
let h = CaseH {test = "hi"}
let hJson = JsonConvert.SerializeObject(h, settings)

test <@ """{"case":"CaseH","test":"hi"}""" = hJson @>
test <@ """{"case":"CaseH","test":"hi"}""" = serialize h @>

let i = CaseI ({test = "hi"}, "bye")
let iJson = JsonConvert.SerializeObject(i, settings)
test <@ """{"case":"CaseI","a":{"test":"hi"},"b":"bye"}""" = serialize i @>

test <@ "{\"case\":\"CaseI\",\"a\":{\"test\":\"hi\"},\"b\":\"bye\"}" = iJson @>
let p = CaseP (CartId.Parse "0000000000000000948d503fcfc20f17")
test <@ """{"case":"CaseP","Item":"0000000000000000948d503fcfc20f17"}""" = serialize p @>

let requiredSettingsToHandleOptionalFields =
// NB this is me documenting current behavior - ideally optionality wou
Expand All @@ -103,72 +96,49 @@ let requiredSettingsToHandleOptionalFields =

[<Fact>]
let ``UnionConverter deserializes properly`` () =
let aJson = """{"case":"CaseA"}"""
let a = JsonConvert.DeserializeObject<TestDU>(aJson, settings)
test <@ CaseA {test = null} = a @>
let deserialize json = JsonConvert.DeserializeObject<TestDU>(json, settings)
test <@ CaseA {test = null} = deserialize """{"case":"CaseA"}""" @>
test <@ CaseA {test = "hi"} = deserialize """{"case":"CaseA","test":"hi"}""" @>
test <@ CaseA {test = "hi"} = deserialize """{"case":"CaseA","test":"hi","extraField":"hello"}""" @>

let aJson = """{"case":"CaseA","test":"hi"}"""
let a = JsonConvert.DeserializeObject<TestDU>(aJson, settings)
test <@ CaseA {test = "hi"} = a @>
test <@ CaseB = deserialize """{"case":"CaseB"}""" @>

let aJson = """{"case":"CaseA","test":"hi","extraField":"hello"}"""
let a = JsonConvert.DeserializeObject<TestDU>(aJson, settings)
test <@ CaseA {test = "hi"} = a @>
test <@ CaseC "hi" = deserialize """{"case":"CaseC","Item":"hi"}""" @>

let bJson = """{"case":"CaseB"}"""
let b = JsonConvert.DeserializeObject<TestDU>(bJson, settings)
test <@ CaseB = b @>
test <@ CaseD "hi" = deserialize """{"case":"CaseD","a":"hi"}""" @>

let cJson = """{"case":"CaseC","Item":"hi"}"""
let c = JsonConvert.DeserializeObject<TestDU>(cJson, settings)
test <@ CaseC "hi" = c @>
test <@ CaseE ("hi", 0) = deserialize """{"case":"CaseE","Item1":"hi","Item2":0}""" @>
test <@ CaseE (null, 0) = deserialize """{"case":"CaseE","Item3":"hi","Item4":0}""" @>

let dJson = """{"case":"CaseD","a":"hi"}"""
let d = JsonConvert.DeserializeObject<TestDU>(dJson, settings)
test <@ CaseD "hi" = d @>

let eJson = """{"case":"CaseE","Item1":"hi","Item2":0}"""
let e = JsonConvert.DeserializeObject<TestDU>(eJson, settings)
test <@ CaseE ("hi", 0) = e @>

let eJson = """{"case":"CaseE","Item3":"hi","Item4":0}"""
let e = JsonConvert.DeserializeObject<TestDU>(eJson, settings)
test <@ CaseE (null, 0) = e @>

let fJson = """{"case":"CaseF","a":"hi","b":0}"""
let f = JsonConvert.DeserializeObject<TestDU>(fJson, settings)
test <@ CaseF ("hi", 0) = f @>
test <@ CaseF ("hi", 0) = deserialize """{"case":"CaseF","a":"hi","b":0}""" @>

let gJson = """{"case":"CaseG","Item":"hi"}"""
let g = JsonConvert.DeserializeObject<TestDU>(gJson, settings)
test <@ CaseG {Item = "hi"} = g @>
test <@ CaseG {Item = "hi"} = deserialize """{"case":"CaseG","Item":"hi"}""" @>

let hJson = """{"case":"CaseH","test":"hi"}"""
let h = JsonConvert.DeserializeObject<TestDU>(hJson, settings)
test <@ CaseH {test = "hi"} = h @>
test <@ CaseH {test = "hi"} = deserialize """{"case":"CaseH","test":"hi"}""" @>

let iJson = """{"case":"CaseI","a":{"test":"hi"},"b":"bye"}"""
let i = JsonConvert.DeserializeObject<TestDU>(iJson, settings)
test <@ CaseI ({test = "hi"}, "bye") = i @>
test <@ CaseI ({test = "hi"}, "bye") = deserialize """{"case":"CaseI","a":{"test":"hi"},"b":"bye"}""" @>

test <@ CaseJ (Nullable 1) = JsonConvert.DeserializeObject<TestDU>("""{"case":"CaseJ","a":1}""", settings) @>
test <@ CaseK (1, Nullable 2) = JsonConvert.DeserializeObject<TestDU>("""{"case":"CaseK", "a":1, "b":2 }""", settings) @>
test <@ CaseL (Nullable 1, Nullable 2) = JsonConvert.DeserializeObject<TestDU>("""{"case":"CaseL", "a": 1, "b": 2 }""", settings) @>
test <@ CaseJ (Nullable 1) = deserialize """{"case":"CaseJ","a":1}""" @>
test <@ CaseK (1, Nullable 2) = deserialize """{"case":"CaseK", "a":1, "b":2 }""" @>
test <@ CaseL (Nullable 1, Nullable 2) = deserialize """{"case":"CaseL", "a": 1, "b": 2 }""" @>

let deserialzeCustom s = JsonConvert.DeserializeObject<TestDU>(s, requiredSettingsToHandleOptionalFields)
test <@ CaseM (Some 1) = deserialzeCustom """{"case":"CaseM","a":1}""" @>
test <@ CaseN (1, Some 2) = deserialzeCustom """{"case":"CaseN", "a":1, "b":2 }""" @>
test <@ CaseO (Some 1, Some 2) = deserialzeCustom """{"case":"CaseO", "a": 1, "b": 2 }""" @>

test <@ CaseP (CartId.Parse "0000000000000000948d503fcfc20f17") = deserialize """{"case":"CaseP","Item":"0000000000000000948d503fcfc20f17"}""" @>

[<Fact>]
let ``UnionConverter handles missing fields`` () =
test <@ CaseJ (Nullable<int>()) = JsonConvert.DeserializeObject<TestDU>("""{"case":"CaseJ"}""", settings) @>
test <@ CaseK (1, (Nullable<int>())) = JsonConvert.DeserializeObject<TestDU>("""{"case":"CaseK","a":1}""", settings) @>
test <@ CaseL ((Nullable<int>()), (Nullable<int>())) = JsonConvert.DeserializeObject<TestDU>("""{"case":"CaseL"}""", settings) @>
let deserialize json = JsonConvert.DeserializeObject<TestDU>(json, settings)
test <@ CaseJ (Nullable<int>()) = deserialize """{"case":"CaseJ"}""" @>
test <@ CaseK (1, (Nullable<int>())) = deserialize """{"case":"CaseK","a":1}""" @>
test <@ CaseL ((Nullable<int>()), (Nullable<int>())) = deserialize """{"case":"CaseL"}""" @>

test <@ CaseM None = JsonConvert.DeserializeObject<TestDU>("""{"case":"CaseM"}""", settings) @>
test <@ CaseN (1, None) = JsonConvert.DeserializeObject<TestDU>("""{"case":"CaseN","a":1}""", settings) @>
test <@ CaseO (None, None) = JsonConvert.DeserializeObject<TestDU>("""{"case":"CaseO"}""", settings) @>
test <@ CaseM None = deserialize """{"case":"CaseM"}""" @>
test <@ CaseN (1, None) = deserialize """{"case":"CaseN","a":1}""" @>
test <@ CaseO (None, None) = deserialize """{"case":"CaseO"}""" @>

let (|Q|) (s : string) = Newtonsoft.Json.JsonConvert.SerializeObject s

Expand Down Expand Up @@ -210,8 +180,21 @@ let render = function
| CaseO (None,b) -> sprintf """{"case":"CaseO","b":%d}""" b.Value
| CaseO (a,None) -> sprintf """{"case":"CaseO","a":%d}""" a.Value
| CaseO (Some a,Some b) -> sprintf """{"case":"CaseO","a":%d,"b":%d}""" a b
| CaseP id -> sprintf """{"case":"CaseP","Item":"%s"}""" id.Value
| CaseQ id -> sprintf """{"case":"CaseQ","Item":"%s"}""" id.Value
| CaseR id -> sprintf """{"case":"CaseR","a":"%s"}""" id.Value
| CaseS id -> sprintf """{"case":"CaseS","a":"%s"}""" id.Value
| CaseT (None, x) -> sprintf """{"case":"CaseT","b":"%s"}""" x.Value
| CaseT (Some x, y) -> sprintf """{"case":"CaseT","a":"%s","b":"%s"}""" x.Value y.Value

type FsCheckGenerators =
static member CartId = Arb.generate |> Gen.map CartId |> Arb.fromGen
static member SkuId = Arb.generate |> Gen.map SkuId |> Arb.fromGen

type DomainPropertyAttribute() =
inherit FsCheck.Xunit.PropertyAttribute(QuietOnSuccess = true, Arbitrary=[| typeof<FsCheckGenerators> |])

[<FsCheck.Xunit.Property(MaxTest=1000)>]
[<DomainPropertyAttribute(MaxTest=1000)>]
let ``UnionConverter roundtrip property test`` (x: TestDU) =
let serialized = JsonConvert.SerializeObject(x, requiredSettingsToHandleOptionalFields)
render x =! serialized
Expand Down