module Microsoft.FSharpLu.Json.Tests open Microsoft.VisualStudio.TestTools.UnitTesting open FsCheck open Microsoft.FSharpLu.Json open Newtonsoft.Json open Compact.CamelCaseNoFormatting type WithFields = SomeField of int * int type SimpleDu = Foo | FooBar | Bar type ComplexDu = ComplexDu of WithFields | SimpleDU | AString of string type 'a RecursiveList = RecListLeaf of 'a | RecListCons of 'a RecursiveList type OptionOfBase = int option type OptionOfDu = SimpleDu option type Color = Red | Blue type Shape = Circle of int * int | Rectangle type 'a Tree = Leaf of 'a | Node of 'a Tree * 'a Tree type 'a Test = Case1 | Case2 of int | Case3 of int * string * 'a type MapType = Map type 'a NestedOptions = 'a option option option option type ConsecutiveUppercaseRecord = { BAR : int ; BAZNumber : int } type ConsecutiveUppercaseDu = FOO | FOOWithRecord of ConsecutiveUppercaseRecord type RecordWithDU = { du: ComplexDu } type 'a Wrapper = { WrappedField : 'a } type NestedStructure = { subField : int } type NestedOptionStructure = { field : NestedStructure option } type SomeTupleType = int * string list * int * int64 * OptionOfDu * Color * int Tree /// Test cases for possible ambiguity between option types and other DU or records with a 'Some' field. module SomeAmbiguity = type 'a RecordWithFieldNamedSome = { Some : 'a } type DUWithFieldlessCaseNamedSome = Some of string | Bla type DUWithCaseWithFieldNamedSome = Some | Bla type 'a Ambiguous1 = 'a RecordWithFieldNamedSome option type Ambiguous2 = DUWithFieldlessCaseNamedSome option type Ambiguous3 = DUWithCaseWithFieldNamedSome option let inline defaultSerialize< ^T> (x: ^T) = Compact.serialize< ^T> x let inline reciprocal< ^T when ^T:equality> (serialize: ^T->string) (deserialize: string-> ^T) (x: ^T) = // theoretically one round trip is sufficient; we perform // two round trips here to test for possible side-effects x |> serialize |> deserialize |> serialize |> deserialize = x let inline canBeSerialized< ^T when ^T:equality> (serialize: ^T->string) (deserialize: string-> ^T) (x: ^T) = serialize x |> printfn "%A" let inline areReciprocal< ^T when ^T:equality> (serialize: ^T->string) (deserialize: string-> ^T) (x: ^T) = let s = x |> serialize let sds = s |> deserialize |> serialize Assert.AreEqual(s, sds, sprintf "Inconsistent serialization: 1st call: <%s> 2nd call <%s>. Type %A" s sds (typeof< ^T>)) let sdsd = sds |> deserialize Assert.AreEqual(sdsd, x, sprintf "Did not get the same object back: <%A> gave back <%A> for type %A" x sdsd (typeof< ^T>)) /// Check that given object serializes to the specified Json string (using default serializer) let inline serializedAs json o = let s = defaultSerialize o Assert.AreEqual(json, s, sprintf "Object was not serialized to the expected format") /// Check that deserialization coincides with NewtonSoft's default serializer. /// That is: when the Json is deserializable by both deserializers Union and Default /// they produce the same output object. let inline coincidesWithDefault< ^T when ^T:equality> (x: ^T) = let deserializationMustCoincide json = match Default.tryDeserialize< ^T> json, Compact.tryDeserialize< ^T> json with | Choice2Of2 error1, Choice2Of2 error2-> Assert.IsTrue(true, "Json not parseable by either deserializer: no ambiguity") | Choice1Of2 _, Choice2Of2 error | Choice2Of2 error, Choice1Of2 _ -> Assert.IsTrue(true, "Json parseable by exactly one deserializer: no ambiguity") | Choice1Of2 v1, Choice1Of2 v2 when v1 <> v2 -> Assert.Fail(sprintf "Deserializers do not coincide: %A <> %A" v1 v2) | Choice1Of2 v1, Choice1Of2 v2 -> Assert.IsTrue(true) x |> Default.serialize |> deserializationMustCoincide x |> Compact.serialize |> deserializationMustCoincide /// Check that output format of Default Json.Net serializer can be parsed by /// the BackwardCompatible deserializer let inline backwardCompatibleWithDefault< ^T when ^T:equality> (x: ^T) = let json = x |> Default.serialize let o1 = json |> Default.deserialize : ^T let o2 = json |> BackwardCompatible.deserialize : ^T Assert.AreEqual(o1, o2, sprintf "BackwardCompatible should coincide with Json.Net when deserializing default Json format. %A <> %A" o1 o2) type ReciprocalityCompact () = static member x1 = reciprocal Compact.serialize Compact.deserialize static member x2 = reciprocal Compact.serialize Compact.deserialize static member x3 = reciprocal Compact.serialize Compact.deserialize static member x4 = reciprocal Compact.serialize Compact.deserialize static member x5 = reciprocal Compact.serialize Compact.deserialize static member x6 = reciprocal Compact.serialize Compact.deserialize static member x7 = reciprocal Compact.serialize Compact.deserialize static member x8 = reciprocal Compact.serialize Compact.deserialize static member x9 = reciprocal Compact.serialize Compact.deserialize static member x10 = reciprocal Compact.serialize Compact.deserialize static member x11 = reciprocal Compact.serialize Compact.deserialize static member x12 = reciprocal Compact.serialize Compact.deserialize static member x13 = reciprocal Compact.serialize Compact.deserialize static member x14 = reciprocal Compact.serialize Compact.deserialize static member x15 = reciprocal Compact.serialize Compact.deserialize static member x16 = reciprocal Compact.serialize Compact.deserialize static member x17 = reciprocal Compact.serialize Compact.deserialize static member x18 = reciprocal Compact.serialize Compact.deserialize static member x19 = reciprocal Compact.serialize Compact.deserialize static member x20 = reciprocal> Compact.serialize Compact.deserialize static member x21 = reciprocal> Compact.serialize Compact.deserialize static member x22 = reciprocal Compact.serialize Compact.deserialize static member x23 = reciprocal Compact.serialize Compact.deserialize static member x24 = reciprocal Compact.serialize Compact.deserialize static member x25 = reciprocal Compact.serialize Compact.deserialize static member x26 = reciprocal Compact.serialize Compact.deserialize type ReciprocalityCamelCase () = static member x1 = reciprocal CamelCaseSerializer.serialize CamelCaseSerializer.deserialize static member x2 = reciprocal CamelCaseSerializer.serialize CamelCaseSerializer.deserialize static member x3 = reciprocal CamelCaseSerializer.serialize CamelCaseSerializer.deserialize static member x4 = reciprocal CamelCaseSerializer.serialize CamelCaseSerializer.deserialize static member x5 = reciprocal CamelCaseSerializer.serialize CamelCaseSerializer.deserialize static member x6 = reciprocal CamelCaseSerializer.serialize CamelCaseSerializer.deserialize static member x7 = reciprocal CamelCaseSerializer.serialize CamelCaseSerializer.deserialize static member x8 = reciprocal CamelCaseSerializer.serialize CamelCaseSerializer.deserialize static member x9 = reciprocal CamelCaseSerializer.serialize CamelCaseSerializer.deserialize static member x10 = reciprocal CamelCaseSerializer.serialize CamelCaseSerializer.deserialize static member x11 = reciprocal CamelCaseSerializer.serialize CamelCaseSerializer.deserialize static member x12 = reciprocal CamelCaseSerializer.serialize CamelCaseSerializer.deserialize static member x13 = reciprocal CamelCaseSerializer.serialize CamelCaseSerializer.deserialize static member x14 = reciprocal CamelCaseSerializer.serialize CamelCaseSerializer.deserialize static member x15 = reciprocal CamelCaseSerializer.serialize CamelCaseSerializer.deserialize static member x16 = reciprocal CamelCaseSerializer.serialize CamelCaseSerializer.deserialize static member x17 = reciprocal CamelCaseSerializer.serialize CamelCaseSerializer.deserialize static member x18 = reciprocal CamelCaseSerializer.serialize CamelCaseSerializer.deserialize static member x19 = reciprocal CamelCaseSerializer.serialize CamelCaseSerializer.deserialize static member x20 = reciprocal> CamelCaseSerializer.serialize CamelCaseSerializer.deserialize static member x21 = reciprocal> CamelCaseSerializer.serialize CamelCaseSerializer.deserialize static member x22 = reciprocal CamelCaseSerializer.serialize CamelCaseSerializer.deserialize static member x23 = reciprocal CamelCaseSerializer.serialize CamelCaseSerializer.deserialize static member x24 = reciprocal CamelCaseSerializer.serialize CamelCaseSerializer.deserialize static member x25 = reciprocal CamelCaseSerializer.serialize CamelCaseSerializer.deserialize static member x26 = reciprocal CamelCaseSerializer.serialize CamelCaseSerializer.deserialize type CoincidesWithJsonNetOnDeserialization () = static member x1 = coincidesWithDefault static member x2 = coincidesWithDefault static member x3 = coincidesWithDefault static member x4 = coincidesWithDefault static member x5 = coincidesWithDefault static member x6 = coincidesWithDefault static member x7 = coincidesWithDefault static member x8 = coincidesWithDefault static member x9 = coincidesWithDefault static member x10 = coincidesWithDefault static member x11 = coincidesWithDefault static member x12 = coincidesWithDefault static member x13 = coincidesWithDefault static member x14 = coincidesWithDefault static member x15 = coincidesWithDefault static member x16 = coincidesWithDefault static member x17 = coincidesWithDefault static member x18 = coincidesWithDefault static member x19 = coincidesWithDefault static member x20 = coincidesWithDefault> static member x21 = coincidesWithDefault> static member x22 = coincidesWithDefault static member x23 = coincidesWithDefault static member x24 = coincidesWithDefault static member x25 = coincidesWithDefault static member x26 = coincidesWithDefault type BackwardCompatibility () = static member x1 = backwardCompatibleWithDefault static member x2 = backwardCompatibleWithDefault static member x3 = backwardCompatibleWithDefault static member x4 = backwardCompatibleWithDefault static member x5 = backwardCompatibleWithDefault static member x6 = backwardCompatibleWithDefault static member x7 = backwardCompatibleWithDefault static member x8 = backwardCompatibleWithDefault static member x9 = backwardCompatibleWithDefault static member x10 = backwardCompatibleWithDefault static member x11 = backwardCompatibleWithDefault static member x12 = backwardCompatibleWithDefault static member x13 = backwardCompatibleWithDefault static member x14 = backwardCompatibleWithDefault static member x15 = backwardCompatibleWithDefault static member x16 = backwardCompatibleWithDefault static member x17 = backwardCompatibleWithDefault static member x18 = backwardCompatibleWithDefault static member x19 = backwardCompatibleWithDefault static member x20 = backwardCompatibleWithDefault> static member x21 = backwardCompatibleWithDefault> static member x22 = backwardCompatibleWithDefault static member x23 = backwardCompatibleWithDefault static member x24 = backwardCompatibleWithDefault static member x25 = backwardCompatibleWithDefault static member x26 = backwardCompatibleWithDefault let inline ``Run using all serializers``< ^T when ^T:equality> (test: (^T->string)->(string-> ^T)-> ^T->unit) (input: ^T) = [ Compact.serialize, Compact.deserialize Default.serialize, Default.deserialize CamelCaseSerializer.serialize, CamelCaseSerializer.deserialize BackwardCompatible.serialize, BackwardCompatible.deserialize ] |> List.iter (fun (s, d) -> test s d input) let inline testBackwardCompat< ^T when ^T:equality> (x: ^T) = let y = x |> Compact.Legacy.serialize< ^T> |> Compact.deserialize< ^T> Assert.AreEqual(x, y, "Tuple deserialization is not backward compatible!") type ARecord = { id: System.Guid name: string } let inline assertStrictFailsToDeserialize< ^T> t = let s = Compact.Strict.tryDeserialize t match s with | Choice1Of2 _ -> Assert.IsTrue(true) | Choice2Of2 _ -> Assert.IsTrue(false) /// Test for the compact serializer [] type JsonSerializerTests() = [] static member Init(context : TestContext) = () [] [] member __.``Serialize field-less DU`` () = FooBar |> serializedAs "\"FooBar\"" [] [] member __.``Dont' touch Fsharp lists`` () = [1;2;3] |> serializedAs (Default.serialize [1;2;3]) [] [] member __.``Handles Some`` () = Some "test" |> serializedAs "\"test\"" [] [] member __.``Handles None`` () = None |> serializedAs "null" [] [] member __.``Handles just the expected types``() = let conv = CompactUnionJsonConverter(true) Assert.IsTrue(conv.CanConvert(Color.Red.GetType())) Assert.IsTrue(conv.CanConvert(typeof)) Assert.IsTrue(conv.CanConvert(typeof<_ option>)) Assert.IsTrue(conv.CanConvert(typeof<_ Tree>)) Assert.IsTrue(conv.CanConvert(typeof)) Assert.IsFalse(conv.CanConvert(typeof<_ list>)) Assert.IsFalse(conv.CanConvert(typeof>)) [] [] member __.``Serialization does not raise exceptions for basic types``() = ``Run using all serializers`` canBeSerialized <| Red ``Run using all serializers`` canBeSerialized <| Blue ``Run using all serializers`` canBeSerialized <| Circle (8,99) ``Run using all serializers`` canBeSerialized <| Some 8 ``Run using all serializers`` canBeSerialized <| Some (Circle(5,120)) ``Run using all serializers`` canBeSerialized <| None ``Run using all serializers`` canBeSerialized <| Some (Node(Leaf 1,Leaf 9)) ``Run using all serializers`` canBeSerialized <| Case1 ``Run using all serializers`` canBeSerialized <| Case2 (3) ``Run using all serializers`` canBeSerialized <| Case3 (3,"s", "Foo") ``Run using all serializers`` canBeSerialized <| Circle (8,10) ``Run using all serializers`` canBeSerialized <| Leaf ["s";"s"] [] [] member __.``Serialization and deserialization are reciprocal``() = ``Run using all serializers`` areReciprocal <| Some 8 ``Run using all serializers`` areReciprocal <| Leaf "s" ``Run using all serializers`` areReciprocal <| Leaf ["s";"s"] ``Run using all serializers`` areReciprocal <| Leaf "s" ``Run using all serializers`` areReciprocal <| Some 8 ``Run using all serializers`` areReciprocal <| Red ``Run using all serializers`` areReciprocal <| Circle (8,10) ``Run using all serializers`` areReciprocal <| Node((Leaf "s"),(Leaf "s")) ``Run using all serializers`` areReciprocal <| Leaf ["s";"s"] ``Run using all serializers`` areReciprocal <| Node((Leaf 1),(Leaf 9)) ``Run using all serializers`` areReciprocal <| Case1 ``Run using all serializers`` areReciprocal <| Case2 (3) ``Run using all serializers`` areReciprocal <| Case3 (3,"s", "Foo") ``Run using all serializers`` areReciprocal <| (["test", [3;3;4]] |> Map.ofSeq) ``Run using all serializers`` areReciprocal <| ["test", [3;3;4]] ``Run using all serializers`` areReciprocal <| Some (Some (Some None)) ``Run using all serializers`` areReciprocal <| Some (Some None) ``Run using all serializers`` areReciprocal <| Some null ``Run using all serializers`` areReciprocal <| Some None ``Run using all serializers`` areReciprocal <| Some (Some (Some None)) ``Run using all serializers`` areReciprocal <| (1,2,3,4,5,6,7,8,9,10) ``Run using all serializers`` areReciprocal <| [ "UnDromadaire", 1; "UnChameau", 2; "DeuxChats", 3 ] [] [] member __.``No ambiguity between records and Option type``() = ``Run using all serializers`` areReciprocal <| Some (Some (Some None)) ``Run using all serializers`` areReciprocal <| { SomeAmbiguity.Some = null } ``Run using all serializers`` areReciprocal <| { SomeAmbiguity.Some = SimpleDu.Foo } ``Run using all serializers`` areReciprocal <| { SomeAmbiguity.Some = "test" } ``Run using all serializers`` areReciprocal <| { SomeAmbiguity.Some = 123 } ``Run using all serializers`` areReciprocal <| (Option.Some { SomeAmbiguity.Some = 345 }) ``Run using all serializers`` areReciprocal <| (Option.Some <| SomeAmbiguity.DUWithFieldlessCaseNamedSome.Some "ambiguous") ``Run using all serializers`` areReciprocal <| (Option.Some { SomeAmbiguity.RecordWithFieldNamedSome.Some = 8 }) ``Run using all serializers`` areReciprocal <| (Option.Some <| SomeAmbiguity.DUWithCaseWithFieldNamedSome.Some) [] [] member __.``CamelCaseSerializer handles discriminated unions`` () = let du = ComplexDu <| SomeField (2, 3) let str = CamelCaseSerializer.serialize du Assert.AreEqual("""{"complexDu":{"someField":[2,3]}}""", str) [] [] member __.``CamelCaseSerializer handles discriminated unions with consecutive uppercase characters`` () = let du = [ FOO ; FOOWithRecord { BAR=2; BAZNumber=3 } ] let str = CamelCaseSerializer.serialize du Assert.AreEqual("""["FOO",{"fooWithRecord":{"bar":2,"bazNumber":3}}]""", str) [] [] member __.``CamelCaseSerializer makes properties camelCase but not values`` () = let du = [ AString "foo"; SimpleDU ] let str = CamelCaseSerializer.serialize du Assert.AreEqual("""[{"aString":"foo"},"SimpleDU"]""", str) [] [] member __.``CamelCaseSerializer handles option type`` () = let du = { WrappedField = Some Red } let str = CamelCaseSerializer.serialize du Assert.AreEqual("""{"wrappedField":"Red"}""", str) [] [] member __.``Fuzzing Reciprocal Compact`` () = Check.VerboseThrowOnFailureAll() [] [] member __.``Fuzzing Reciprocal CamelCase`` () = Check.VerboseThrowOnFailureAll() [] [] member __.``Deserialization coincides with JSon.Net (Fuzzing)`` () = Check.VerboseThrowOnFailureAll() [] [] member __.``BackwardCompatible deserializes default Json.Net format and returns same object`` () = Check.VerboseThrowOnFailureAll() [] [] member __.``Serialize tuples as list`` () = (1, 2) |> serializedAs (defaultSerialize [1; 2]) (1, 2, 3) |> serializedAs (defaultSerialize [1; 2; 3]) (1, 2, 3, 4, 5, 6, 7, 8, 9, 10) |> serializedAs (defaultSerialize [1; 2; 3; 4; 5; 6; 7; 8; 9; 10]) [] [] member __.``Tuple serialization is backward compatible`` () = (1, 2) |> testBackwardCompat (1, "test", 5) |> testBackwardCompat (1, ["foo"; "bar"]) |> testBackwardCompat (1, ["foo"; "bar"], 4, "hello", ("bird", 3), 2, 3, 2, 4, 7) |> testBackwardCompat (1, 2, 3, 4, 5, 6, 7, 8, 9, 10) |> testBackwardCompat // Check for nested serialization: legacy JSON.net serialization breaks down tuples // in buckets of 7 elements maximum. Each additional bucket gets nested under a // "Rest" JSON field. (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17) |> testBackwardCompat [] [] member __.``Legacy tuple deserialization handles property reordering`` () = let r = """{ "Item3": 3, "Item2": 2, "Item1": 1 }""" |> Compact.deserialize Assert.AreEqual(r, (1,2,3), "Tuple deserialization should handle JSON properties in any order") [] [] member __.``Reject missing fields`` () = assertStrictFailsToDeserialize """{ "name":"hola" }""" assertStrictFailsToDeserialize """{ "id":"f893e695-496d-4e30-8fc7-b2ff59725e6c" }""" Assert.ThrowsException(fun () -> Compact.Strict.deserialize """{ "name":"hola" }""" |> ignore) |> ignore () [] [] member __.``Don't change the casing of dictionary keys`` () = let d = [ "UnDromadaire", 1 "UnChameau", 2 "DeuxChats", 3 ] |> dict let camel = CamelCaseSerializer.serialize d Assert.AreEqual("""{"UnDromadaire":1,"UnChameau":2,"DeuxChats":3}""", camel) let compact = Compact.serialize d Assert.AreEqual("""{ "UnDromadaire": 1, "UnChameau": 2, "DeuxChats": 3 }""", compact)