diff --git a/README.md b/README.md index c998aa2..fef41ed 100644 --- a/README.md +++ b/README.md @@ -16,3 +16,5 @@ This is to keep track of different blog post that I refer to sometimes when thin ### Tests For the tests, we use a shared project `Thoth.Json.Tests` that is referenced by the different runners. This is because we want each runner to only have the minimum amount of dependencies, and also if we include files from outside the `.fsproj` folder, then some generated files by Fable escape from the specify `outDir`. + +Some of the tests require specific versions of Node.js, Python, etc. You can enter a shell with pinned versions available using `nix develop`. diff --git a/Thoth.Json.sln b/Thoth.Json.sln index 5bc2bec..88791b5 100644 --- a/Thoth.Json.sln +++ b/Thoth.Json.sln @@ -29,6 +29,8 @@ Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Thoth.Json", "packages\Thot EndProject Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Build", "build\EasyBuild.fsproj", "{1A292FB0-2CCA-41C2-A9E6-EB69A31BAA5C}" EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Thoth.Json.Auto", "packages\Thoth.Json.Auto\Thoth.Json.Auto.fsproj", "{D7497D92-ABF7-4B81-9DC9-9DA52FD6F5EB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -82,6 +84,10 @@ Global {1A292FB0-2CCA-41C2-A9E6-EB69A31BAA5C}.Debug|Any CPU.Build.0 = Debug|Any CPU {1A292FB0-2CCA-41C2-A9E6-EB69A31BAA5C}.Release|Any CPU.ActiveCfg = Release|Any CPU {1A292FB0-2CCA-41C2-A9E6-EB69A31BAA5C}.Release|Any CPU.Build.0 = Release|Any CPU + {D7497D92-ABF7-4B81-9DC9-9DA52FD6F5EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D7497D92-ABF7-4B81-9DC9-9DA52FD6F5EB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D7497D92-ABF7-4B81-9DC9-9DA52FD6F5EB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D7497D92-ABF7-4B81-9DC9-9DA52FD6F5EB}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {7E82EFD6-6C43-4F64-8651-3BBDE9E18871} = {1C828713-FFEF-40AA-B204-0F1753D9626B} @@ -94,5 +100,6 @@ Global {5DD6593A-A2BB-48C8-90A2-5DD3E957CE75} = {61B51532-DABC-4F8E-9F95-50BE75BA59B4} {81253746-095B-405B-BA26-E8CBDA25C992} = {61B51532-DABC-4F8E-9F95-50BE75BA59B4} {D03314F6-68F9-46BD-A0D1-75B2B18E76A0} = {1C828713-FFEF-40AA-B204-0F1753D9626B} + {D7497D92-ABF7-4B81-9DC9-9DA52FD6F5EB} = {1C828713-FFEF-40AA-B204-0F1753D9626B} EndGlobalSection EndGlobal diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..cb3e9a6 --- /dev/null +++ b/flake.lock @@ -0,0 +1,25 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1722869614, + "narHash": "sha256-7ojM1KSk3mzutD7SkrdSflHXEujPvW1u7QuqWoTLXQU=", + "rev": "883180e6550c1723395a3a342f830bfc5c371f6b", + "revCount": 633812, + "type": "tarball", + "url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.2405.633812%2Brev-883180e6550c1723395a3a342f830bfc5c371f6b/01912867-c4cd-766e-bc62-2cc7d1546d12/source.tar.gz" + }, + "original": { + "type": "tarball", + "url": "https://flakehub.com/f/NixOS/nixpkgs/0.2405.%2A.tar.gz" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..075108e --- /dev/null +++ b/flake.nix @@ -0,0 +1,34 @@ +{ + description = "Development environment"; + + inputs = { + nixpkgs.url = "https://flakehub.com/f/NixOS/nixpkgs/0.2405.*.tar.gz"; + }; + + outputs = { self, nixpkgs }: + let + allSystems = [ + "x86_64-linux" + "aarch64-linux" + "x86_64-darwin" + "aarch64-darwin" + ]; + + forAllSystems = f: nixpkgs.lib.genAttrs allSystems (system: f { + pkgs = import nixpkgs { inherit system; }; + }); + in + { + devShells = forAllSystems ({ pkgs }: { + default = + pkgs.mkShell { + packages = [ + pkgs.bashInteractive + pkgs.dotnet-sdk_8 + pkgs.nodejs_20 + pkgs.python312 + ]; + }; + }); + }; +} diff --git a/packages/Thoth.Json.Auto/Casing.fs b/packages/Thoth.Json.Auto/Casing.fs new file mode 100644 index 0000000..9d02107 --- /dev/null +++ b/packages/Thoth.Json.Auto/Casing.fs @@ -0,0 +1,109 @@ +module Thoth.Json.Auto.Casing + +open System +open System.Text + +let private upperFirst (str: string) = + str.[..0].ToUpperInvariant() + str.[1..] + +let private dotNetAcronyms = + Set.ofSeq + [ + "id" + "ip" + ] + +let convertCase (source: CaseStyle) (dest: CaseStyle) (text: string) = + if source = dest then + text + else + let words = + match source with + | SnakeCase + | ScreamingSnakeCase -> + text.Split([| '_' |], StringSplitOptions.RemoveEmptyEntries) + |> Seq.toList + | PascalCase + | CamelCase + | DotNetPascalCase + | DotNetCamelCase -> + seq { + let sb = StringBuilder() + + for c in text do + if Char.IsUpper c && sb.Length > 0 then + yield sb.ToString() + sb.Clear() |> ignore + + sb.Append(c) |> ignore + + if sb.Length > 0 then + yield sb.ToString() + } + |> Seq.fold + (fun state next -> + if next.Length > 1 then + next :: state + else + match state with + | [] -> [ next ] + | x :: xs -> + if + x.Length = 1 + || x |> Seq.forall Char.IsUpper + then + (x + next) :: xs + else + next :: x :: xs + ) + [] + |> Seq.rev + |> Seq.toList + + match dest with + | ScreamingSnakeCase -> + words + |> Seq.map (fun x -> x.ToUpperInvariant()) + |> String.concat "_" + | SnakeCase -> + words + |> Seq.map (fun x -> x.ToLowerInvariant()) + |> String.concat "_" + | PascalCase -> + words + |> Seq.map (fun x -> x.ToLowerInvariant() |> upperFirst) + |> String.concat "" + | CamelCase -> + words + |> Seq.mapi (fun i x -> + if i = 0 then + x.ToLowerInvariant() + else + x.ToLowerInvariant() |> upperFirst + ) + |> String.concat "" + | DotNetPascalCase -> + words + |> Seq.map (fun x -> + let u = x.ToLowerInvariant() + + if Set.contains u dotNetAcronyms then + u.ToUpperInvariant() + else + upperFirst u + ) + |> String.concat "" + | DotNetCamelCase -> + words + |> Seq.mapi (fun i x -> + if i = 0 then + x.ToLowerInvariant() + else + let u = x.ToLowerInvariant() + + if Set.contains u dotNetAcronyms then + u.ToUpperInvariant() + else + upperFirst u + ) + |> String.concat "" diff --git a/packages/Thoth.Json.Auto/Decode.fs b/packages/Thoth.Json.Auto/Decode.fs new file mode 100644 index 0000000..860926b --- /dev/null +++ b/packages/Thoth.Json.Auto/Decode.fs @@ -0,0 +1,1525 @@ +namespace Thoth.Json.Auto + +open System +open System.Reflection +open FSharp.Reflection +open Thoth.Json +open Thoth.Json.Core + +[] +module Decode = + + [] + module private Helpers = + + type DecodeHelpers = + static member inline Lazily<'t> + (x: Lazy>) + : Decoder<'t> + = + Decode.lazily x + + static member Option<'t>(x: Decoder<'t>) : Decoder<'t option> = + Decode.option x + + static member List<'t>(x: Decoder<'t>) : Decoder<'t list> = + Decode.list x + + static member Array<'t>(x: Decoder<'t>) : Decoder<'t array> = + Decode.array x + + static member Seq<'t>(x: Decoder<'t>) : Decoder<'t seq> = + Decode.list x |> Decode.map Seq.ofList + + static member Set<'t when 't: comparison> + (x: Decoder<'t>) + : Decoder> + = + Decode.list x |> Decode.map Set.ofList + + static member Dict<'t>(x: Decoder<'t>) : Decoder> = + Decode.dict x + + static member MapAsArray<'k, 'v when 'k: comparison> + ( + keyDecoder: Decoder<'k>, + valueDecoder: Decoder<'v> + ) + : Decoder> + = + Decode.map' keyDecoder valueDecoder + + static member Field<'t> + ( + name: string, + x: Decoder<'t> + ) + : Decoder<'t> + = + Decode.field name x + + static member Optional<'t> + ( + name: string, + x: Decoder<'t> + ) + : Decoder<'t option> + = + Decode.optional name x + + static member Index<'t>(index: int, x: Decoder<'t>) : Decoder<'t> = + Decode.index index x + + static member Succeed<'t>(x: 't) : Decoder<'t> = Decode.succeed x + + static member Fail<'t>(x: string) : Decoder<'t> = Decode.fail x + + static member Bind<'t, 'u> + ( + f: 't -> Decoder<'u>, + x: Decoder<'t> + ) + : Decoder<'u> + = + Decode.andThen f x + + static member Map<'t, 'u> + ( + f: 't -> 'u, + x: Decoder<'t> + ) + : Decoder<'u> + = + Decode.map f x + + static member Zip<'a, 'b> + ( + x: Decoder<'a>, + y: Decoder<'b> + ) + : Decoder<'a * 'b> + = + Decode.map2 (fun x y -> x, y) x y + + static member Either<'t> + ( + x: Decoder<'t>, + y: Decoder<'t> + ) + : Decoder<'t> + = + Decode.oneOf + [ + x + y + ] + + static member inline EnumByte<'t when 't: enum> + () + : Decoder<'t> + = + Decode.Enum.byte + + static member inline EnumSbyte<'t when 't: enum> + () + : Decoder<'t> + = + Decode.Enum.sbyte + + static member inline EnumInt16<'t when 't: enum> + () + : Decoder<'t> + = + Decode.Enum.int16 + + static member inline EnumUint16<'t when 't: enum> + () + : Decoder<'t> + = + Decode.Enum.uint16 + + static member inline EnumInt<'t when 't: enum> + () + : Decoder<'t> + = + Decode.Enum.int + + static member inline EnumUint32<'t when 't: enum> + () + : Decoder<'t> + = + Decode.Enum.uint32 + +#if !FABLE_COMPILER + let getGenericMethodDefinition (name: string) : MethodInfo = + typeof + .GetMethods(BindingFlags.Static ||| BindingFlags.NonPublic) + |> Seq.filter (fun x -> x.Name = name) + |> Seq.exactlyOne + |> fun mi -> mi.GetGenericMethodDefinition() +#endif + + let makeDecoderType (ty: Type) : Type = + typedefof>.MakeGenericType([| ty |]) + + [] + module internal Decode = + + [] + module Generic = + +#if FABLE_COMPILER + let option (innerType: Type) (decoder: obj) : obj = + Decode.option (unbox decoder) |> box +#else + let private optionGenericMethodDefinition = + getGenericMethodDefinition "Option" + + let option (innerType: Type) (decoder: obj) : obj = + optionGenericMethodDefinition + .MakeGenericMethod([| innerType |]) + .Invoke(null, [| decoder |]) +#endif + +#if FABLE_COMPILER + let seq (innerType: Type) (decoder: obj) : obj = + Decode.list (unbox decoder) |> box +#else + let private seqGenericMethodDefinition = + getGenericMethodDefinition "Seq" + + let seq (innerType: Type) (decoder: obj) : obj = + seqGenericMethodDefinition + .MakeGenericMethod([| innerType |]) + .Invoke(null, [| decoder |]) +#endif + +#if FABLE_COMPILER + let set (innerType: Type) (decoder: obj) : obj = + Decode.list (unbox decoder) |> Decode.map Set.ofList |> box +#else + let private setGenericMethodDefinition = + getGenericMethodDefinition "Set" + + let set (innerType: Type) (decoder: obj) : obj = + setGenericMethodDefinition + .MakeGenericMethod([| innerType |]) + .Invoke(null, [| decoder |]) +#endif + +#if FABLE_COMPILER + let list (innerType: Type) (decoder: obj) : obj = + Decode.list (unbox decoder) |> box +#else + let private listGenericMethodDefinition = + getGenericMethodDefinition "List" + + let list (innerType: Type) (decoder: obj) : obj = + listGenericMethodDefinition + .MakeGenericMethod([| innerType |]) + .Invoke(null, [| decoder |]) +#endif + +#if FABLE_COMPILER + let array (innerType: Type) (decoder: obj) : obj = + Decode.array (unbox decoder) |> box +#else + let private arrayGenericMethodDefinition = + getGenericMethodDefinition "Array" + + let array (innerType: Type) (decoder: obj) : obj = + arrayGenericMethodDefinition + .MakeGenericMethod([| innerType |]) + .Invoke(null, [| decoder |]) +#endif + +#if FABLE_COMPILER + let dict (innerType: Type) (decoder: obj) : obj = + Decode.dict (unbox decoder) |> box +#else + let private dictGenericMethodDefinition = + getGenericMethodDefinition "Dict" + + let dict (innerType: Type) (decoder: obj) : obj = + dictGenericMethodDefinition + .MakeGenericMethod([| innerType |]) + .Invoke(null, [| decoder |]) +#endif + +#if FABLE_COMPILER + let mapAsArray + (keyType: Type) + (valueType: Type) + (keyDecoder: obj) + (valueDecoder: obj) + : obj + = + Decode.map' (unbox keyDecoder) (unbox valueDecoder) |> box +#else + let private mapAsArrayMethodDefinition = + getGenericMethodDefinition "MapAsArray" + + let mapAsArray + (keyType: Type) + (valueType: Type) + (keyDecoder: obj) + (valueDecoder: obj) + : obj + = + mapAsArrayMethodDefinition + .MakeGenericMethod( + [| + keyType + valueType + |] + ) + .Invoke( + null, + [| + keyDecoder + valueDecoder + |] + ) +#endif + +#if FABLE_COMPILER + let map + (fromType: Type) + (toType: Type) + (func: obj) + (decoder: obj) + : obj + = + Decode.map (unbox func) (unbox decoder) |> box +#else + let private mapGenericMethodDefinition = + getGenericMethodDefinition "Map" + + let map + (fromType: Type) + (toType: Type) + (func: obj) + (decoder: obj) + : obj + = + mapGenericMethodDefinition + .MakeGenericMethod( + [| + fromType + toType + |] + ) + .Invoke( + null, + [| + func + decoder + |] + ) +#endif + +#if FABLE_COMPILER + let zip + (leftType: Type) + (rightType: Type) + (leftDecoder: obj) + (rightDecoder: obj) + : obj + = + Decode.map2 + (fun x y -> x, y) + (unbox leftDecoder) + (unbox rightDecoder) + |> box +#else + let private zipGenericMethodDefinition = + getGenericMethodDefinition "Zip" + + let zip + (leftType: Type) + (rightType: Type) + (leftDecoder: obj) + (rightDecoder: obj) + : obj + = + zipGenericMethodDefinition + .MakeGenericMethod( + [| + leftType + rightType + |] + ) + .Invoke( + null, + [| + leftDecoder + rightDecoder + |] + ) +#endif + +#if FABLE_COMPILER + let lazily (innerType: Type) (x: obj) : obj = + Decode.lazily (unbox x) |> box +#else + let private lazilyGenericMethodDefinition = + getGenericMethodDefinition "Lazily" + + let lazily (innerType: Type) (x: obj) : obj = + lazilyGenericMethodDefinition + .MakeGenericMethod([| innerType |]) + .Invoke(null, [| x |]) +#endif + +#if FABLE_COMPILER + let succeed (innerType: Type) (x: obj) : obj = + Decode.succeed (unbox x) |> box +#else + let private succeedGenericMethodDefinition = + getGenericMethodDefinition "Succeed" + + let succeed (innerType: Type) (x: obj) : obj = + succeedGenericMethodDefinition + .MakeGenericMethod([| innerType |]) + .Invoke(null, [| x |]) +#endif + +#if FABLE_COMPILER + let fail (innerType: Type) (x: string) : obj = + Decode.fail x |> box +#else + let private failGenericMethodDefinition = + getGenericMethodDefinition "Fail" + + let fail (innerType: Type) (x: string) : obj = + failGenericMethodDefinition + .MakeGenericMethod([| innerType |]) + .Invoke(null, [| x |]) +#endif + +#if FABLE_COMPILER + let index (innerType: Type) (index: int) (decoder: obj) : obj = + Decode.index index (unbox decoder) |> box +#else + let private indexGenericMethodDefinition = + getGenericMethodDefinition "Index" + + let index (innerType: Type) (index: int) (decoder: obj) : obj = + indexGenericMethodDefinition + .MakeGenericMethod([| innerType |]) + .Invoke( + null, + [| + index + decoder + |] + ) +#endif + +#if FABLE_COMPILER + let field + (innerType: Type) + (name: string) + (decoder: obj) + : obj + = + Decode.field name (unbox decoder) |> box +#else + let private fieldGenericMethodDefinition = + getGenericMethodDefinition "Field" + + let field + (innerType: Type) + (name: string) + (decoder: obj) + : obj + = + fieldGenericMethodDefinition + .MakeGenericMethod([| innerType |]) + .Invoke( + null, + [| + name + decoder + |] + ) +#endif + +#if FABLE_COMPILER + let optional + (innerType: Type) + (name: string) + (decoder: obj) + : obj + = + Decode.optional name (unbox decoder) |> box +#else + let private optionalGenericMethodDefinition = + getGenericMethodDefinition "Optional" + + let optional + (innerType: Type) + (name: string) + (decoder: obj) + : obj + = + optionalGenericMethodDefinition + .MakeGenericMethod([| innerType |]) + .Invoke( + null, + [| + name + decoder + |] + ) +#endif + +#if FABLE_COMPILER + let bind + (fromType: Type) + (toType: Type) + (func: obj) + (decoder: obj) + : obj + = + Decode.andThen (unbox func) (unbox decoder) |> box +#else + let private bindGenericMethodDefinition = + getGenericMethodDefinition "Bind" + + let bind + (fromType: Type) + (toType: Type) + (func: obj) + (decoder: obj) + : obj + = + bindGenericMethodDefinition + .MakeGenericMethod( + [| + fromType + toType + |] + ) + .Invoke( + null, + [| + func + decoder + |] + ) +#endif + +#if FABLE_COMPILER + let either + (innerType: Type) + (decoderA: obj) + (decoderB: obj) + : obj + = + Decode.oneOf + [ + unbox decoderA + unbox decoderB + ] + |> box +#else + let private eitherGenericMethodDefinition = + getGenericMethodDefinition "Either" + + let either + (innerType: Type) + (decoderA: obj) + (decoderB: obj) + : obj + = + eitherGenericMethodDefinition + .MakeGenericMethod([| innerType |]) + .Invoke( + null, + [| + decoderA + decoderB + |] + ) +#endif + + module Enum = +#if FABLE_COMPILER + let checkEnumValue (innerType: Type) = + (fun value -> + if System.Enum.IsDefined(innerType, value) then + value |> Decode.succeed + else + { new Decoder<_> with + member _.Decode<'JsonValue> + ( + _, + value: 'JsonValue + ) + = + ("", + BadPrimitiveExtra( + innerType.FullName, + value, + "Unkown value provided for the enum" + )) + |> Error + } + ) +#endif + +#if FABLE_COMPILER + let byte (innerType: Type) : obj = + Decode.byte + |> Decode.andThen (checkEnumValue innerType) + |> box +#else + let private enumByteGenericMethodDefinition = + getGenericMethodDefinition "EnumByte" + + let byte (innerType: Type) : obj = + enumByteGenericMethodDefinition + .MakeGenericMethod([| innerType |]) + .Invoke(null, [||]) +#endif + +#if FABLE_COMPILER + let sbyte (innerType: Type) : obj = + Decode.sbyte + |> Decode.andThen (checkEnumValue innerType) + |> box +#else + let private enumSbyteGenericMethodDefinition = + getGenericMethodDefinition "EnumSbyte" + + let sbyte (innerType: Type) : obj = + enumSbyteGenericMethodDefinition + .MakeGenericMethod([| innerType |]) + .Invoke(null, [||]) +#endif + +#if FABLE_COMPILER + let int16 (innerType: Type) : obj = + Decode.int16 + |> Decode.andThen (checkEnumValue innerType) + |> box +#else + let private enumInt16GenericMethodDefinition = + getGenericMethodDefinition "EnumInt16" + + let int16 (innerType: Type) : obj = + enumInt16GenericMethodDefinition + .MakeGenericMethod([| innerType |]) + .Invoke(null, [||]) +#endif + +#if FABLE_COMPILER + let uint16 (innerType: Type) : obj = + Decode.int16 + |> Decode.andThen (checkEnumValue innerType) + |> box +#else + let private enumUint16GenericMethodDefinition = + getGenericMethodDefinition "EnumUint16" + + let uint16 (innerType: Type) : obj = + enumUint16GenericMethodDefinition + .MakeGenericMethod([| innerType |]) + .Invoke(null, [||]) +#endif + +#if FABLE_COMPILER + let int (innerType: Type) : obj = + Decode.int + |> Decode.andThen (checkEnumValue innerType) + |> box +#else + let private enumIntGenericMethodDefinition = + getGenericMethodDefinition "EnumInt" + + let int (innerType: Type) : obj = + enumIntGenericMethodDefinition + .MakeGenericMethod([| innerType |]) + .Invoke(null, [||]) +#endif + +#if FABLE_COMPILER + let uint32 (innerType: Type) : obj = + Decode.uint32 + |> Decode.andThen (checkEnumValue innerType) + |> box +#else + let private enumUint32GenericMethodDefinition = + getGenericMethodDefinition "EnumUint32" + + let uint32 (innerType: Type) : obj = + enumUint32GenericMethodDefinition + .MakeGenericMethod([| innerType |]) + .Invoke(null, [||]) +#endif + + // Unpacks tuples encoded like this: + // "a" + // ("a", "b") + // (("a", "b"), "c") + // ((("a", "b"), "c"), "d") + let getNestedTupleFields (tuple: obj) (length: int) = + if length = 1 then + [| tuple |] + else + let result = Array.zeroCreate length + + let mutable x = tuple + let mutable i = length - 1 + + while i > 0 do + result[i] <- FSharpValue.GetTupleField(x, 1) + + i <- i - 1 + + if i = 0 then + result[i] <- FSharpValue.GetTupleField(x, 0) + else + x <- FSharpValue.GetTupleField(x, 0) + + result + + let rec generateDecoder + (caseStyle: CaseStyle option) + (existingDecoders: Map) + (ty: Type) + : obj + = + match Map.tryFind (TypeKey.ofType ty) existingDecoders with + | Some x -> x + | None -> + match ty with + | UnitType _ -> box Decode.unit + | StringType _ -> box Decode.string + | CharType _ -> box Decode.char + | IntType _ -> box Decode.int + | BoolType _ -> box Decode.bool + | Int64Type _ -> box Decode.int64 + | DecimalType _ -> box Decode.decimal + | ByteType _ -> box Decode.byte + | SByteType _ -> box Decode.sbyte + | UInt16Type _ -> box Decode.uint16 + | SByteType _ -> box Decode.sbyte + | Int16Type _ -> box Decode.int16 + | UIntType _ -> box Decode.uint32 + | UInt64Type _ -> box Decode.uint64 + | SingleType _ -> box Decode.float32 + | DoubleType -> box Decode.float + | BigIntType _ -> box Decode.bigint + | GuidType _ -> box Decode.guid + | TimeSpanType _ -> box Decode.timespan + | DateTimeType _ -> +#if FABLE_COMPILER_PYTHON + box Decode.datetimeLocal +#else + box Decode.datetimeUtc +#endif +#if !FABLE_COMPILER_PYTHON + | DateTimeOffsetType _ -> box Decode.datetimeOffset +#endif + | OptionType inner -> + Decode.Generic.option + inner + (generateDecoder caseStyle existingDecoders inner) + | ListType inner -> + Decode.Generic.list + inner + (generateDecoder caseStyle existingDecoders inner) + | ArrayType inner -> + Decode.Generic.array + inner + (generateDecoder caseStyle existingDecoders inner) + | SeqType inner -> + Decode.Generic.seq + inner + (generateDecoder caseStyle existingDecoders inner) + | SetType inner -> + Decode.Generic.set + inner + (generateDecoder caseStyle existingDecoders inner) + | MapType(StringType, valueType) -> + Decode.Generic.dict + valueType + (generateDecoder caseStyle existingDecoders valueType) + | MapType(keyType, valueType) -> + let keyDecoder = + generateDecoder caseStyle existingDecoders keyType + + let valueDecoder = + generateDecoder caseStyle existingDecoders valueType + + Decode.Generic.mapAsArray + keyType + valueType + keyDecoder + valueDecoder + | FSharpRecordType _ -> + genericRecordDecoder caseStyle existingDecoders ty + | FSharpUnionType _ -> + genericUnionDecoder caseStyle existingDecoders ty + | FSharpTupleType _ -> + genericTupleDecoder caseStyle existingDecoders ty + | EnumType(ByteType _) -> Decode.Generic.Enum.byte ty + | EnumType(SByteType _) -> Decode.Generic.Enum.sbyte ty + | EnumType(Int16Type _) -> Decode.Generic.Enum.int16 ty + | EnumType(UInt16Type _) -> Decode.Generic.Enum.uint16 ty + | EnumType(IntType _) -> Decode.Generic.Enum.int ty + | EnumType(UIntType _) -> Decode.Generic.Enum.uint32 ty + | x -> failwith $"Unsupported type %s{x.FullName}" + + and private genericRecordDecoder + (caseStyle: CaseStyle option) + (existingDecoders: Map) + (ty: Type) + : obj + = +#if FABLE_COMPILER + let mutable self = Unchecked.defaultof<_> + + let existingDecoders = + if Type.isRecursive ty then + let lazySelf = + Decode.Generic.lazily + ty + (Lazy.makeGeneric (makeDecoderType ty) (fun _ -> self)) + + existingDecoders |> Map.add (TypeKey.ofType ty) lazySelf + else + existingDecoders + + let recordFields = + match ty with + | FSharpRecordType fields -> fields + | _ -> failwith $"Expected an F# record type" + + let fieldDecoders = + [| + for field in recordFields do + let encodedFieldName = + match caseStyle with + | Some caseStyle -> + Casing.convertCase + DotNetPascalCase + caseStyle + field.Name + | None -> field.Name + + let decoder = + match field.PropertyType with + | UnitType _ -> + Decode.Generic.optional + field.PropertyType + encodedFieldName + (generateDecoder + caseStyle + existingDecoders + field.PropertyType) + |> Decode.Generic.map + typeof + typeof + (fun (o: obj) -> + let maybeUnit = unbox o + maybeUnit |> Option.defaultValue () |> box + ) + | OptionType innerType -> + Decode.Generic.optional + innerType + encodedFieldName + (generateDecoder + caseStyle + existingDecoders + innerType) + | _ -> + Decode.Generic.field + field.PropertyType + encodedFieldName + (generateDecoder + caseStyle + existingDecoders + field.PropertyType) + + field.PropertyType, decoder + |] + + let rec mergeDecoders (state: Type * obj) (next: Type * obj) = + let aggregateType, aggregateDecoder = state + let fieldType, nextDecoder = next + + let nextType = + FSharpType.MakeTupleType( + [| + aggregateType + fieldType + |] + ) + + let nextDecoder = + Decode.Generic.zip + aggregateType + fieldType + aggregateDecoder + nextDecoder + + nextType, nextDecoder + + let tupleType, decoder = fieldDecoders |> Array.reduce mergeDecoders // There will always be at least one field + + let tupleToRecord: obj -> obj = + fun x -> + let values = getNestedTupleFields x (Array.length recordFields) +#if FABLE_COMPILER_PYTHON + FSharpValue.MakeRecord(ty, values) +#else + FSharpValue.MakeRecord( + ty, + values, + allowAccessToPrivateRepresentation = true + ) +#endif + + let decoder = + Decode.Generic.map tupleType ty (box tupleToRecord) decoder + + self <- decoder + + decoder +#else + let mutable self = Unchecked.defaultof<_> + + let existingDecoders = + if Type.isRecursive ty then + let lazySelf = + Decode.Generic.lazily + ty + (Lazy.makeGeneric + (makeDecoderType ty) + (FSharpValue.MakeFunction( + FSharpType.MakeFunctionType( + typeof, + makeDecoderType ty + ), + (fun _ -> self) + ))) + + existingDecoders |> Map.add (TypeKey.ofType ty) lazySelf + else + existingDecoders + + let recordFields = + match ty with + | FSharpRecordType fields -> fields + | _ -> failwith $"Expected an F# record type" + + let fieldDecoders = + [| + for field in recordFields do + let encodedFieldName = + match caseStyle with + | Some caseStyle -> + Casing.convertCase + DotNetPascalCase + caseStyle + field.Name + | None -> field.Name + + let decoder = + match field.PropertyType with + | UnitType _ -> + Decode.Generic.optional + field.PropertyType + encodedFieldName + (generateDecoder + caseStyle + existingDecoders + field.PropertyType) + |> Decode.Generic.map + typeof + typeof + (Option.defaultValue ()) + | OptionType innerType -> + Decode.Generic.optional + innerType + encodedFieldName + (generateDecoder + caseStyle + existingDecoders + innerType) + | _ -> + Decode.Generic.field + field.PropertyType + encodedFieldName + (generateDecoder + caseStyle + existingDecoders + field.PropertyType) + + field.PropertyType, decoder + |] + + let rec mergeDecoders (state: Type * obj) (next: Type * obj) = + let aggregateType, aggregateDecoder = state + let fieldType, nextDecoder = next + + let nextType = + FSharpType.MakeTupleType( + [| + aggregateType + fieldType + |] + ) + + let nextDecoder = + Decode.Generic.zip + aggregateType + fieldType + aggregateDecoder + nextDecoder + + nextType, nextDecoder + + let tupleType, decoder = fieldDecoders |> Array.reduce mergeDecoders // There will always be at least one field + + let tupleToRecordType = FSharpType.MakeFunctionType(tupleType, ty) + + let tupleToRecordImpl: obj -> obj = + fun x -> + let values = getNestedTupleFields x (Array.length recordFields) +#if FABLE_COMPILER_PYTHON + FSharpValue.MakeRecord(ty, values) +#else + FSharpValue.MakeRecord( + ty, + values, + allowAccessToPrivateRepresentation = true + ) +#endif + + let tupleToRecord: obj = + FSharpValue.MakeFunction(tupleToRecordType, tupleToRecordImpl) + + let decoder = Decode.Generic.map tupleType ty tupleToRecord decoder + + self <- decoder + + decoder +#endif + + and private genericUnionDecoder + (caseStyle: CaseStyle option) + (existingDecoders: Map) + (ty: Type) + : obj + = +#if FABLE_COMPILER + let mutable self = Unchecked.defaultof<_> + + let existingDecoders = + if Type.isRecursive ty then + let lazySelf = + Decode.Generic.lazily + ty + (Lazy.makeGeneric (makeDecoderType ty) ((fun _ -> self))) + + existingDecoders |> Map.add (TypeKey.ofType ty) lazySelf + else + existingDecoders + + let unionCases = + match ty with + | FSharpUnionType cases -> cases + | _ -> + failwith $"Expected an F# union type but found %s{ty.FullName}" + + let alternatives = + [ + for case in unionCases do + let caseFields = case.GetFields() + + if Array.isEmpty caseFields then +#if FABLE_COMPILER_PYTHON + let caseObject = FSharpValue.MakeUnion(case, [||]) +#else + let caseObject = + FSharpValue.MakeUnion( + case, + [||], + allowAccessToPrivateRepresentation = true + ) +#endif + + let funcImpl: obj -> obj = + (fun x -> + let x = unbox x + + if x = case.Name then + Decode.Generic.succeed ty caseObject + else + Decode.Generic.fail + ty + $"Expected %s{case.Name} but found \"%s{x}\"" + ) + + Decode.Generic.bind + typeof + ty + funcImpl + Decode.string + else + let fieldDecoders = + [| + for index, field in Array.indexed caseFields do + let decoder = + Decode.Generic.index + field.PropertyType + (index + 1) + (generateDecoder + caseStyle + existingDecoders + field.PropertyType) + + field.PropertyType, decoder + |] + + let rec mergeDecoders + (state: Type * obj) + (next: Type * obj) + = + let aggregateType, aggregateDecoder = state + let fieldType, nextDecoder = next + + let nextType = + FSharpType.MakeTupleType( + [| + aggregateType + fieldType + |] + ) + + let nextDecoder = + Decode.Generic.zip + aggregateType + fieldType + aggregateDecoder + nextDecoder + + nextType, nextDecoder + + let tupleType, decoder = + fieldDecoders |> Array.reduce mergeDecoders // There will always be at least one field + + let tupleToUnionCase: obj -> obj = + fun (x: obj) -> + let values = + getNestedTupleFields + x + (Array.length caseFields) +#if FABLE_COMPILER_PYTHON + FSharpValue.MakeUnion(case, values) +#else + FSharpValue.MakeUnion( + case, + values, + allowAccessToPrivateRepresentation = true + ) +#endif + + let prefix = + Decode.index 0 Decode.string + |> Decode.andThen (fun x -> + if x = case.Name then + Decode.succeed () + else + Decode.fail + $"Expected %s{case.Name} but found \"%s{x}\"" + ) + + let dec = + Decode.Generic.map + tupleType + ty + (box tupleToUnionCase) + decoder + + let unitToUnionCaseImpl: obj -> obj = fun _ -> dec + + Decode.Generic.bind + typeof + ty + (box unitToUnionCaseImpl) + prefix + ] + + let decoder = alternatives |> Seq.reduce (Decode.Generic.either ty) + + self <- decoder + + decoder +#else + let mutable self = Unchecked.defaultof<_> + + let existingDecoders = + if Type.isRecursive ty then + let lazySelf = + Decode.Generic.lazily + ty + (Lazy.makeGeneric + (makeDecoderType ty) + (FSharpValue.MakeFunction( + FSharpType.MakeFunctionType( + typeof, + makeDecoderType ty + ), + (fun _ -> self) + ))) + + existingDecoders |> Map.add (TypeKey.ofType ty) lazySelf + else + existingDecoders + + let unionCases = + match ty with + | FSharpUnionType cases -> cases + | _ -> + failwith $"Expected an F# union type but found %s{ty.FullName}" + + let alternatives = + [ + for case in unionCases do + let caseFields = case.GetFields() + + if Array.isEmpty caseFields then + let funcType = + FSharpType.MakeFunctionType( + typeof, + makeDecoderType ty + ) + + let caseObject = +#if FABLE_COMPILER_PYTHON + FSharpValue.MakeUnion(case, [||]) +#else + FSharpValue.MakeUnion( + case, + [||], + allowAccessToPrivateRepresentation = true + ) +#endif + + let funcImpl: obj -> obj = + (fun x -> + let x = unbox x + + if x = case.Name then + Decode.Generic.succeed ty caseObject + else + Decode.Generic.fail + ty + $"Expected %s{case.Name} but found \"%s{x}\"" + ) + + let func = FSharpValue.MakeFunction(funcType, funcImpl) + + Decode.Generic.bind typeof ty func Decode.string + else + let fieldDecoders = + [| + for index, field in Array.indexed caseFields do + let decoder = + Decode.Generic.index + field.PropertyType + (index + 1) + (generateDecoder + caseStyle + existingDecoders + field.PropertyType) + + field.PropertyType, decoder + |] + + let rec mergeDecoders + (state: Type * obj) + (next: Type * obj) + = + let aggregateType, aggregateDecoder = state + let fieldType, nextDecoder = next + + let nextType = + FSharpType.MakeTupleType( + [| + aggregateType + fieldType + |] + ) + + let nextDecoder = + Decode.Generic.zip + aggregateType + fieldType + aggregateDecoder + nextDecoder + + nextType, nextDecoder + + let tupleType, decoder = + fieldDecoders |> Array.reduce mergeDecoders // There will always be at least one field + + let tupleToUnionCaseType = + FSharpType.MakeFunctionType(tupleType, ty) + + let tupleToUnionCaseImpl: obj -> obj = + fun x -> + let values = + getNestedTupleFields + x + (Array.length caseFields) +#if FABLE_COMPILER_PYTHON + FSharpValue.MakeUnion(case, values) +#else + FSharpValue.MakeUnion( + case, + values, + allowAccessToPrivateRepresentation = true + ) +#endif + + let tupleToUnionCase: obj = + FSharpValue.MakeFunction( + tupleToUnionCaseType, + tupleToUnionCaseImpl + ) + + let prefix = + Decode.index 0 Decode.string + |> Decode.andThen (fun x -> + if x = case.Name then + Decode.succeed () + else + Decode.fail + $"Expected %s{case.Name} but found \"%s{x}\"" + ) + + let dec = + Decode.Generic.map + tupleType + ty + tupleToUnionCase + decoder + + let unitToUnionCaseDecoderType = + FSharpType.MakeFunctionType( + typeof, + makeDecoderType ty + ) + + let unitToUnionCaseDecoderImpl: obj -> obj = + fun _ -> dec + + let unitToUnionCaseImpl = + FSharpValue.MakeFunction( + unitToUnionCaseDecoderType, + unitToUnionCaseDecoderImpl + ) + + Decode.Generic.bind + typeof + ty + unitToUnionCaseImpl + prefix + ] + + let decoder = alternatives |> Seq.reduce (Decode.Generic.either ty) + + self <- decoder + + decoder +#endif + + and private genericTupleDecoder + (caseStyle: CaseStyle option) + (existingDecoders: Map) + (ty: Type) + = +#if FABLE_COMPILER + let elements = FSharpType.GetTupleElements(ty) + + let elementDecoders = + [| + for index, elementType in Array.indexed elements do + let decoder = + Decode.Generic.index + elementType + index + (generateDecoder + caseStyle + existingDecoders + elementType) + + elementType, decoder + |] + + let rec mergeDecoders (state: Type * obj) (next: Type * obj) = + let aggregateType, aggregateDecoder = state + let fieldType, nextDecoder = next + + let nextType = + FSharpType.MakeTupleType( + [| + aggregateType + fieldType + |] + ) + + let nextDecoder = + Decode.Generic.zip + aggregateType + fieldType + aggregateDecoder + nextDecoder + + nextType, nextDecoder + + let tupleType, decoder = elementDecoders |> Array.reduce mergeDecoders // There will always be at least one element + + let nestedTuplesToTupleImpl: obj -> obj = + fun x -> + let values = getNestedTupleFields x (Array.length elements) + FSharpValue.MakeTuple(values, ty) + + let nestedTuplesToTuple: obj = box nestedTuplesToTupleImpl + + Decode.Generic.map tupleType ty nestedTuplesToTuple decoder +#else + let elements = FSharpType.GetTupleElements(ty) + + let elementDecoders = + [| + for index, elementType in Array.indexed elements do + let decoder = + Decode.Generic.index + elementType + index + (generateDecoder + caseStyle + existingDecoders + elementType) + + elementType, decoder + |] + + let rec mergeDecoders (state: Type * obj) (next: Type * obj) = + let aggregateType, aggregateDecoder = state + let fieldType, nextDecoder = next + + let nextType = + FSharpType.MakeTupleType( + [| + aggregateType + fieldType + |] + ) + + let nextDecoder = + Decode.Generic.zip + aggregateType + fieldType + aggregateDecoder + nextDecoder + + nextType, nextDecoder + + let tupleType, decoder = elementDecoders |> Array.reduce mergeDecoders // There will always be at least one element + + let nestedTuplesToTupleType = FSharpType.MakeFunctionType(tupleType, ty) + + let nestedTuplesToTupleImpl: obj -> obj = + fun x -> + let values = getNestedTupleFields x (Array.length elements) + FSharpValue.MakeTuple(values, ty) + + let nestedTuplesToTuple: obj = + FSharpValue.MakeFunction( + nestedTuplesToTupleType, + nestedTuplesToTupleImpl + ) + + Decode.Generic.map tupleType ty nestedTuplesToTuple decoder +#endif + + let inline autoWithOptions<'t> + (caseStrategy: CaseStyle option) + (extra: ExtraCoders) + : Decoder<'t> + = + let ty = typeof<'t> + let decoder = generateDecoder caseStrategy extra.DecoderOverrides ty + unbox decoder + +#if !FABLE_COMPILER + open System.Threading +#endif + + type Auto = +#if FABLE_COMPILER + static let instance = Cache() +#else + static let instance = + new ThreadLocal<_>(fun () -> Cache()) +#endif + +#if FABLE_COMPILER + static member inline generateDecoder + ( + ?caseStrategy: CaseStyle, + ?extra: ExtraCoders + ) + = +#else + static member generateDecoder + ( + ?caseStrategy: CaseStyle, + ?extra: ExtraCoders + ) + = +#endif + let extra = defaultArg extra Extra.empty + autoWithOptions caseStrategy extra + +#if FABLE_COMPILER + static member inline generateDecoderCached<'T> +#else + static member generateDecoderCached<'T> +#endif + ( + ?caseStrategy: CaseStyle, + ?extra: ExtraCoders + ) + : Decoder<'T> + = + let extra = defaultArg extra Extra.empty + + let t = typeof<'T> + + let key = + t.FullName + |> (+) (Operators.string caseStrategy) + |> (+) extra.Hash + +#if FABLE_COMPILER + let cache = instance +#else + let cache = instance.Value +#endif + + cache.GetOrAdd( + key, + fun () -> + let dec: Decoder<'T> = autoWithOptions caseStrategy extra + box dec + ) + |> unbox diff --git a/packages/Thoth.Json.Auto/Domain.fs b/packages/Thoth.Json.Auto/Domain.fs new file mode 100644 index 0000000..1663222 --- /dev/null +++ b/packages/Thoth.Json.Auto/Domain.fs @@ -0,0 +1,84 @@ +namespace Thoth.Json.Auto + +open System +open System.Collections.Generic +open Thoth.Json.Core + +type CaseStyle = + | SnakeCase + | ScreamingSnakeCase + | PascalCase + | CamelCase + | DotNetPascalCase + | DotNetCamelCase + +type TypeKey = + private + | TypeKey of string + + static member Create(t: Type) = TypeKey t.FullName + +[] +module TypeKey = + + let ofType (t: Type) = TypeKey.Create(t) + +type Cache<'Value>() = + let cache = Dictionary() + + member this.GetOrAdd(key, factory) = + match cache.TryGetValue(key) with + | true, x -> x + | false, _ -> + let x = factory () + cache.Add(key, x) + x + +type BoxedEncoder = obj + +type BoxedDecoder = obj + +[] +type ExtraCoders = + { + Hash: string + EncoderOverrides: Map + DecoderOverrides: Map + } + +[] +module Extra = + + let empty = + { + Hash = "" + EncoderOverrides = Map.empty + DecoderOverrides = Map.empty + } + + let inline withCustom + (encoder: Encoder<'t>) + (decoder: Decoder<'t>) + (opts: ExtraCoders) + : ExtraCoders + = + let hash = Guid.NewGuid() + let typeKey = TypeKey.ofType typeof<'t> + + { + Hash = string hash + EncoderOverrides = opts.EncoderOverrides |> Map.add typeKey encoder + DecoderOverrides = opts.DecoderOverrides |> Map.add typeKey decoder + } + + let inline withInt64 (extra: ExtraCoders) : ExtraCoders = + withCustom Encode.int64 Decode.int64 extra + + let inline withUInt64 (extra: ExtraCoders) : ExtraCoders = + withCustom Encode.uint64 Decode.uint64 extra + + let inline withDecimal (extra: ExtraCoders) : ExtraCoders = + withCustom Encode.decimal Decode.decimal extra + + let inline withBigInt (extra: ExtraCoders) : ExtraCoders = + withCustom Encode.bigint Decode.bigint extra diff --git a/packages/Thoth.Json.Auto/Encode.fs b/packages/Thoth.Json.Auto/Encode.fs new file mode 100644 index 0000000..447893b --- /dev/null +++ b/packages/Thoth.Json.Auto/Encode.fs @@ -0,0 +1,1128 @@ +namespace Thoth.Json.Auto + +open System +open System.Reflection +open FSharp.Reflection +open Thoth.Json.Core +open Thoth.Json.Auto + +[] +module Encode = + + [] + module internal Encode = + + [] + module Generic = + + type EncodeHelpers = + static member OptionOf<'t> + (enc: Encoder<'t>) + : Encoder<'t option> + = + Encode.option enc + + static member Lazily<'t>(enc: Lazy>) : Encoder<'t> = + Encode.lazily enc + + static member SeqOf<'t>(enc: Encoder<'t>) : Encoder<'t seq> = + fun xs -> xs |> Seq.map enc |> Seq.toArray |> Encode.array + + static member ListOf<'t>(enc: Encoder<'t>) : Encoder<'t list> = + fun xs -> xs |> List.map enc |> Encode.list + + static member MapOf<'k, 'v when 'k: comparison> + ( + stringifyKey: 'k -> string, + enc: Encoder<'v> + ) + : Encoder> + = + fun m -> + [ + for KeyValue(k, v) in m do + stringifyKey k, enc v + ] + |> Encode.object + + static member MapAsArrayOf<'k, 'v when 'k: comparison> + ( + keyEncoder: Encoder<'k>, + valueEncoder: Encoder<'v> + ) + : Encoder> + = + fun m -> + [| + for KeyValue(k, v) in m do + Encode.tuple2 keyEncoder valueEncoder (k, v) + |] + |> Encode.array + + static member SetOf<'t when 't: comparison> + (enc: Encoder<'t>) + : Encoder> + = + fun xs -> xs |> Seq.map enc |> Seq.toArray |> Encode.array + + static member ArrayOf<'t> + (enc: Encoder<'t>) + : Encoder<'t array> + = + fun xs -> xs |> Array.map enc |> Encode.array + + static member EnumByte<'t when 't: enum>() : Encoder<'t> = + Encode.Enum.byte + + static member EnumSbyte<'t when 't: enum> + () + : Encoder<'t> + = + Encode.Enum.sbyte + + static member EnumInt16<'t when 't: enum> + () + : Encoder<'t> + = + Encode.Enum.int16 + + static member EnumUint16<'t when 't: enum> + () + : Encoder<'t> + = + Encode.Enum.uint16 + + static member EnumInt<'t when 't: enum>() : Encoder<'t> = + Encode.Enum.int + + static member EnumUint32<'t when 't: enum> + () + : Encoder<'t> + = + Encode.Enum.uint32 + + // static member Field<'t, 'u>(picker : 't -> 'u, fieldEncoder : Encoder<'u>) : Encode.IFieldEncoder<'t> = + // Encode.field picker fieldEncoder + + // static member Object<'t>(fields : seq>) : Encoder<'t> = + // Encode.object fields + + // static member Element<'t, 'u>(picker : 't -> 'u, elementEncoder : Encoder<'u>) : Encoder<'t> = + // Encode.element picker elementEncoder + + // static member FixedArray<'t>(elements : Encoder<'t> seq) : Encoder<'t> = + // Encode.fixedArray elements + + // static member Union<'t>(picker : 't -> Encode.ICase<'t>) : Encoder<'t> = + // Encode.union picker + + // static member Case<'t>(tag : string, data : Encode.ICaseData<'t> seq) : Encode.ICase<'t> = + // Encode.case tag data + +#if FABLE_COMPILER + let optionOf (innerType: Type) (enc: obj) : obj = + Encode.option (unbox enc) +#else + let private getGenericMethodDefinition (name: string) = + typeof + .GetMethods(BindingFlags.Static ||| BindingFlags.NonPublic) + |> Seq.filter (fun x -> x.Name = name) + |> Seq.exactlyOne + |> fun mi -> mi.GetGenericMethodDefinition() + + let private optionOfMethodDefinition = + getGenericMethodDefinition "OptionOf" + + let optionOf (innerType: Type) (enc: obj) : obj = + let methodInfo = + optionOfMethodDefinition.MakeGenericMethod(innerType) + + methodInfo.Invoke(null, [| enc |]) +#endif + +#if FABLE_COMPILER + let seqOf (innerType: Type) (enc: obj) : obj = + box (fun xs -> unbox xs |> Seq.map (unbox enc) |> Encode.seq) +#else + let private seqOfMethodDefinition = + getGenericMethodDefinition "SeqOf" + + let seqOf (innerType: Type) (enc: obj) : obj = + let methodInfo = + seqOfMethodDefinition.MakeGenericMethod(innerType) + + methodInfo.Invoke(null, [| enc |]) +#endif + +#if FABLE_COMPILER + let listOf (innerType: Type) (enc: obj) : obj = + box (fun xs -> unbox xs |> List.map (unbox enc) |> Encode.list) +#else + let private listOfMethodDefinition = + getGenericMethodDefinition "ListOf" + + let listOf (innerType: Type) (enc: obj) : obj = + let methodInfo = + listOfMethodDefinition.MakeGenericMethod(innerType) + + methodInfo.Invoke(null, [| enc |]) +#endif + +#if FABLE_COMPILER + let mapOf + (keyType: Type) + (valueType: Type) + (stringifyKey: obj) + (enc: obj) + : obj + = + (fun m -> + let stringifyKey = unbox stringifyKey + let enc = unbox enc + + m + |> unbox + |> Map.toSeq + |> Seq.map (fun (k, v) -> stringifyKey k, enc v) + |> Map.ofSeq + |> Encode.dict + ) + |> box +#else + let private mapOfMethodDefinition = + getGenericMethodDefinition "MapOf" + + let mapOf + (keyType: Type) + (valueType: Type) + (stringifyKey: obj) + (enc: obj) + : obj + = + let methodInfo = + mapOfMethodDefinition.MakeGenericMethod(keyType, valueType) + + methodInfo.Invoke( + null, + [| + stringifyKey + enc + |] + ) +#endif + +#if FABLE_COMPILER + let mapAsArrayOf + (keyType: Type) + (valueType: Type) + (keyEncoder: obj) + (valueEncoder: obj) + : obj + = + (fun xs -> + let enc = + Encode.tuple2 (unbox keyEncoder) (unbox valueEncoder) + + unbox xs |> Map.toList |> List.map enc |> Encode.list + ) + |> box +#else + let private mapAsArrayOfMethodDefinition = + getGenericMethodDefinition "MapAsArrayOf" + + let mapAsArrayOf + (keyType: Type) + (valueType: Type) + (keyEncoder: obj) + (valueEncoder: obj) + : obj + = + let methodInfo = + mapAsArrayOfMethodDefinition.MakeGenericMethod( + keyType, + valueType + ) + + methodInfo.Invoke( + null, + [| + keyEncoder + valueEncoder + |] + ) +#endif + +#if FABLE_COMPILER + let setOf (innerType: Type) (enc: obj) : obj = + box (fun xs -> + unbox xs + |> Seq.map (unbox enc) + |> Seq.toArray + |> Encode.array + ) +#else + let private setOfMethodDefinition = + getGenericMethodDefinition "SetOf" + + let setOf (innerType: Type) (enc: obj) : obj = + let methodInfo = + setOfMethodDefinition.MakeGenericMethod(innerType) + + methodInfo.Invoke(null, [| enc |]) +#endif + +#if FABLE_COMPILER + let arrayOf (innerType: Type) (enc: obj) : obj = + EncodeHelpers.ArrayOf(unbox enc) +#else + let private arrayOfMethodDefinition = + getGenericMethodDefinition "ArrayOf" + + let arrayOf (innerType: Type) (enc: obj) : obj = + let methodInfo = + arrayOfMethodDefinition.MakeGenericMethod(innerType) + + methodInfo.Invoke(null, [| enc |]) +#endif + +#if FABLE_COMPILER + let lazily (innerType: Type) (enc: obj) : obj = + Encode.lazily (unbox enc) +#else + let private lazilyMethodDefinition = + getGenericMethodDefinition "Lazily" + + let lazily (innerType: Type) (enc: obj) : obj = + let methodInfo = + lazilyMethodDefinition.MakeGenericMethod(innerType) + + methodInfo.Invoke(null, [| enc |]) +#endif + + module Enum = + +#if FABLE_COMPILER + let byte (innerType: Type) : obj = Encode.byte |> box +#else + let private enumByteGenericMethodDefinition = + getGenericMethodDefinition "EnumByte" + + let byte (innerType: Type) : obj = + enumByteGenericMethodDefinition + .MakeGenericMethod([| innerType |]) + .Invoke(null, [||]) +#endif + +#if FABLE_COMPILER + let sbyte (innerType: Type) : obj = Encode.sbyte |> box +#else + let private enumSbyteGenericMethodDefinition = + getGenericMethodDefinition "EnumSbyte" + + let sbyte (innerType: Type) : obj = + enumSbyteGenericMethodDefinition + .MakeGenericMethod([| innerType |]) + .Invoke(null, [||]) +#endif + +#if FABLE_COMPILER + let int16 (innerType: Type) : obj = Encode.int16 |> box +#else + let private enumInt16GenericMethodDefinition = + getGenericMethodDefinition "EnumInt16" + + let int16 (innerType: Type) : obj = + enumInt16GenericMethodDefinition + .MakeGenericMethod([| innerType |]) + .Invoke(null, [||]) +#endif + +#if FABLE_COMPILER + let uint16 (innerType: Type) : obj = Encode.uint16 |> box +#else + let private enumUint16GenericMethodDefinition = + getGenericMethodDefinition "EnumUint16" + + let uint16 (innerType: Type) : obj = + enumUint16GenericMethodDefinition + .MakeGenericMethod([| innerType |]) + .Invoke(null, [||]) +#endif + +#if FABLE_COMPILER + let int (innerType: Type) : obj = Encode.int |> box +#else + let private enumIntGenericMethodDefinition = + getGenericMethodDefinition "EnumInt" + + let int (innerType: Type) : obj = + enumIntGenericMethodDefinition + .MakeGenericMethod([| innerType |]) + .Invoke(null, [||]) +#endif + +#if FABLE_COMPILER + let uint32 (innerType: Type) : obj = Encode.uint32 |> box +#else + let private enumUint32GenericMethodDefinition = + getGenericMethodDefinition "EnumUint32" + + let uint32 (innerType: Type) : obj = + enumUint32GenericMethodDefinition + .MakeGenericMethod([| innerType |]) + .Invoke(null, [||]) +#endif + + // let private fieldMethodDefinition = getGenericMethodDefinition "Field" + + // let field (objectType : Type) (fieldType : Type) (picker : obj) (fieldEncoder : obj) : obj = + // let methodInfo = fieldMethodDefinition.MakeGenericMethod(objectType, fieldType) + // methodInfo.Invoke(null, [| picker; fieldEncoder |]) + + // let private objectMethodDefinition = getGenericMethodDefinition "Object" + + // let object (objectType : Type) (fields : obj) : obj = + // let methodInfo = objectMethodDefinition.MakeGenericMethod(objectType) + // methodInfo.Invoke(null, [| fields |]) + + // let private elementMethodDefinition = getGenericMethodDefinition "Element" + + // let element (objectType : Type) (elementType : Type) (picker : obj) (elementEncoder : obj) : obj = + // let methodInfo = elementMethodDefinition.MakeGenericMethod(objectType, elementType) + // methodInfo.Invoke(null, [| picker; elementEncoder |]) + + // let private fixedArrayMethodDefinition = getGenericMethodDefinition "FixedArray" + + // let fixedArray (objectType : Type) (elements : obj) : obj = + // let methodInfo = fixedArrayMethodDefinition.MakeGenericMethod(objectType) + // methodInfo.Invoke(null, [| elements |]) + + // let private unionMethodDefinition = getGenericMethodDefinition "Union" + + // let union (objectType : Type) (picker : obj) : obj = + // let methodInfo = unionMethodDefinition.MakeGenericMethod(objectType) + // methodInfo.Invoke(null, [| picker |]) + + // let private caseMethodDefinition = getGenericMethodDefinition "Case" + + // let case (objectType : Type) (tag : string) (data : obj) : obj = + // let methodInfo = caseMethodDefinition.MakeGenericMethod(objectType) + // methodInfo.Invoke(null, [| tag; data |]) + + // let private makeFieldEncoderType (ty : Type) : Type = + // typedefof>.MakeGenericType(ty) + + // let private makeEncodeCaseType (ty : Type) : Type = + // typedefof>.MakeGenericType(ty) + +#if !FABLE_COMPILER + let private makeEncoderType (ty: Type) : Type = + FSharpType.MakeFunctionType(ty, typeof) + // typedefof>.MakeGenericType(ty) +#endif + + let rec generateEncoder + (caseStyle: CaseStyle option) + (existingEncoders: Map) + (skipNullField: bool) + (ty: Type) + : BoxedEncoder + = + match Map.tryFind (TypeKey.ofType ty) existingEncoders with + | Some x -> x + | None -> + match ty with + | UnitType _ -> box Encode.unit + | IntType _ -> box Encode.int + | CharType _ -> box Encode.char + | StringType _ -> box Encode.string + | BoolType _ -> box Encode.bool + | ByteType _ -> box Encode.byte + | SByteType _ -> box Encode.sbyte + | UInt16Type _ -> box Encode.uint16 + | Int16Type _ -> box Encode.int16 + | Int64Type _ -> box Encode.int64 + | UIntType _ -> box Encode.uint32 + | UInt64Type _ -> box Encode.uint64 + | BigIntType _ -> box Encode.bigint + | SingleType _ -> box Encode.float32 + | DoubleType _ -> box Encode.float + | DecimalType _ -> box Encode.decimal + | GuidType _ -> box (fun (g: Guid) -> Encode.guid g) + | TimeSpanType _ -> box (fun (ts: TimeSpan) -> Encode.timespan ts) + | DateTimeType _ -> box Encode.datetime + | DateTimeOffsetType _ -> box Encode.datetimeOffset + | OptionType innerType -> + let innerEncoder = + generateEncoder + caseStyle + existingEncoders + skipNullField + innerType + + Encode.Generic.optionOf innerType innerEncoder + | SeqType innerType -> + let innerEncoder = + generateEncoder + caseStyle + existingEncoders + skipNullField + innerType + + Encode.Generic.seqOf innerType innerEncoder + | ListType innerType -> + let innerEncoder = + generateEncoder + caseStyle + existingEncoders + skipNullField + innerType + + Encode.Generic.listOf innerType innerEncoder + | MapType(StringType, valueType) -> + let valueEncoder = + generateEncoder + caseStyle + existingEncoders + skipNullField + valueType + + let stringifyKey = fun (s: string) -> s + + Encode.Generic.mapOf + typeof + valueType + stringifyKey + valueEncoder + | MapType(GuidType, valueType) -> + let valueEncoder = + generateEncoder + caseStyle + existingEncoders + skipNullField + valueType + + let stringifyKey = fun (g: Guid) -> string g + + Encode.Generic.mapOf + typeof + valueType + stringifyKey + valueEncoder + | MapType(keyType, valueType) -> + let keyEncoder = + generateEncoder + caseStyle + existingEncoders + skipNullField + keyType + + let valueEncoder = + generateEncoder + caseStyle + existingEncoders + skipNullField + valueType + + Encode.Generic.mapAsArrayOf + keyType + valueType + keyEncoder + valueEncoder + | SetType innerType -> + let innerEncoder = + generateEncoder + caseStyle + existingEncoders + skipNullField + innerType + + Encode.Generic.setOf innerType innerEncoder + | ArrayType innerType -> + let innerEncoder = + generateEncoder + caseStyle + existingEncoders + skipNullField + innerType + + Encode.Generic.arrayOf innerType innerEncoder + | FSharpRecordType _ -> + generateEncoderForRecord + caseStyle + existingEncoders + skipNullField + ty + | FSharpUnionType _ -> + generateEncoderForUnion + caseStyle + existingEncoders + skipNullField + ty + | FSharpTupleType _ -> + generateEncoderForTuple + caseStyle + existingEncoders + skipNullField + ty + | EnumType(ByteType _) -> Encode.Generic.Enum.byte ty + | EnumType(SByteType _) -> Encode.Generic.Enum.sbyte ty + | EnumType(Int16Type _) -> Encode.Generic.Enum.int16 ty + | EnumType(UInt16Type _) -> Encode.Generic.Enum.uint16 ty + | EnumType(IntType _) -> Encode.Generic.Enum.int ty + | EnumType(UIntType _) -> Encode.Generic.Enum.uint32 ty + | _ -> failwith $"Unsupported type %s{ty.FullName}" + + and generateEncoderForRecord + (caseStyle: CaseStyle option) + (existingEncoders: Map) + (skipNullField: bool) + (ty: Type) + : obj + = +#if FABLE_COMPILER + let mutable self = Unchecked.defaultof<_> + + let existingEncoders = + if Type.isRecursive ty then + let lazySelf = + Encode.Generic.lazily + ty + (Lazy.makeGeneric (typeof obj>) ((fun _ -> self))) + + existingEncoders |> Map.add (TypeKey.ofType ty) lazySelf + else + existingEncoders + + let recordFieldsWithEncoders = + [| + let recordFields = + match ty with + | FSharpRecordType fields -> fields + | _ -> failwith $"Expected an F# record type" + + for pi in recordFields do + let fieldEncoder: obj -> IEncodable = + generateEncoder + caseStyle + existingEncoders + skipNullField + pi.PropertyType + |> unbox + + let reader = + fun (record: obj) -> + FSharpValue.GetRecordField(record, pi) + + let readAndEncode (record: obj) = + let value = reader record + + if skipNullField && isNull value then + None + else + fieldEncoder value |> Some + + pi.Name, readAndEncode + |] + + let encoder: obj -> obj = + fun o -> + let fields = + [| + for fieldName, readAndEncode in recordFieldsWithEncoders do + let encodedFieldName = + match caseStyle with + | Some caseStyle -> + Casing.convertCase + DotNetPascalCase + caseStyle + fieldName + | None -> fieldName + + match readAndEncode o with + | Some encoded -> encodedFieldName, encoded + | None -> () + |] + + Encode.object fields + + self <- box encoder + + box encoder +#else + let mutable self = Unchecked.defaultof<_> + + let existingEncoders = + if Type.isRecursive ty then + let lazySelf = + Encode.Generic.lazily + ty + (Lazy.makeGeneric + (makeEncoderType ty) + (FSharpValue.MakeFunction( + FSharpType.MakeFunctionType( + typeof, + makeEncoderType ty + ), + (fun _ -> self) + ))) + + existingEncoders |> Map.add (TypeKey.ofType ty) lazySelf + else + existingEncoders + + let funcType = makeEncoderType ty + + let recordFieldsWithEncoders = + [| + let recordFields = + match ty with + | FSharpRecordType fields -> fields + | _ -> failwith $"Expected an F# record type" + + for pi in recordFields do + let fieldEncoder = + generateEncoder + caseStyle + existingEncoders + skipNullField + pi.PropertyType + + let invokeMethodInfo = + fieldEncoder.GetType().GetMethods() + |> Array.find (fun x -> + x.Name = "Invoke" + && x.ReturnType = typedefof + ) + + let reader = FSharpValue.PreComputeRecordFieldReader(pi) + + let readAndEncode (record: obj) = + let value = reader record + + if skipNullField && isNull value then + None + else + invokeMethodInfo.Invoke(fieldEncoder, [| value |]) + :?> IEncodable + |> Some + + pi.Name, readAndEncode + |] + + let funcImpl: obj -> obj = + fun o -> + let fields = + [| + for fieldName, readAndEncode in recordFieldsWithEncoders do + let encodedFieldName = + match caseStyle with + | Some caseStyle -> + Casing.convertCase + DotNetPascalCase + caseStyle + fieldName + | None -> fieldName + + match readAndEncode o with + | Some encoded -> encodedFieldName, encoded + | None -> () + |] + + Encode.object fields + + let encoder = FSharpValue.MakeFunction(funcType, funcImpl) + + self <- encoder + + encoder +#endif + + and generateEncoderForUnion + (caseStyle: CaseStyle option) + (existingEncoders: Map) + (skipNullField: bool) + (ty: Type) + : obj + = +#if FABLE_COMPILER + let mutable self = Unchecked.defaultof<_> + + let existingEncoders = + if Type.isRecursive ty then + let lazySelf = + Encode.Generic.lazily + ty + (Lazy.makeGeneric (typeof obj>) ((fun _ -> self))) + + existingEncoders |> Map.add (TypeKey.ofType ty) lazySelf + else + existingEncoders + + let unionCases = + match ty with + | FSharpUnionType cases -> cases + | _ -> + failwith $"Expected an F# union type but found %s{ty.FullName}" + + let caseEncoders = + [| + for unionCase in unionCases do + let name = + match ty with +#if !FABLE_COMPILER +#endif + | _ -> unionCase.Name + + let encodedUnionCaseName = + match caseStyle with + | Some caseStyle -> + Casing.convertCase + DotNetPascalCase + caseStyle + unionCase.Name + | None -> unionCase.Name + + let caseHasData = + unionCase.GetFields() |> Seq.isEmpty |> not + + if caseHasData then + let fieldEncoders = + [| + for pi in unionCase.GetFields() do + let encoder: obj -> IEncodable = + generateEncoder + caseStyle + existingEncoders + skipNullField + pi.PropertyType + |> unbox + + encoder + |] + + let n = Array.length fieldEncoders - 1 + + fun o -> + let _, values = FSharpValue.GetUnionFields(o, ty) + + Encode.array + [| + Encode.string encodedUnionCaseName + + for i = 0 to n do + let value = values[i] + + let encoder: obj -> IEncodable = + unbox fieldEncoders[i] + + encoder value + |] + else + fun _ -> Encode.string encodedUnionCaseName + |] + + let encoder: obj -> obj = + fun o -> + let caseInfo, _ = FSharpValue.GetUnionFields(o, ty) + let tag = caseInfo.Tag + let caseEncoder = caseEncoders[tag] + + caseEncoder o + + self <- encoder + + encoder +#else + let mutable self = Unchecked.defaultof<_> + + let funcType = makeEncoderType ty + + let existingEncoders = + if Type.isRecursive ty then + let lazySelf = + Encode.Generic.lazily + ty + (Lazy.makeGeneric + (makeEncoderType ty) + (FSharpValue.MakeFunction( + FSharpType.MakeFunctionType( + typeof, + makeEncoderType ty + ), + (fun _ -> self) + ))) + + existingEncoders |> Map.add (TypeKey.ofType ty) lazySelf + else + existingEncoders + + let unionCases = + match ty with + | FSharpUnionType cases -> cases + | _ -> + failwith $"Expected an F# union type but found %s{ty.FullName}" + + let tagReader = + FSharpValue.PreComputeUnionTagReader( + ty, + allowAccessToPrivateRepresentation = true + ) + + let caseEncoders = + [| + for unionCase in unionCases do + let fromCase, name = + match ty with + | StringEnum ty -> + match unionCase with + | CompiledName name -> DotNetPascalCase, name + | _ -> + match ty.ConstructorArguments with + | LowerFirst -> + let name = + unionCase.Name.[..0].ToLowerInvariant() + + unionCase.Name.[1..] + + DotNetCamelCase, name + | Forward -> DotNetPascalCase, unionCase.Name + | _ -> DotNetPascalCase, unionCase.Name + + let encodedUnionCaseName = + match caseStyle with + | Some caseStyle -> + Casing.convertCase fromCase caseStyle name + | None -> name + + let caseHasData = + unionCase.GetFields() |> Seq.isEmpty |> not + + if caseHasData then + let unionReader = + FSharpValue.PreComputeUnionReader( + unionCase, + allowAccessToPrivateRepresentation = true + ) + + let fieldEncoders = + [| + for pi in unionCase.GetFields() do + let encoder = + generateEncoder + caseStyle + existingEncoders + skipNullField + pi.PropertyType + + let invokeMethodInfo = + encoder.GetType().GetMethods() + |> Array.find (fun x -> + x.Name = "Invoke" + && x.ReturnType = typeof + ) + + fun o -> + invokeMethodInfo.Invoke( + encoder, + [| o |] + ) + :?> IEncodable + |] + + let n = Array.length fieldEncoders - 1 + + fun o -> + let values = unionReader o + + Encode.array + [| + Encode.string encodedUnionCaseName + + for i = 0 to n do + let value = values[i] + let encoder = fieldEncoders[i] + + encoder value + |] + else + fun _ -> Encode.string encodedUnionCaseName + |] + + let funcImpl: obj -> obj = + fun o -> + let tag = tagReader o + let caseEncoder = caseEncoders[tag] + + caseEncoder o + + let encoder = FSharpValue.MakeFunction(funcType, funcImpl) + + self <- encoder + + encoder +#endif + + and generateEncoderForTuple + (caseStyle: CaseStyle option) + (existingEncoders: Map) + (skipNullField: bool) + (ty: Type) + : obj + = +#if FABLE_COMPILER + let encoders = + [| + for elementType in FSharpType.GetTupleElements(ty) do + let elementEncoder = + generateEncoder + caseStyle + existingEncoders + skipNullField + elementType + + box elementEncoder + |] + + let funcImpl: obj -> obj = + fun o -> + let values: ResizeArray = unbox o + + Encode.array + [| + for i = 0 to Array.length encoders - 1 do + let value = unbox values[i] + let encode: obj -> IEncodable = unbox encoders[i] + + encode value + |] + + box funcImpl +#else + let funcType = makeEncoderType ty + + let reader = FSharpValue.PreComputeTupleReader(ty) + + let encoders = + [| + for elementType in FSharpType.GetTupleElements(ty) do + let elementEncoder = + generateEncoder + caseStyle + existingEncoders + skipNullField + elementType + + let invokeMethodInfo = + elementEncoder.GetType().GetMethods() + |> Array.find (fun x -> + x.Name = "Invoke" + && x.ReturnType = typedefof + ) + + let encode (value: obj) = + invokeMethodInfo.Invoke(elementEncoder, [| value |]) + :?> IEncodable + + encode + |] + + let n = Array.length encoders - 1 + + let funcImpl: obj -> obj = + fun o -> + let values = reader o + + let elements = + [| + for i = 0 to n do + let value = values[i] + let encode = encoders[i] + + encode value + |] + + Encode.array elements + + FSharpValue.MakeFunction(funcType, funcImpl) +#endif + + let inline autoWithOptions<'t> + (caseStrategy: CaseStyle option) + (extra: ExtraCoders) + (skipNullField: bool) + : Encoder<'t> + = + let ty = typeof<'t> + + let encoder = + generateEncoder caseStrategy extra.EncoderOverrides skipNullField ty + + unbox encoder + +#if !FABLE_COMPILER + open System.Threading +#endif + + type Auto = +#if FABLE_COMPILER + static let instance = Cache() +#else + static let instance = + new ThreadLocal<_>(fun () -> Cache()) +#endif + +#if FABLE_COMPILER + static member inline generateEncoder<'T> +#else + static member generateEncoder<'T> +#endif + ( + ?caseStrategy: CaseStyle, + ?extra: ExtraCoders, + ?skipNullField: bool + ) + : Encoder<'T> + = + let extra = defaultArg extra Extra.empty + let skipNullField = defaultArg skipNullField true + + autoWithOptions caseStrategy extra skipNullField + +#if FABLE_COMPILER + static member inline generateEncoderCached<'T> +#else + static member generateEncoderCached<'T> +#endif + ( + ?caseStrategy: CaseStyle, + ?extra: ExtraCoders, + ?skipNullField: bool + ) + : Encoder<'T> + = + let extra = defaultArg extra Extra.empty + let skipNullField = defaultArg skipNullField true + + let t = typeof<'T> + + let key = + t.FullName + |> (+) (Operators.string caseStrategy) + |> (+) (Operators.string skipNullField) + |> (+) extra.Hash + +#if FABLE_COMPILER + let cache = instance +#else + let cache = instance.Value +#endif + + cache.GetOrAdd( + key, + fun () -> + let enc: Encoder<'T> = + autoWithOptions caseStrategy extra skipNullField + + box enc + ) + |> unbox diff --git a/packages/Thoth.Json.Auto/Prelude.fs b/packages/Thoth.Json.Auto/Prelude.fs new file mode 100644 index 0000000..2e2763f --- /dev/null +++ b/packages/Thoth.Json.Auto/Prelude.fs @@ -0,0 +1,435 @@ +namespace Thoth.Json.Auto + +open System +open System.Collections.Generic +open FSharp.Reflection + +[] +module internal Prelude = + + type FSharpValue with + + static member MakeList(elementType: Type, elements: seq) : obj = + let objListType = typedefof + let listType = objListType.MakeGenericType(elementType) + let ucis = FSharpType.GetUnionCases(listType) + let emptyUci = ucis |> Seq.find (fun uci -> uci.Name = "Empty") + let consUci = ucis |> Seq.find (fun uci -> uci.Name = "Cons") + let empty = FSharpValue.MakeUnion(emptyUci, [||]) + + elements + |> Seq.rev + |> Seq.fold + (fun acc x -> + FSharpValue.MakeUnion( + consUci, + [| + x + acc + |] + ) + ) + empty + + [] + module Map = + + let merge (a: Map<'k, 'v>) (b: Map<'k, 'v>) = + if a = b then + a + else + seq { + for KeyValue(k, v) in a do + yield k, v + + for KeyValue(k, v) in b do + yield k, v + } + |> Map.ofSeq + + [] + module Lazy = + +#if FABLE_COMPILER + let makeGeneric (ty: Type) (func: obj) : obj = + let factory: unit -> obj = unbox func + Lazy(factory) |> box +#else + open System.Reflection + + type private LazyHelper = + static member Create<'t>(factory: unit -> 't) : Lazy<'t> = + Lazy<'t>(factory, true) + + let lazyCreateMethodDefinition = + typeof + .GetMethods(BindingFlags.Static ||| BindingFlags.NonPublic) + |> Seq.filter (fun x -> x.Name = "Create") + |> Seq.exactlyOne + |> fun mi -> mi.GetGenericMethodDefinition() + + /// Creates a lazy value of the given type. + /// ty is the type of 't + /// func is a boxed value of unit -> 't + let makeGeneric (ty: Type) (func: obj) = + lazyCreateMethodDefinition + .MakeGenericMethod([| ty |]) + .Invoke(null, [| func |]) +#endif + + [] + module internal ActivePatterns = + + let (|UnitType|_|) (ty: Type) = + if ty.FullName = typeof.FullName then + Some() + else + None + + let (|StringType|_|) (ty: Type) = + if ty.FullName = typeof.FullName then + Some() + else + None + + let (|CharType|_|) (ty: Type) = + if ty.FullName = typeof.FullName then + Some() + else + None + + let (|IntType|_|) (ty: Type) = + if ty.FullName = typeof.FullName then + Some() + else + None + + let (|BoolType|_|) (ty: Type) = + if ty.FullName = typeof.FullName then + Some() + else + None + + let (|Int64Type|_|) (ty: Type) = + if ty.FullName = typeof.FullName then + Some() + else + None + + let (|SingleType|_|) (ty: Type) = + if ty.FullName = typeof.FullName then + Some() + else + None + + let (|DoubleType|_|) (ty: Type) = + if ty.FullName = typeof.FullName then + Some() + else + None + + let (|DecimalType|_|) (ty: Type) = + if ty.FullName = typeof.FullName then + Some() + else + None + + let (|ByteType|_|) (ty: Type) = + if ty.FullName = typeof.FullName then + Some() + else + None + + let (|SByteType|_|) (ty: Type) = + if ty.FullName = typeof.FullName then + Some() + else + None + + let (|UInt16Type|_|) (ty: Type) = + if ty.FullName = typeof.FullName then + Some() + else + None + + let (|Int16Type|_|) (ty: Type) = + if ty.FullName = typeof.FullName then + Some() + else + None + + let (|UIntType|_|) (ty: Type) = + if ty.FullName = typeof.FullName then + Some() + else + None + + let (|UInt64Type|_|) (ty: Type) = + if ty.FullName = typeof.FullName then + Some() + else + None + + let (|BigIntType|_|) (ty: Type) = + if ty.FullName = typeof.FullName then + Some() + else + None + + let (|GuidType|_|) (ty: Type) = + if ty.FullName = typeof.FullName then + Some() + else + None + + let (|TimeSpanType|_|) (ty: Type) = + if ty.FullName = typeof.FullName then + Some() + else + None + + let (|DateTimeType|_|) (ty: Type) = + if ty.FullName = typeof.FullName then + Some() + else + None + + let (|DateTimeOffsetType|_|) (ty: Type) = + if ty.FullName = typeof.FullName then + Some() + else + None + + let (|OptionType|_|) (ty: Type) = + if + ty.IsGenericType + && ty.GetGenericTypeDefinition() = typedefof + then + Some(ty.GetGenericArguments()[0]) + else + None + + let (|ListType|_|) (ty: Type) = + if + ty.IsGenericType + && ty.GetGenericTypeDefinition() = typedefof + then + Some(ty.GetGenericArguments()[0]) + else + None + + let (|MapType|_|) (ty: Type) = + if + ty.IsGenericType + && ty.GetGenericTypeDefinition() = typedefof> + then + Some(ty.GetGenericArguments()[0], ty.GetGenericArguments()[1]) + else + None + + let (|ArrayType|_|) (ty: Type) = +#if FABLE_COMPILER + if ty.IsArray then + Some(ty.GetElementType()) + else + None +#else + if ty.IsArray && ty.GetArrayRank() = 1 then + Some(ty.GetElementType()) + else + None +#endif + + let (|SeqType|_|) (ty: Type) = + if + ty.IsGenericType + && ty.GetGenericTypeDefinition() = typedefof + then + Some(ty.GetGenericArguments()[0]) + else + None + + let (|SetType|_|) (ty: Type) = + if + ty.IsGenericType + && ty.GetGenericTypeDefinition() = typedefof> + then + Some(ty.GetGenericArguments()[0]) + else + None + + let (|FSharpRecordType|_|) (ty: Type) = +#if FABLE_COMPILER_PYTHON + if FSharpType.IsRecord(ty) then + Some(FSharpType.GetRecordFields(ty)) + else + None +#else + if FSharpType.IsRecord(ty, true) then + Some( + FSharpType.GetRecordFields( + ty, + allowAccessToPrivateRepresentation = true + ) + ) + else + None +#endif + + let (|FSharpUnionType|_|) (ty: Type) = +#if FABLE_COMPILER_PYTHON + if FSharpType.IsUnion(ty) then + Some(FSharpType.GetUnionCases(ty)) + else + None +#else + if FSharpType.IsUnion(ty, true) then + Some( + FSharpType.GetUnionCases( + ty, + allowAccessToPrivateRepresentation = true + ) + ) + else + None +#endif + + let (|FSharpTupleType|_|) (ty: Type) = + if FSharpType.IsTuple(ty) then + Some(FSharpType.GetTupleElements(ty)) + else + None + + let (|EnumType|_|) (ty: Type) = + if ty.IsEnum then + Some(ty.GetEnumUnderlyingType()) + else + None + +#if !FABLE_COMPILER + let (|StringEnum|_|) (ty: Type) = + ty.CustomAttributes + |> Seq.tryPick ( + function + | attr when + attr.AttributeType.FullName = "Fable.Core.StringEnumAttribute" + -> + Some attr + | _ -> None + ) +#endif + +#if !FABLE_COMPILER + let (|CompiledName|_|) (caseInfo: UnionCaseInfo) = + caseInfo.GetCustomAttributes() + |> Seq.tryPick ( + function + | :? CompiledNameAttribute as att -> Some att.CompiledName + | _ -> None + ) +#endif + +#if !FABLE_COMPILER + let (|LowerFirst|Forward|) + (args: IList) + = + args + |> Seq.tryPick ( + function + | rule when + rule.ArgumentType.FullName = typeof + .FullName + -> + Some rule + | _ -> None + ) + |> function + | Some rule -> + match rule.Value with + | :? int as value -> + match value with + | 0 -> Forward + | 1 -> LowerFirst + | _ -> LowerFirst // should not happen + | _ -> LowerFirst // should not happen + | None -> LowerFirst +#endif + + [] + module Type = + + let isRecursive (ty: Type) = + let rec loop (seen: Set) (current: Type) = + match current with + | FSharpTupleType elementTypes -> + if Array.contains ty elementTypes then + true + else + let seenNext = + Set.union + seen + (elementTypes + |> Array.map (fun ty -> ty.FullName) + |> Set.ofArray) + + elementTypes + |> Seq.filter (fun ty -> + not (Set.contains ty.FullName seen) + ) + |> Seq.exists (fun ty -> loop seenNext ty) + | current when + current.IsGenericType + && current.GetGenericTypeDefinition() = typedefof + -> + let elementType = + current.GetGenericArguments() |> Array.head + + if elementType = ty then + true + else + let seenNext = Set.add elementType.FullName seen + + loop seenNext elementType + | FSharpRecordType fields -> + let fieldTypes = + fields |> Array.map (fun pi -> pi.PropertyType) + + if Array.contains ty fieldTypes then + true + else + let seenNext = + Set.union + seen + (fieldTypes + |> Array.map (fun ty -> ty.FullName) + |> Set.ofArray) + + fieldTypes + |> Seq.filter (fun ty -> + not (Set.contains ty.FullName seen) + ) + |> Seq.exists (fun ty -> loop seenNext ty) + | FSharpUnionType fields -> + let fieldTypes = + fields + |> Array.collect (fun uci -> uci.GetFields()) + |> Array.map (fun pi -> pi.PropertyType) + + if Array.contains ty fieldTypes then + true + else + let seenNext = + Set.union + seen + (fieldTypes + |> Array.map (fun ty -> ty.FullName) + |> Set.ofArray) + + fieldTypes + |> Seq.filter (fun ty -> + not (Set.contains ty.FullName seen) + ) + |> Seq.exists (fun ty -> loop seenNext ty) + | _ -> false + + loop Set.empty ty diff --git a/packages/Thoth.Json.Auto/Thoth.Json.Auto.fsproj b/packages/Thoth.Json.Auto/Thoth.Json.Auto.fsproj new file mode 100644 index 0000000..6970028 --- /dev/null +++ b/packages/Thoth.Json.Auto/Thoth.Json.Auto.fsproj @@ -0,0 +1,20 @@ + + + + netstandard2.0 + true + + + + + + + + + + + + + + + diff --git a/packages/Thoth.Json.Auto/packages.lock.json b/packages/Thoth.Json.Auto/packages.lock.json new file mode 100644 index 0000000..de129a1 --- /dev/null +++ b/packages/Thoth.Json.Auto/packages.lock.json @@ -0,0 +1,105 @@ +{ + "version": 2, + "dependencies": { + ".NETStandard,Version=v2.0": { + "DotNet.ReproducibleBuilds": { + "type": "Direct", + "requested": "[1.1.1, )", + "resolved": "1.1.1", + "contentHash": "+H2t/t34h6mhEoUvHi8yGXyuZ2GjSovcGYehJrS2MDm2XgmPfZL2Sdxg+uL2lKgZ4M6tTwKHIlxOob2bgh0NRQ==", + "dependencies": { + "Microsoft.SourceLink.AzureRepos.Git": "1.1.1", + "Microsoft.SourceLink.Bitbucket.Git": "1.1.1", + "Microsoft.SourceLink.GitHub": "1.1.1", + "Microsoft.SourceLink.GitLab": "1.1.1" + } + }, + "FSharp.Core": { + "type": "Direct", + "requested": "[5.0.0, )", + "resolved": "5.0.0", + "contentHash": "iHoYXA0VaSQUONGENB1aVafjDDZDZpwu39MtaRCTrmwFW/cTcK0b2yKNVYneFHJMc3ChtsSoM9lNtJ1dYXkHfA==" + }, + "NETStandard.Library": { + "type": "Direct", + "requested": "[2.0.3, )", + "resolved": "2.0.3", + "contentHash": "st47PosZSHrjECdjeIzZQbzivYBJFv6P2nv4cj2ypdI204DO+vZ7l5raGMiX4eXMJ53RfOIg+/s4DHVZ54Nu2A==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0" + } + }, + "Microsoft.Build.Tasks.Git": { + "type": "Transitive", + "resolved": "1.1.1", + "contentHash": "AT3HlgTjsqHnWpBHSNeR0KxbLZD7bztlZVj7I8vgeYG9SYqbeFGh0TM/KVtC6fg53nrWHl3VfZFvb5BiQFcY6Q==" + }, + "Microsoft.NETCore.Platforms": { + "type": "Transitive", + "resolved": "1.1.0", + "contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==" + }, + "Microsoft.SourceLink.AzureRepos.Git": { + "type": "Transitive", + "resolved": "1.1.1", + "contentHash": "qB5urvw9LO2bG3eVAkuL+2ughxz2rR7aYgm2iyrB8Rlk9cp2ndvGRCvehk3rNIhRuNtQaeKwctOl1KvWiklv5w==", + "dependencies": { + "Microsoft.Build.Tasks.Git": "1.1.1", + "Microsoft.SourceLink.Common": "1.1.1" + } + }, + "Microsoft.SourceLink.Bitbucket.Git": { + "type": "Transitive", + "resolved": "1.1.1", + "contentHash": "cDzxXwlyWpLWaH0em4Idj0H3AmVo3L/6xRXKssYemx+7W52iNskj/SQ4FOmfCb8YQt39otTDNMveCZzYtMoucQ==", + "dependencies": { + "Microsoft.Build.Tasks.Git": "1.1.1", + "Microsoft.SourceLink.Common": "1.1.1" + } + }, + "Microsoft.SourceLink.Common": { + "type": "Transitive", + "resolved": "1.1.1", + "contentHash": "WMcGpWKrmJmzrNeuaEb23bEMnbtR/vLmvZtkAP5qWu7vQsY59GqfRJd65sFpBszbd2k/bQ8cs8eWawQKAabkVg==" + }, + "Microsoft.SourceLink.GitHub": { + "type": "Transitive", + "resolved": "1.1.1", + "contentHash": "IaJGnOv/M7UQjRJks7B6p7pbPnOwisYGOIzqCz5ilGFTApZ3ktOR+6zJ12ZRPInulBmdAf1SrGdDG2MU8g6XTw==", + "dependencies": { + "Microsoft.Build.Tasks.Git": "1.1.1", + "Microsoft.SourceLink.Common": "1.1.1" + } + }, + "Microsoft.SourceLink.GitLab": { + "type": "Transitive", + "resolved": "1.1.1", + "contentHash": "tvsg47DDLqqedlPeYVE2lmiTpND8F0hkrealQ5hYltSmvruy/Gr5nHAKSsjyw5L3NeM/HLMI5ORv7on/M4qyZw==", + "dependencies": { + "Microsoft.Build.Tasks.Git": "1.1.1", + "Microsoft.SourceLink.Common": "1.1.1" + } + }, + "thoth.json.core": { + "type": "Project", + "dependencies": { + "FSharp.Core": "[5.0.0, )", + "Fable.Core": "[4.1.0, )", + "Fable.Package.SDK": "[0.1.0, )" + } + }, + "Fable.Core": { + "type": "CentralTransitive", + "requested": "[4.1.0, )", + "resolved": "4.1.0", + "contentHash": "NISAbAVGEcvH2s+vHLSOCzh98xMYx4aIadWacQdWPcQLploxpSQXLEe9SeszUBhbHa73KMiKREsH4/W3q4A4iA==" + }, + "Fable.Package.SDK": { + "type": "CentralTransitive", + "requested": "[0.1.0, )", + "resolved": "0.1.0", + "contentHash": "wrEcGovUimN0PRGgVHlX/gsqCm5d/p9eOG74iaHoteX2dsFZQ9P7d066LRAl5Gj7GUHy7azLyDE41KFvZx1v9A==" + } + } + } +} \ No newline at end of file diff --git a/packages/Thoth.Json.Core/Decode.fs b/packages/Thoth.Json.Core/Decode.fs index c692281..a78242c 100644 --- a/packages/Thoth.Json.Core/Decode.fs +++ b/packages/Thoth.Json.Core/Decode.fs @@ -782,7 +782,6 @@ module Decode = ("", BadPrimitive("an array", value)) |> Error } - let keys: Decoder = { new Decoder with member _.Decode(helpers, value) = @@ -885,6 +884,23 @@ module Decode = runner decoders [] } + [] + type private LazyDecoder<'t>(x: Lazy>) = + struct + end + + interface Decoder<'t> with + member this.Decode<'json> + ( + helpers: IDecoderHelpers<'json>, + json: 'json + ) + = + let decoder = x.Force() + decoder.Decode(helpers, json) + + let lazily (x: Lazy>) : Decoder<'t> = LazyDecoder(x) :> _ + ///////////////////// // Map functions /// /////////////////// @@ -1394,36 +1410,99 @@ module Decode = let inline byte<'TEnum when 'TEnum: enum> : Decoder<'TEnum> = byte |> andThen (fun value -> - LanguagePrimitives.EnumOfValue value |> succeed + if System.Enum.IsDefined(typeof<'TEnum>, value) then + LanguagePrimitives.EnumOfValue<_, 'TEnum> value |> succeed + else + { new Decoder<_> with + member _.Decode<'JsonValue>(_, value: 'JsonValue) = + ("", + BadPrimitiveExtra( + typeof<'TEnum>.FullName, + value, + "Unkown value provided for the enum" + )) + |> Error + } ) let inline sbyte<'TEnum when 'TEnum: enum> : Decoder<'TEnum> = sbyte |> andThen (fun value -> - LanguagePrimitives.EnumOfValue value |> succeed + if System.Enum.IsDefined(typeof<'TEnum>, value) then + LanguagePrimitives.EnumOfValue<_, 'TEnum> value |> succeed + else + { new Decoder<_> with + member _.Decode<'JsonValue>(_, value: 'JsonValue) = + ("", + BadPrimitiveExtra( + typeof<'TEnum>.FullName, + value, + "Unkown value provided for the enum" + )) + |> Error + } ) let inline int16<'TEnum when 'TEnum: enum> : Decoder<'TEnum> = int16 |> andThen (fun value -> - LanguagePrimitives.EnumOfValue value |> succeed + if System.Enum.IsDefined(typeof<'TEnum>, value) then + LanguagePrimitives.EnumOfValue<_, 'TEnum> value |> succeed + else + { new Decoder<_> with + member _.Decode<'JsonValue>(_, value: 'JsonValue) = + ("", + BadPrimitiveExtra( + typeof<'TEnum>.FullName, + value, + "Unkown value provided for the enum" + )) + |> Error + } ) let inline uint16<'TEnum when 'TEnum: enum> : Decoder<'TEnum> = uint16 |> andThen (fun value -> - LanguagePrimitives.EnumOfValue value |> succeed + if System.Enum.IsDefined(typeof<'TEnum>, value) then + LanguagePrimitives.EnumOfValue<_, 'TEnum> value |> succeed + else + { new Decoder<_> with + member _.Decode<'JsonValue>(_, value: 'JsonValue) = + ("", + BadPrimitiveExtra( + typeof<'TEnum>.FullName, + value, + "Unkown value provided for the enum" + )) + |> Error + } ) let inline int<'TEnum when 'TEnum: enum> : Decoder<'TEnum> = int |> andThen (fun value -> - LanguagePrimitives.EnumOfValue value |> succeed + if System.Enum.IsDefined(typeof<'TEnum>, value) then + LanguagePrimitives.EnumOfValue<_, 'TEnum> value |> succeed + else + { new Decoder<_> with + member _.Decode<'JsonValue>(_, value: 'JsonValue) = + ("", + BadPrimitiveExtra( + typeof<'TEnum>.FullName, + value, + "Unkown value provided for the enum" + )) + |> Error + } ) let inline uint32<'TEnum when 'TEnum: enum> : Decoder<'TEnum> = uint32 |> andThen (fun value -> - LanguagePrimitives.EnumOfValue value |> succeed + if System.Enum.IsDefined(typeof<'TEnum>, value) then + LanguagePrimitives.EnumOfValue<_, 'TEnum> value |> succeed + else + fail "Unkown value provided for the enum" ) #endif diff --git a/packages/Thoth.Json.Core/Encode.fs b/packages/Thoth.Json.Core/Encode.fs index 77e89a1..ef99966 100644 --- a/packages/Thoth.Json.Core/Encode.fs +++ b/packages/Thoth.Json.Core/Encode.fs @@ -272,6 +272,9 @@ module Encode = |> List.map (tuple2 keyEncoder valueEncoder) |> list + let lazily<'t> (enc: Lazy>) : Encoder<'t> = + fun (x: 't) -> enc.Value x + //////////// /// Enum /// /////////// diff --git a/packages/Thoth.Json.Core/Thoth.Json.Core.fsproj b/packages/Thoth.Json.Core/Thoth.Json.Core.fsproj index 41e69ae..49767c5 100644 --- a/packages/Thoth.Json.Core/Thoth.Json.Core.fsproj +++ b/packages/Thoth.Json.Core/Thoth.Json.Core.fsproj @@ -20,7 +20,6 @@ Pick the right additional package for your runtime: - diff --git a/tests/Thoth.Json.Tests.JavaScript/packages.lock.json b/tests/Thoth.Json.Tests.JavaScript/packages.lock.json index cec7916..1d65dab 100644 --- a/tests/Thoth.Json.Tests.JavaScript/packages.lock.json +++ b/tests/Thoth.Json.Tests.JavaScript/packages.lock.json @@ -97,6 +97,13 @@ "Microsoft.SourceLink.Common": "1.1.1" } }, + "thoth.json.auto": { + "type": "Project", + "dependencies": { + "FSharp.Core": "[5.0.0, )", + "Thoth.Json.Core": "[1.0.0, )" + } + }, "thoth.json.core": { "type": "Project", "dependencies": { @@ -120,6 +127,7 @@ "FSharp.Core": "[5.0.0, )", "Fable.Core": "[4.1.0, )", "Fable.Pyxpecto": "[1.1.0, )", + "Thoth.Json.Auto": "[1.0.0, )", "Thoth.Json.Core": "[1.0.0, )" } }, diff --git a/tests/Thoth.Json.Tests.Legacy/Decoders.fs b/tests/Thoth.Json.Tests.Legacy/Decoders.fs index 2835988..7e45005 100644 --- a/tests/Thoth.Json.Tests.Legacy/Decoders.fs +++ b/tests/Thoth.Json.Tests.Legacy/Decoders.fs @@ -3967,9 +3967,8 @@ Expecting a boolean but instead got: "not_a_boolean" let json = Encode.Auto.toString (4, value) let res = - Decode.Auto - .unsafeFromString(json) - // .ToLocalTime() + Decode.Auto.unsafeFromString (json) + // .ToLocalTime() equal value.Date res.Date equal value.Hour res.Hour diff --git a/tests/Thoth.Json.Tests.Newtonsoft/packages.lock.json b/tests/Thoth.Json.Tests.Newtonsoft/packages.lock.json index 70c229a..57ff763 100644 --- a/tests/Thoth.Json.Tests.Newtonsoft/packages.lock.json +++ b/tests/Thoth.Json.Tests.Newtonsoft/packages.lock.json @@ -77,6 +77,13 @@ "Microsoft.SourceLink.Common": "1.1.1" } }, + "thoth.json.auto": { + "type": "Project", + "dependencies": { + "FSharp.Core": "[5.0.0, )", + "Thoth.Json.Core": "[1.0.0, )" + } + }, "thoth.json.core": { "type": "Project", "dependencies": { @@ -101,6 +108,7 @@ "FSharp.Core": "[5.0.0, )", "Fable.Core": "[4.1.0, )", "Fable.Pyxpecto": "[1.1.0, )", + "Thoth.Json.Auto": "[1.0.0, )", "Thoth.Json.Core": "[1.0.0, )" } }, diff --git a/tests/Thoth.Json.Tests.Python/packages.lock.json b/tests/Thoth.Json.Tests.Python/packages.lock.json index 76de110..d78bf5a 100644 --- a/tests/Thoth.Json.Tests.Python/packages.lock.json +++ b/tests/Thoth.Json.Tests.Python/packages.lock.json @@ -83,6 +83,13 @@ "Microsoft.SourceLink.Common": "1.1.1" } }, + "thoth.json.auto": { + "type": "Project", + "dependencies": { + "FSharp.Core": "[5.0.0, )", + "Thoth.Json.Core": "[1.0.0, )" + } + }, "thoth.json.core": { "type": "Project", "dependencies": { @@ -107,6 +114,7 @@ "FSharp.Core": "[5.0.0, )", "Fable.Core": "[4.1.0, )", "Fable.Pyxpecto": "[1.1.0, )", + "Thoth.Json.Auto": "[1.0.0, )", "Thoth.Json.Core": "[1.0.0, )" } }, diff --git a/tests/Thoth.Json.Tests/Decoders.fs b/tests/Thoth.Json.Tests/Decoders.fs index cf13ce8..14c3596 100644 --- a/tests/Thoth.Json.Tests/Decoders.fs +++ b/tests/Thoth.Json.Tests/Decoders.fs @@ -9,6 +9,7 @@ open System open Fable.Pyxpecto open Thoth.Json.Core +open Thoth.Json.Auto let jsonRecord = """{ "a": 1.0, @@ -78,18 +79,33 @@ let tests (runner: TestRunner<_>) = let actual = runner.Decode.fromString Decode.float "maxime" - equal expected actual + equal actual expected + + testCase "invalid json #2 - Special case for Thoth.Json.Net" + <| fun _ -> + // See: https://github.com/thoth-org/Thoth.Json.Net/issues/42 +#if FABLE_COMPILER_PYTHON + let expected: Result = + Error + "Given an invalid JSON: Extra data: line 1 column 6 (char 5)" +#else +#if FABLE_COMPILER + let expected: Result = + Error + "Given an invalid JSON: Unexpected non-whitespace character after JSON at position 5" +#else + let expected: Result = + Error + "Given an invalid JSON: Additional text encountered after finished reading JSON content: ,. Path '', line 1, position 5." +#endif +#endif + let decoder = Decode.Auto.generateDecoder () - // testCase "invalid json #2 - Special case for Thoth.Json.Net" <| fun _ -> - // // See: https://github.com/thoth-org/Thoth.Json.Net/issues/42 - // #if FABLE_COMPILER - // let expected : Result = Error "Given an invalid JSON: Unexpected token , in JSON at position 5" - // #else - // let expected : Result = Error "Given an invalid JSON: Additional text encountered after finished reading JSON content: ,. Path '', line 1, position 5." - // #endif - // let actual = Decode.Auto.fromString(""""Foo","42"]""") + let actual = + """"Foo","42"]""" + |> runner.Decode.fromString decoder - // equal expected actual + equal actual expected testCase "invalid json #3 - Special case for Thoth.Json.Net" <| fun _ -> @@ -125,7 +141,7 @@ let tests (runner: TestRunner<_>) = let actual = runner.Decode.fromString Decode.float incorrectJson - equal expected actual + equal actual expected testCase "user exceptions are not captured by the decoders" <| fun _ -> @@ -227,7 +243,7 @@ let tests (runner: TestRunner<_>) = |> ignore // Ignore the result as we only want to trigger the decoder and capture the exception with ex -> - equal expected ex.Message + equal ex.Message expected ] testList @@ -774,6 +790,7 @@ Expecting a bigint but instead got: "maxime" equal expected actual +#if !FABLE_COMPILER_PYTHON testCase "a string representing a DateTime should be accepted as a string" <| fun _ -> @@ -785,6 +802,7 @@ Expecting a bigint but instead got: "maxime" "\"2018-10-01T11:12:55.00Z\"" equal (Ok expected) actual +#endif #if !FABLE_COMPILER_PYTHON testCase "a datetime works" @@ -2446,7 +2464,7 @@ Expecting an object with a field named `height` but instead got: let actual = runner.Decode.fromString (Decode.succeed 7) "maxime" - equal expected actual + equal actual expected testCase "fail works" <| fun _ -> @@ -3888,631 +3906,1467 @@ Expecting a boolean but instead got: "not_a_boolean" equal expected actual ] - // testList "Auto" [ - // testCase "Auto.runner.Decode.fromString works" <| fun _ -> - // let now = DateTime.Now - // let value : Record9 = - // { - // a = 5 - // b = "bar" - // c = [false, 3; true, 5; false, 10] - // d = [|Some(Foo 14); None|] - // e = Map [("oh", { a = 2.; b = 2. }); ("ah", { a = -1.5; b = 0. })] - // f = now - // g = set [{ a = 2.; b = 2. }; { a = -1.5; b = 0. }] - // h = TimeSpan.FromSeconds(5.) - // i = 120y - // j = 120uy - // k = 250s - // l = 250us - // m = 99u - // n = 99L - // o = 999UL - // p = () - // r = Map [( {a = 1.; b = 2.}, "value 1"); ( {a = -2.5; b = 22.1}, "value 2")] - // s = 'y' - // // s = seq [ "item n°1"; "item n°2"] - // } - // let extra = - // Extra.empty - // |> Extra.withInt64 - // |> Extra.withUInt64 - // let json = Encode.Auto.toString(4, value, extra = extra) - // // printfn "AUTO ENCODED %s" json - // let r2 = Decode.Auto.unsafeFromString(json, extra = extra) - // equal 5 r2.a - // equal "bar" r2.b - // equal [false, 3; true, 5; false, 10] r2.c - // equal (Some(Foo 14)) r2.d.[0] - // equal None r2.d.[1] - // equal -1.5 (Map.find "ah" r2.e).a - // equal 2. (Map.find "oh" r2.e).b - // equal (now.ToString()) (value.f.ToString()) - // equal true (Set.contains { a = -1.5; b = 0. } r2.g) - // equal false (Set.contains { a = 1.5; b = 0. } r2.g) - // equal 5000. value.h.TotalMilliseconds - // equal 120y r2.i - // equal 120uy r2.j - // equal 250s r2.k - // equal 250us r2.l - // equal 99u r2.m - // equal 99L r2.n - // equal 999UL r2.o - // equal () r2.p - // equal (Map [( {a = 1.; b = 2.}, "value 1"); ( {a = -2.5; b = 22.1}, "value 2")]) r2.r - // equal 'y' r2.s - // // equal ((seq [ "item n°1"; "item n°2"]) |> Seq.toList) (r2.s |> Seq.toList) - - // testCase "Auto serialization works with recursive types" <| fun _ -> - // let len xs = - // let rec lenInner acc = function - // | Cons(_,rest) -> lenInner (acc + 1) rest - // | Nil -> acc - // lenInner 0 xs - // let li = Cons(1, Cons(2, Cons(3, Nil))) - // let json = Encode.Auto.toString(4, li) - // // printfn "AUTO ENCODED MYLIST %s" json - // let li2 = Decode.Auto.unsafeFromString>(json) - // len li2 |> equal 3 - // match li with - // | Cons(i1, Cons(i2, Cons(i3, Nil))) -> i1 + i2 + i3 - // | Cons(i,_) -> i - // | Nil -> 0 - // |> equal 6 - - // testCase "Auto decoders works for string" <| fun _ -> - // let value = "maxime" - // let json = Encode.Auto.toString(4, value) - // let res = Decode.Auto.unsafeFromString(json) - // equal value res - - // testCase "Auto decoders works for guid" <| fun _ -> - // let value = Guid.NewGuid() - // let json = Encode.Auto.toString(4, value) - // let res = Decode.Auto.unsafeFromString(json) - // equal value res - - // testCase "Auto decoders works for int" <| fun _ -> - // let value = 12 - // let json = Encode.Auto.toString(4, value) - // let res = Decode.Auto.unsafeFromString(json) - // equal value res - - // testCase "Auto decoders works for int64" <| fun _ -> - // let extra = Extra.empty |> Extra.withInt64 - // let value = 9999999999L - // let json = Encode.Auto.toString(4, value, extra=extra) - // let res = Decode.Auto.unsafeFromString(json, extra=extra) - // equal value res - - // testCase "Auto decoders works for uint32" <| fun _ -> - // let value = 12u - // let json = Encode.Auto.toString(4, value) - // let res = Decode.Auto.unsafeFromString(json) - // equal value res - - // testCase "Auto decoders works for uint64" <| fun _ -> - // let extra = Extra.empty |> Extra.withUInt64 - // let value = 9999999999999999999UL - // let json = Encode.Auto.toString(4, value, extra=extra) - // let res = Decode.Auto.unsafeFromString(json, extra=extra) - // equal value res - - // testCase "Auto decoders works for bigint" <| fun _ -> - // let extra = Extra.empty |> Extra.withBigInt - // let value = 99999999999999999999999I - // let json = Encode.Auto.toString(4, value, extra=extra) - // let res = Decode.Auto.unsafeFromString(json, extra=extra) - // equal value res - - // testCase "Auto decoders works for bool" <| fun _ -> - // let value = false - // let json = Encode.Auto.toString(4, value) - // let res = Decode.Auto.unsafeFromString(json) - // equal value res - - // testCase "Auto decoders works for float" <| fun _ -> - // let value = 12. - // let json = Encode.Auto.toString(4, value) - // let res = Decode.Auto.unsafeFromString(json) - // equal value res - - // testCase "Auto decoders works for decimal" <| fun _ -> - // let extra = Extra.empty |> Extra.withDecimal - // let value = 0.7833M - // let json = Encode.Auto.toString(4, value, extra=extra) - // let res = Decode.Auto.unsafeFromString(json, extra=extra) - // equal value res - - // testCase "Auto extra decoders can override default decoders" <| fun _ -> - // let extra = Extra.empty |> Extra.withCustom IntAsRecord.encode IntAsRecord.decode - // let json = """ - // { - // "type": "int", - // "value": 12 - // } - // """ - // let res = Decode.Auto.unsafeFromString(json, extra=extra) - // equal 12 res - - // // testCase "Auto decoders works for datetime" <| fun _ -> - // // let value = DateTime.Now - // // let json = Encode.Auto.toString(4, value) - // // let res = Decode.Auto.unsafeFromString(json) - // // equal value.Date res.Date - // // equal value.Hour res.Hour - // // equal value.Minute res.Minute - // // equal value.Second res.Second - - // testCase "Auto decoders works for datetime UTC" <| fun _ -> - // let value = DateTime.UtcNow - // let json = Encode.Auto.toString(4, value) - // let res = Decode.Auto.unsafeFromString(json) - // equal value.Date res.Date - // equal value.Hour res.Hour - // equal value.Minute res.Minute - // equal value.Second res.Second - - // testCase "Auto decoders works for datetimeOffset" <| fun _ -> - // let value = DateTimeOffset.Now - // let json = Encode.Auto.toString(4, value) - // let res = Decode.Auto.unsafeFromString(json).ToLocalTime() - // equal value.Date res.Date - // equal value.Hour res.Hour - // equal value.Minute res.Minute - // equal value.Second res.Second - - // testCase "Auto decoders works for datetimeOffset UTC" <| fun _ -> - // let value = DateTimeOffset.UtcNow - // let json = Encode.Auto.toString(4, value) - // let res = Decode.Auto.unsafeFromString(json).ToUniversalTime() - // // printfn "SOURCE %A JSON %s OUTPUT %A" value json res - // equal value.Date res.Date - // equal value.Hour res.Hour - // equal value.Minute res.Minute - // equal value.Second res.Second - - // testCase "Auto decoders works for TimeSpan" <| fun _ -> - // let value = TimeSpan(1,2,3,4,5) - // let json = Encode.Auto.toString(4, value) - // let res = Decode.Auto.unsafeFromString(json) - // equal value.Days res.Days - // equal value.Hours res.Hours - // equal value.Minutes res.Minutes - // equal value.Seconds res.Seconds - // equal value.Milliseconds res.Milliseconds - - // testCase "Auto decoders works for list" <| fun _ -> - // let value = [1; 2; 3; 4] - // let json = Encode.Auto.toString(4, value) - // let res = Decode.Auto.unsafeFromString(json) - // equal value res - - // testCase "Auto decoders works for array" <| fun _ -> - // let value = [| 1; 2; 3; 4 |] - // let json = Encode.Auto.toString(4, value) - // let res = Decode.Auto.unsafeFromString(json) - // equal value res - - // testCase "Auto decoders works for Map with string keys" <| fun _ -> - // let value = Map.ofSeq [ "a", 1; "b", 2; "c", 3 ] - // let json = Encode.Auto.toString(4, value) - // let res = Decode.Auto.unsafeFromString>(json) - // equal value res - - // testCase "Auto decoders works for Map with complex keys" <| fun _ -> - // let value = Map.ofSeq [ (1, 6), "a"; (2, 7), "b"; (3, 8), "c" ] - // let json = Encode.Auto.toString(4, value) - // let res = Decode.Auto.unsafeFromString>(json) - // equal value res - - // testCase "Auto decoders works for option None" <| fun _ -> - // let value = None - // let json = Encode.Auto.toString(4, value) - // let res = Decode.Auto.unsafeFromString(json) - // equal value res - - // testCase "Auto decoders works for option Some" <| fun _ -> - // let value = Some 5 - // let json = Encode.Auto.toString(4, value) - // let res = Decode.Auto.unsafeFromString(json) - // equal value res - - // testCase "Auto decoders works for Unit" <| fun _ -> - // let value = () - // let json = Encode.Auto.toString(4, value) - // let res = Decode.Auto.unsafeFromString(json) - // equal value res - - // testCase "Auto decoders works for enum" <| fun _ -> - // let res = Decode.Auto.unsafeFromString("99") - // equal Enum_Int8.NinetyNine res - - // testCase "Auto decoders for enum returns an error if the Enum value is invalid" <| fun _ -> - // #if FABLE_COMPILER - // let value = - // Error( - // """ - // Error at: `$` - // Expecting Tests.Types.Enum_Int8[System.SByte] but instead got: 2 - // Reason: Unkown value provided for the enum - // """.Trim()) - // #else - // let value = - // Error( - // """ - // Error at: `$` - // Expecting Tests.Types+Enum_Int8 but instead got: 2 - // Reason: Unkown value provided for the enum - // """.Trim()) - // #endif - - // let res = Decode.Auto.fromString("2") - // equal value res - - // testCase "Auto decoders works for enum" <| fun _ -> - // let res = Decode.Auto.unsafeFromString("99") - // equal Enum_UInt8.NinetyNine res - - // testCase "Auto decoders for enum returns an error if the Enum value is invalid" <| fun _ -> - // #if FABLE_COMPILER - // let value = - // Error( - // """ - // Error at: `$` - // Expecting Tests.Types.Enum_UInt8[System.Byte] but instead got: 2 - // Reason: Unkown value provided for the enum - // """.Trim()) - // #else - // let value = - // Error( - // """ - // Error at: `$` - // Expecting Tests.Types+Enum_UInt8 but instead got: 2 - // Reason: Unkown value provided for the enum - // """.Trim()) - // #endif - - // let res = Decode.Auto.fromString("2") - // equal value res - - // testCase "Auto decoders works for enum" <| fun _ -> - // let res = Decode.Auto.unsafeFromString("99") - // equal Enum_Int16.NinetyNine res - - // testCase "Auto decoders for enum returns an error if the Enum value is invalid" <| fun _ -> - // #if FABLE_COMPILER - // let value = - // Error( - // """ - // Error at: `$` - // Expecting Tests.Types.Enum_Int16[System.Int16] but instead got: 2 - // Reason: Unkown value provided for the enum - // """.Trim()) - // #else - // let value = - // Error( - // """ - // Error at: `$` - // Expecting Tests.Types+Enum_Int16 but instead got: 2 - // Reason: Unkown value provided for the enum - // """.Trim()) - // #endif - - // let res = Decode.Auto.fromString("2") - // equal value res - - // testCase "Auto decoders works for enum" <| fun _ -> - // let res = Decode.Auto.unsafeFromString("99") - // equal Enum_UInt16.NinetyNine res - - // testCase "Auto decoders for enum<ºint16> returns an error if the Enum value is invalid" <| fun _ -> - // #if FABLE_COMPILER - // let value = - // Error( - // """ - // Error at: `$` - // Expecting Tests.Types.Enum_UInt16[System.UInt16] but instead got: 2 - // Reason: Unkown value provided for the enum - // """.Trim()) - // #else - // let value = - // Error( - // """ - // Error at: `$` - // Expecting Tests.Types+Enum_UInt16 but instead got: 2 - // Reason: Unkown value provided for the enum - // """.Trim()) - // #endif - - // let res = Decode.Auto.fromString("2") - // equal value res - - // testCase "Auto decoders works for enum" <| fun _ -> - // let res = Decode.Auto.unsafeFromString("1") - // equal Enum_Int.One res - - // testCase "Auto decoders for enum returns an error if the Enum value is invalid" <| fun _ -> - // #if FABLE_COMPILER - // let value = - // Error( - // """ - // Error at: `$` - // Expecting Tests.Types.Enum_Int[System.Int32] but instead got: 4 - // Reason: Unkown value provided for the enum - // """.Trim()) - // #else - // let value = - // Error( - // """ - // Error at: `$` - // Expecting Tests.Types+Enum_Int but instead got: 4 - // Reason: Unkown value provided for the enum - // """.Trim()) - // #endif - - // let res = Decode.Auto.fromString("4") - // equal value res - - // testCase "Auto decoders works for enum" <| fun _ -> - // let res = Decode.Auto.unsafeFromString("99") - // equal Enum_UInt32.NinetyNine res - - // testCase "Auto decoders for enum returns an error if the Enum value is invalid" <| fun _ -> - // #if FABLE_COMPILER - // let value = - // Error( - // """ - // Error at: `$` - // Expecting Tests.Types.Enum_UInt32[System.UInt32] but instead got: 2 - // Reason: Unkown value provided for the enum - // """.Trim()) - // #else - // let value = - // Error( - // """ - // Error at: `$` - // Expecting Tests.Types+Enum_UInt32 but instead got: 2 - // Reason: Unkown value provided for the enum - // """.Trim()) - // #endif - - // let res = Decode.Auto.fromString("2") - // equal value res - - // (* - // #if NETFRAMEWORK - // testCase "Auto decoders works with char based Enums" <| fun _ -> - // let value = CharEnum.A - // let json = Encode.Auto.toString(4, value) - // let res = Decode.Auto.unsafeFromString(json) - // equal value res - // #endif - // *) - // testCase "Auto decoders works for null" <| fun _ -> - // let value = null - // let json = Encode.Auto.toString(4, value) - // let res = Decode.Auto.unsafeFromString(json) - // equal value res - - // testCase "Auto decoders works for anonymous record" <| fun _ -> - // let value = {| A = "string" |} - // let json = Encode.Auto.toString(4, value) - // let res = Decode.Auto.unsafeFromString(json) - // equal value res - - // testCase "Auto decoders works for nested anonymous record" <| fun _ -> - // let value = {| A = {| B = "string" |} |} - // let json = Encode.Auto.toString(4, value) - // let res = Decode.Auto.unsafeFromString(json) - // equal value res - - // testCase "Auto decoders works even if type is determined by the compiler" <| fun _ -> - // let value = [1; 2; 3; 4] - // let json = Encode.Auto.toString(4, value) - // let res = Decode.Auto.unsafeFromString<_>(json) - // equal value res - - // testCase "Auto.unsafeFromString works with camelCase" <| fun _ -> - // let json = """{ "id" : 0, "name": "maxime", "email": "mail@domain.com", "followers": 0 }""" - // let user = Decode.Auto.unsafeFromString(json, caseStrategy=CamelCase) - // equal "maxime" user.Name - // equal 0 user.Id - // equal 0 user.Followers - // equal "mail@domain.com" user.Email - - // testCase "Auto.fromString works with snake_case" <| fun _ -> - // let json = """{ "one" : 1, "two_part": 2, "three_part_field": 3 }""" - // let decoded = Decode.Auto.fromString(json, caseStrategy=SnakeCase) - // let expected = Ok { One = 1; TwoPart = 2; ThreePartField = 3 } - // equal expected decoded - - // testCase "Auto.fromString works with camelCase" <| fun _ -> - // let json = """{ "id" : 0, "name": "maxime", "email": "mail@domain.com", "followers": 0 }""" - // let user = Decode.Auto.fromString(json, caseStrategy=CamelCase) - // let expected = Ok { Id = 0; Name = "maxime"; Email = "mail@domain.com"; Followers = 0 } - // equal expected user - - // testCase "Auto.fromString works for records with an actual value for the optional field value" <| fun _ -> - // let json = """{ "maybe" : "maybe value", "must": "must value"}""" - // let actual = Decode.Auto.fromString(json, caseStrategy=CamelCase) - // let expected = - // Ok ({ Maybe = Some "maybe value" - // Must = "must value" } : TestMaybeRecord) - // equal expected actual - - // testCase "Auto.fromString works for records with `null` for the optional field value" <| fun _ -> - // let json = """{ "maybe" : null, "must": "must value"}""" - // let actual = Decode.Auto.fromString(json, caseStrategy=CamelCase) - // let expected = - // Ok ({ Maybe = None - // Must = "must value" } : TestMaybeRecord) - // equal expected actual - - // testCase "Auto.fromString works for records with `null` for the optional field value on classes" <| fun _ -> - // let json = """{ "maybeClass" : null, "must": "must value"}""" - // let actual = Decode.Auto.fromString(json, caseStrategy=CamelCase) - // let expected = - // Ok ({ MaybeClass = None - // Must = "must value" } : RecordWithOptionalClass) - // equal expected actual - - // testCase "Auto.fromString works for records missing optional field value on classes" <| fun _ -> - // let json = """{ "must": "must value"}""" - // let actual = Decode.Auto.fromString(json, caseStrategy=CamelCase) - // let expected = - // Ok ({ MaybeClass = None - // Must = "must value" } : RecordWithOptionalClass) - // equal expected actual - - // testCase "Auto.generateDecoder throws for field using a non optional class" <| fun _ -> - // let expected = """Cannot generate auto decoder for Tests.Types.BaseClass. Please pass an extra decoder. - - // Documentation available at: https://thoth-org.github.io/Thoth.Json/documentation/auto/extra-coders.html#ready-to-use-extra-coders""" - - // let errorMsg = - // try - // let decoder = Decode.Auto.generateDecoder(caseStrategy=CamelCase) - // "" - // with ex -> - // ex.Message - // errorMsg.Replace("+", ".") |> equal expected - - // testCase "Auto.fromString works for Class marked as optional" <| fun _ -> - // let json = """null""" - - // let actual = Decode.Auto.fromString(json, caseStrategy=CamelCase) - // let expected = Ok None - // equal expected actual - - // testCase "Auto.generateDecoder throws for Class" <| fun _ -> - // let expected = """Cannot generate auto decoder for Tests.Types.BaseClass. Please pass an extra decoder. - - // Documentation available at: https://thoth-org.github.io/Thoth.Json/documentation/auto/extra-coders.html#ready-to-use-extra-coders""" - - // let errorMsg = - // try - // let decoder = Decode.Auto.generateDecoder(caseStrategy=CamelCase) - // "" - // with ex -> - // ex.Message - // errorMsg.Replace("+", ".") |> equal expected - - // testCase "Auto.fromString works for records missing an optional field" <| fun _ -> - // let json = """{ "must": "must value"}""" - // let actual = Decode.Auto.fromString(json, caseStrategy=CamelCase) - // let expected = - // Ok ({ Maybe = None - // Must = "must value" } : TestMaybeRecord) - // equal expected actual - - // testCase "Auto.fromString works with maps encoded as objects" <| fun _ -> - // let expected = Map [("oh", { a = 2.; b = 2. }); ("ah", { a = -1.5; b = 0. })] - // let json = """{"ah":{"a":-1.5,"b":0},"oh":{"a":2,"b":2}}""" - // let actual = Decode.Auto.fromString json - // equal (Ok expected) actual - - // testCase "Auto.fromString works with maps encoded as arrays" <| fun _ -> - // let expected = Map [({ a = 2.; b = 2. }, "oh"); ({ a = -1.5; b = 0. }, "ah")] - // let json = """[[{"a":-1.5,"b":0},"ah"],[{"a":2,"b":2},"oh"]]""" - // let actual = Decode.Auto.fromString json - // equal (Ok expected) actual - - // testCase "Decoder.Auto.toString works with bigint extra" <| fun _ -> - // let extra = Extra.empty |> Extra.withBigInt - // let expected = { bigintField = 9999999999999999999999I } - // let actual = Decode.Auto.fromString("""{"bigintField":"9999999999999999999999"}""", extra=extra) - // equal (Ok expected) actual - - // testCase "Decoder.Auto.toString works with custom extra" <| fun _ -> - // let extra = Extra.empty |> Extra.withCustom ChildType.Encode ChildType.Decoder - // let expected = { ParentField = { ChildField = "bumbabon" } } - // let actual = Decode.Auto.fromString("""{"ParentField":"bumbabon"}""", extra=extra) - // equal (Ok expected) actual - - // testCase "Auto.fromString works with records with private constructors" <| fun _ -> - // let json = """{ "foo1": 5, "foo2": 7.8 }""" - // Decode.Auto.fromString(json, caseStrategy=CamelCase) - // |> equal (Ok ({ Foo1 = 5; Foo2 = 7.8 }: RecordWithPrivateConstructor)) - - // testCase "Auto.fromString works with unions with private constructors" <| fun _ -> - // let json = """[ "Baz", ["Bar", "foo"]]""" - // Decode.Auto.fromString(json, caseStrategy=CamelCase) - // |> equal (Ok [Baz; Bar "foo"]) - - // testCase "Auto.fromString works gives proper error for wrong union fields" <| fun _ -> - // let json = """["Multi", "bar", "foo", "zas"]""" - // Decode.Auto.fromString(json, caseStrategy=CamelCase) - // |> equal (Error "Error at: `$[2]`\nExpecting an int but instead got: \"foo\"") - - // // TODO: Should we allow shorter arrays when last fields are options? - // testCase "Auto.fromString works gives proper error for wrong array length" <| fun _ -> - // let json = """["Multi", "bar", 1]""" - // Decode.Auto.fromString(json, caseStrategy=CamelCase) - // |> equal (Error "Error at: `$`\nThe following `failure` occurred with the decoder: Expected array of length 4 but got 3") - - // testCase "Auto.fromString works gives proper error for wrong array length when no fields" <| fun _ -> - // let json = """["Multi"]""" - // Decode.Auto.fromString(json, caseStrategy=CamelCase) - // |> equal (Error "Error at: `$`\nThe following `failure` occurred with the decoder: Expected array of length 4 but got 1") - - // testCase "Auto.fromString works gives proper error for wrong case name" <| fun _ -> - // let json = """[1]""" - // Decode.Auto.fromString(json, caseStrategy=CamelCase) - // |> equal (Error "Error at: `$[0]`\nExpecting a string but instead got: 1") - - // testCase "Auto.generateDecoderCached works" <| fun _ -> - // let expected = Ok { Id = 0; Name = "maxime"; Email = "mail@domain.com"; Followers = 0 } - // let json = """{ "id" : 0, "name": "maxime", "email": "mail@domain.com", "followers": 0 }""" - // let decoder1 = Decode.Auto.generateDecoderCached(caseStrategy=CamelCase) - // let decoder2 = Decode.Auto.generateDecoderCached(caseStrategy=CamelCase) - // let actual1 = runner.Decode.fromString decoder1 json - // let actual2 = runner.Decode.fromString decoder2 json - // equal expected actual1 - // equal expected actual2 - // equal actual1 actual2 - - // testCase "Auto.fromString works with strange types if they are None" <| fun _ -> - // let json = """{"Id":0}""" - // Decode.Auto.fromString(json) - // |> equal (Ok { Id = 0; Thread = None }) - - // testCase "Auto.fromString works with recursive types" <| fun _ -> - // let vater = - // { Name = "Alfonso" - // Children = [ { Name = "Narumi"; Children = [] } - // { Name = "Takumi"; Children = [] } ] } - // let json = """{"Name":"Alfonso","Children":[{"Name":"Narumi","Children":[]},{"Name":"Takumi","Children":[]}]}""" - // Decode.Auto.fromString(json) - // |> equal (Ok vater) - - // testCase "Auto.unsafeFromString works for unit" <| fun _ -> - // let json = Encode.unit () |> Encode.toString 4 - // let res = Decode.Auto.unsafeFromString(json) - // equal () res - - // testCase "Erased single-case DUs works" <| fun _ -> - // let expected = NoAllocAttributeId (Guid.NewGuid()) - // let json = Encode.Auto.toString(4, expected) - // let actual = Decode.Auto.unsafeFromString(json) - // equal expected actual - - // testCase "Auto.unsafeFromString works with HTML inside of a string" <| fun _ -> - // let expected = - // { - // FeedName = "Ars" - // Content = "
\"How

Enlarge (credit: Getty / Aurich Lawson)

In 2005, Apple contacted Qualcomm as a potential supplier for modem chips in the first iPhone. Qualcomm's response was unusual: a letter demanding that Apple sign a patent licensing agreement before Qualcomm would even consider supplying chips.

\"I'd spent 20 years in the industry, I had never seen a letter like this,\" said Tony Blevins, Apple's vice president of procurement.

Most suppliers are eager to talk to new customers—especially customers as big and prestigious as Apple. But Qualcomm wasn't like other suppliers; it enjoyed a dominant position in the market for cellular chips. That gave Qualcomm a lot of leverage, and the company wasn't afraid to use it.

Read 70 remaining paragraphs | Comments

" - // } - - // let articleJson = - // """ - // { - // "FeedName": "Ars", - // "Content": "
\"How

Enlarge (credit: Getty / Aurich Lawson)

In 2005, Apple contacted Qualcomm as a potential supplier for modem chips in the first iPhone. Qualcomm's response was unusual: a letter demanding that Apple sign a patent licensing agreement before Qualcomm would even consider supplying chips.

\"I'd spent 20 years in the industry, I had never seen a letter like this,\" said Tony Blevins, Apple's vice president of procurement.

Most suppliers are eager to talk to new customers—especially customers as big and prestigious as Apple. But Qualcomm wasn't like other suppliers; it enjoyed a dominant position in the market for cellular chips. That gave Qualcomm a lot of leverage, and the company wasn't afraid to use it.

Read 70 remaining paragraphs | Comments

" - // } - // """ - - // let actual : TestStringWithHTML = Decode.Auto.unsafeFromString(articleJson) - // equal expected actual - // ] + testList + "Auto" + [ +#if !FABLE_COMPILER_PYTHON + testCase "Auto decoder works" + <| fun _ -> + let now = DateTime.Now + + let value: Record9 = + { + a = 5 + b = "bar" + c = + [ + false, 3 + true, 5 + false, 10 + ] + d = + [| + Some(Foo 14) + None + |] + e = + Map + [ + ("oh", + { + a = 2. + b = 2. + }) + ("ah", + { + a = -1.5 + b = 0. + }) + ] + f = now + g = + set + [ + { + a = 2. + b = 2. + } + { + a = -1.5 + b = 0. + } + ] + h = TimeSpan.FromSeconds(5.) + i = 120y + j = 120uy + k = 250s + l = 250us + m = 99u + n = 99L + o = 999UL + p = () + r = + Map + [ + ({ + a = 1. + b = 2. + }, + "value 1") + ({ + a = -2.5 + b = 22.1 + }, + "value 2") + ] + s = 'y' + // s = seq [ "item n°1"; "item n°2"] + } + + let extra = + Extra.empty |> Extra.withInt64 |> Extra.withUInt64 + + let encoder = Encode.Auto.generateEncoder () + let json = value |> encoder |> runner.Encode.toString 4 + + let decoder = + Decode.Auto.generateDecoder (extra = extra) + + let r2 = + json + |> runner.Decode.unsafeFromString decoder + + equal 5 r2.a + equal "bar" r2.b + + equal + [ + false, 3 + true, 5 + false, 10 + ] + r2.c + + equal (Some(Foo 14)) r2.d.[0] + equal None r2.d.[1] + equal -1.5 (Map.find "ah" r2.e).a + equal 2. (Map.find "oh" r2.e).b + equal (now.ToString()) (value.f.ToString()) + + equal + true + (Set.contains + { + a = -1.5 + b = 0. + } + r2.g) + + equal + false + (Set.contains + { + a = 1.5 + b = 0. + } + r2.g) + + equal 5000. value.h.TotalMilliseconds + equal 120y r2.i + equal 120uy r2.j + equal 250s r2.k + equal 250us r2.l + equal 99u r2.m + equal 99L r2.n + equal 999UL r2.o + equal () r2.p + + equal + (Map + [ + ({ + a = 1. + b = 2. + }, + "value 1") + ({ + a = -2.5 + b = 22.1 + }, + "value 2") + ]) + r2.r + + equal 'y' r2.s + // equal ((seq [ "item n°1"; "item n°2"]) |> Seq.toList) (r2.s |> Seq.toList) +#endif + + // testCase "Auto serialization works with recursive types" <| fun _ -> + // let len xs = + // let rec lenInner acc = function + // | Cons(_,rest) -> lenInner (acc + 1) rest + // | Nil -> acc + // lenInner 0 xs + // let li = Cons(1, Cons(2, Cons(3, Nil))) + // let json = li |> Encode.Auto.generateEncoder() |> runner.Encode.toString 4 + // // printfn "AUTO ENCODED MYLIST %s" json + // let decoder = Decode.Auto.generateDecoder<_>() + // let li2 = json |> runner.Decode.unsafeFromString> decoder + // len li2 |> equal 3 + // match li with + // | Cons(i1, Cons(i2, Cons(i3, Nil))) -> i1 + i2 + i3 + // | Cons(i,_) -> i + // | Nil -> 0 + // |> equal 6 + + testCase "Auto decoders works for string" + <| fun _ -> + let value = "maxime" + + let json = + value + |> Encode.Auto.generateEncoder () + |> runner.Encode.toString 4 + + let res = + json + |> runner.Decode.unsafeFromString ( + Decode.Auto.generateDecoder () + ) + + equal value res + + testCase "Auto decoders works for guid" + <| fun _ -> + let value = Guid.NewGuid() + + let json = + value + |> Encode.Auto.generateEncoder () + |> runner.Encode.toString 4 + + let res = + json + |> runner.Decode.unsafeFromString ( + Decode.Auto.generateDecoder () + ) + + equal value res + + testCase "Auto decoders works for int" + <| fun _ -> + let value = 12 + + let json = + value + |> Encode.Auto.generateEncoder () + |> runner.Encode.toString 4 + + let res = + json + |> runner.Decode.unsafeFromString ( + Decode.Auto.generateDecoder () + ) + + equal value res + + testCase "Auto decoders works for int64" + <| fun _ -> + let extra = Extra.empty |> Extra.withInt64 + let value = 9999999999L + + let json = + value + |> Encode.Auto.generateEncoder (extra = extra) + |> runner.Encode.toString 4 + + let res = + json + |> runner.Decode.unsafeFromString ( + Decode.Auto.generateDecoder (extra = extra) + ) + + equal value res + + testCase "Auto decoders works for uint32" + <| fun _ -> + let value = 12u + + let json = + value + |> Encode.Auto.generateEncoder () + |> runner.Encode.toString 4 + + let res = + json + |> runner.Decode.unsafeFromString ( + Decode.Auto.generateDecoder () + ) + + equal value res + + testCase "Auto decoders works for uint64" + <| fun _ -> + let extra = Extra.empty |> Extra.withUInt64 + let value = 9999999999999999999UL + + let json = + value + |> Encode.Auto.generateEncoder (extra = extra) + |> runner.Encode.toString 4 + + let res = + json + |> runner.Decode.unsafeFromString ( + Decode.Auto.generateDecoder (extra = extra) + ) + + equal value res + + testCase "Auto decoders works for bigint" + <| fun _ -> + let extra = Extra.empty |> Extra.withBigInt + let value = 99999999999999999999999I + + let json = + value + |> Encode.Auto.generateEncoder (extra = extra) + |> runner.Encode.toString 4 + + let res = + json + |> runner.Decode.unsafeFromString ( + Decode.Auto.generateDecoder (extra = extra) + ) + + equal value res + + testCase "Auto decoders works for bool" + <| fun _ -> + let value = false + + let json = + value + |> Encode.Auto.generateEncoder () + |> runner.Encode.toString 4 + + let res = + json + |> runner.Decode.unsafeFromString ( + Decode.Auto.generateDecoder () + ) + + equal value res + + testCase "Auto decoders works for float" + <| fun _ -> + let value = 12. + + let json = + value + |> Encode.Auto.generateEncoder () + |> runner.Encode.toString 4 + + let res = + json + |> runner.Decode.unsafeFromString ( + Decode.Auto.generateDecoder () + ) + + equal value res + + testCase "Auto decoders works for decimal" + <| fun _ -> + let extra = Extra.empty |> Extra.withDecimal + let value = 0.7833M + + let json = + value + |> Encode.Auto.generateEncoder (extra = extra) + |> runner.Encode.toString 4 + + let res = + json + |> runner.Decode.unsafeFromString ( + Decode.Auto.generateDecoder (extra = extra) + ) + + equal value res + + testCase "Auto extra decoders can override default decoders" + <| fun _ -> + let extra = + Extra.empty + |> Extra.withCustom + IntAsRecord.encode + IntAsRecord.decode + + let json = + """ + { + "type": "int", + "value": 12 + } + """ + + let res = + json + |> runner.Decode.unsafeFromString ( + Decode.Auto.generateDecoder (extra = extra) + ) + + equal res 12 + + // testCase "Auto decoders works for datetime" <| fun _ -> + // let value = DateTime.Now + // let json = value |> Encode.Auto.generateEncoder() |> runner.Encode.toString 4 + // let res = json |> runner.Decode.unsafeFromString (Decode.Auto.generateDecoder()) + // equal value.Date res.Date + // equal value.Hour res.Hour + // equal value.Minute res.Minute + // equal value.Second res.Second + +#if !FABLE_COMPILER_PYTHON + testCase "Auto decoders works for datetime UTC" + <| fun _ -> + let value = DateTime.UtcNow + + let json = + value + |> Encode.Auto.generateEncoder () + |> runner.Encode.toString 4 + + let res = + json + |> runner.Decode.unsafeFromString ( + Decode.Auto.generateDecoder () + ) + + equal value.Date res.Date + equal value.Hour res.Hour + equal value.Minute res.Minute + equal value.Second res.Second +#endif + +#if !FABLE_COMPILER_PYTHON + testCase "Auto decoders works for datetimeOffset" + <| fun _ -> + let value = DateTimeOffset.Now + + let json = + value + |> Encode.Auto.generateEncoder () + |> runner.Encode.toString 4 + + let res = + json + |> runner.Decode.unsafeFromString ( + Decode.Auto.generateDecoder () + ) + + equal value.Date res.Date + equal value.Hour res.Hour + equal value.Minute res.Minute + equal value.Second res.Second +#endif + +#if !FABLE_COMPILER_PYTHON + testCase "Auto decoders works for datetimeOffset UTC" + <| fun _ -> + let value = DateTimeOffset.UtcNow + + let json = + value + |> Encode.Auto.generateEncoder () + |> runner.Encode.toString 4 + + let res = + json + |> runner.Decode.unsafeFromString ( + Decode.Auto.generateDecoder () + ) + |> fun dto -> dto.ToUniversalTime() + + equal value.Date res.Date + equal value.Hour res.Hour + equal value.Minute res.Minute + equal value.Second res.Second +#endif + + testCase "Auto decoders works for TimeSpan" + <| fun _ -> + let value = TimeSpan(1, 2, 3, 4, 5) + + let json = + value + |> Encode.Auto.generateEncoder () + |> runner.Encode.toString 4 + + let res = + json + |> runner.Decode.unsafeFromString ( + Decode.Auto.generateDecoder () + ) + + equal value.Days res.Days + equal value.Hours res.Hours + equal value.Minutes res.Minutes + equal value.Seconds res.Seconds + equal value.Milliseconds res.Milliseconds + + testCase "Auto decoders works for list" + <| fun _ -> + let value = + [ + 1 + 2 + 3 + 4 + ] + + let json = + value + |> Encode.Auto.generateEncoder () + |> runner.Encode.toString 4 + + let res = + json + |> runner.Decode.unsafeFromString ( + Decode.Auto.generateDecoder () + ) + + equal value res + + testCase "Auto decoders works for array" + <| fun _ -> + let value = + [| + 1 + 2 + 3 + 4 + |] + + let json = + value + |> Encode.Auto.generateEncoder () + |> runner.Encode.toString 4 + + let res = + json + |> runner.Decode.unsafeFromString ( + Decode.Auto.generateDecoder () + ) + + equal value res + + testCase "Auto decoders works for Map with string keys" + <| fun _ -> + let value = + Map.ofSeq + [ + "a", 1 + "b", 2 + "c", 3 + ] + + let json = + value + |> Encode.Auto.generateEncoder () + |> runner.Encode.toString 4 + + let res = + json + |> runner.Decode.unsafeFromString> ( + Decode.Auto.generateDecoder () + ) + + equal value res + + testCase "Auto decoders works for Map with complex keys" + <| fun _ -> + let value = + Map.ofSeq + [ + (1, 6), "a" + (2, 7), "b" + (3, 8), "c" + ] + + let json = + value + |> Encode.Auto.generateEncoder () + |> runner.Encode.toString 4 + + let res = + json + |> runner.Decode.unsafeFromString> ( + Decode.Auto.generateDecoder () + ) + + equal value res + + testCase "Auto decoders works for option None" + <| fun _ -> + let value = None + + let json = + value + |> Encode.Auto.generateEncoder () + |> runner.Encode.toString 4 + + let res = + json + |> runner.Decode.unsafeFromString ( + Decode.Auto.generateDecoder () + ) + + equal value res + + testCase "Auto decoders works for option Some" + <| fun _ -> + let value = Some 5 + + let json = + value + |> Encode.Auto.generateEncoder () + |> runner.Encode.toString 4 + + let res = + json + |> runner.Decode.unsafeFromString ( + Decode.Auto.generateDecoder () + ) + + equal value res + + testCase "Auto decoders works for Unit" + <| fun _ -> + let value = () + + let json = + value + |> Encode.Auto.generateEncoder () + |> runner.Encode.toString 4 + + let res = + json + |> runner.Decode.unsafeFromString ( + Decode.Auto.generateDecoder () + ) + + equal value res + + testCase "Auto decoders works for enum" + <| fun _ -> + let decoder = Decode.Auto.generateDecoder () + + let res = + "99" + |> runner.Decode.unsafeFromString decoder + + equal res Enum_Int8.NinetyNine + + testCase + "Auto decoders for enum returns an error if the Enum value is invalid" + <| fun _ -> +#if FABLE_COMPILER + let value = + Error( + """ + Error at: `$` +Expecting Thoth.Json.Tests.Types.Enum_Int8[System.SByte] but instead got: 2 +Reason: Unkown value provided for the enum + """ + .Trim() + ) +#else + let value = + Error( + """ + Error at: `$` +Expecting Thoth.Json.Tests.Types+Enum_Int8 but instead got: 2 +Reason: Unkown value provided for the enum + """ + .Trim() + ) +#endif + + let decoder = Decode.Auto.generateDecoder () + + let res = + runner.Decode.fromString decoder "2" + + equal res value + + testCase "Auto decoders works for enum" + <| fun _ -> + let res = + runner.Decode.unsafeFromString + (Decode.Auto.generateDecoder ()) + "99" + + equal Enum_UInt8.NinetyNine res + + testCase + "Auto decoders for enum returns an error if the Enum value is invalid" + <| fun _ -> +#if FABLE_COMPILER + let value = + Error( + """ + Error at: `$` +Expecting Thoth.Json.Tests.Types.Enum_UInt8[System.Byte] but instead got: 2 +Reason: Unkown value provided for the enum + """ + .Trim() + ) +#else + let value = + Error( + """ + Error at: `$` +Expecting Thoth.Json.Tests.Types+Enum_UInt8 but instead got: 2 +Reason: Unkown value provided for the enum + """ + .Trim() + ) +#endif + + let res = + runner.Decode.fromString + (Decode.Auto.generateDecoder ()) + "2" + + equal value res + + testCase "Auto decoders works for enum" + <| fun _ -> + let res = + runner.Decode.unsafeFromString + (Decode.Auto.generateDecoder ()) + "99" + + equal Enum_Int16.NinetyNine res + + testCase + "Auto decoders for enum returns an error if the Enum value is invalid" + <| fun _ -> +#if FABLE_COMPILER + let value = + Error( + """ + Error at: `$` +Expecting Thoth.Json.Tests.Types.Enum_Int16[System.Int16] but instead got: 2 +Reason: Unkown value provided for the enum + """ + .Trim() + ) +#else + let value = + Error( + """ + Error at: `$` +Expecting Thoth.Json.Tests.Types+Enum_Int16 but instead got: 2 +Reason: Unkown value provided for the enum + """ + .Trim() + ) +#endif + + let decoder = Decode.Auto.generateDecoder () + + let res = + runner.Decode.fromString decoder "2" + + equal res value + + testCase "Auto decoders works for enum" + <| fun _ -> + let res = + runner.Decode.unsafeFromString + (Decode.Auto.generateDecoder ()) + "99" + + equal Enum_UInt16.NinetyNine res + + testCase + "Auto decoders for enum returns an error if the Enum value is invalid" + <| fun _ -> +#if FABLE_COMPILER + let value = + Error( + """ + Error at: `$` +Expecting Thoth.Json.Tests.Types.Enum_UInt16[System.UInt16] but instead got: 2 +Reason: Unkown value provided for the enum + """ + .Trim() + ) +#else + let value = + Error( + """ + Error at: `$` +Expecting Thoth.Json.Tests.Types+Enum_UInt16 but instead got: 2 +Reason: Unkown value provided for the enum + """ + .Trim() + ) +#endif + + let decoder = + Decode.Auto.generateDecoder () + + let res = + runner.Decode.fromString decoder "2" + + equal res value + + testCase "Auto decoders works for enum" + <| fun _ -> + let decoder = Decode.Auto.generateDecoder () + + let res = + runner.Decode.unsafeFromString decoder "1" + + equal Enum_Int.One res + + testCase + "Auto decoders for enum returns an error if the Enum value is invalid" + <| fun _ -> +#if FABLE_COMPILER + let value = + Error( + """ + Error at: `$` +Expecting Thoth.Json.Tests.Types.Enum_Int[System.Int32] but instead got: 4 +Reason: Unkown value provided for the enum + """ + .Trim() + ) +#else + let value = + Error( + """ + Error at: `$` +Expecting Thoth.Json.Tests.Types+Enum_Int but instead got: 4 +Reason: Unkown value provided for the enum + """ + .Trim() + ) +#endif + + let decoder = Decode.Auto.generateDecoder () + let res = runner.Decode.fromString decoder "4" + equal res value + + testCase "Auto decoders works for enum" + <| fun _ -> + let decoder = + Decode.Auto.generateDecoder () + + let res = + runner.Decode.unsafeFromString + decoder + "99" + + equal res Enum_UInt32.NinetyNine + + testCase + "Auto decoders for enum returns an error if the Enum value is invalid" + <| fun _ -> +#if FABLE_COMPILER + let value = + Error( + """ + Error at: `$` +Expecting Thoth.Json.Tests.Types.Enum_UInt32[System.UInt32] but instead got: 2 +Reason: Unkown value provided for the enum + """ + .Trim() + ) +#else + let value = + Error( + """ + Error at: `$` +The following `failure` occurred with the decoder: Unkown value provided for the enum + """ + .Trim() + ) +#endif + + let decoder = + Decode.Auto.generateDecoder () + + let res = + runner.Decode.fromString decoder "2" + + equal res value + + // (* + // #if NETFRAMEWORK + // testCase "Auto decoders works with char based Enums" <| fun _ -> + // let value = CharEnum.A + // let json = Encode.Auto.toString(4, value) + // let res = Decode.Auto.unsafeFromString(json) + // equal value res + // #endif + // *) + + // testCase "Auto decoders works for null" <| fun _ -> + // let value = null + // let json = value |> Encode.Auto.generateEncoder() |> runner.Encode.toString 4 + // let res = json |> runner.Decode.unsafeFromString (Decode.Auto.generateDecoder()) + // equal value res + + testCase "Auto decoders works for anonymous record" + <| fun _ -> + let value = + {| + A = "string" + |} + + let json = + value + |> Encode.Auto.generateEncoder () + |> runner.Encode.toString 4 + + let decoder = Decode.Auto.generateDecoder () + + let res = + json |> runner.Decode.unsafeFromString<_> decoder + + equal res value + + testCase "Auto decoders works for nested anonymous record" + <| fun _ -> + let value = + {| + A = + {| + B = "string" + |} + |} + + let json = + value + |> Encode.Auto.generateEncoder () + |> runner.Encode.toString 4 + + let decoder = Decode.Auto.generateDecoder () + + let res = + json |> runner.Decode.unsafeFromString<_> decoder + + equal res value + + testCase + "Auto decoders works even if type is determined by the compiler" + <| fun _ -> + let value = + [ + 1 + 2 + 3 + 4 + ] + + let json = + value + |> Encode.Auto.generateEncoder () + |> runner.Encode.toString 4 + + let decoder = Decode.Auto.generateDecoder () + + let res = + json |> runner.Decode.unsafeFromString<_> decoder + + equal res value + + testCase "Auto.unsafeFromString works with camelCase" + <| fun _ -> + let json = + """{ "id" : 0, "name": "maxime", "email": "mail@domain.com", "followers": 0 }""" + + let decoder = + Decode.Auto.generateDecoder<_> ( + caseStrategy = CamelCase + ) + + let user = + json |> runner.Decode.unsafeFromString decoder + + equal "maxime" user.Name + equal 0 user.Id + equal 0 user.Followers + equal "mail@domain.com" user.Email + + testCase "Auto decoder works with snake_case" + <| fun _ -> + let json = + """{ "one" : 1, "two_part": 2, "three_part_field": 3 }""" + + let decoder = + Decode.Auto.generateDecoder<_> ( + caseStrategy = SnakeCase + ) + + let decoded = + runner.Decode.fromString + decoder + json + + let expected = + Ok + { + One = 1 + TwoPart = 2 + ThreePartField = 3 + } + + equal decoded expected + + testCase "Auto decoder works with camelCase" + <| fun _ -> + let json = + """{ "id" : 0, "name": "maxime", "email": "mail@domain.com", "followers": 0 }""" + + let decoder = + Decode.Auto.generateDecoder<_> ( + caseStrategy = CamelCase + ) + + let decoded = + runner.Decode.fromString decoder json + + let expected = + Ok + { + Id = 0 + Name = "maxime" + Email = "mail@domain.com" + Followers = 0 + } + + equal decoded expected + + testCase + "Auto decoder works for records with an actual value for the optional field value" + <| fun _ -> + let json = + """{ "maybe" : "maybe value", "must": "must value"}""" + + let decoder = + Decode.Auto.generateDecoder<_> ( + caseStrategy = CamelCase + ) + + let actual = + runner.Decode.fromString + decoder + json + + let expected = + Ok( + { + Maybe = Some "maybe value" + Must = "must value" + } + : TestMaybeRecord + ) + + equal expected actual + + testCase + "Auto decoder works for records with `null` for the optional field value" + <| fun _ -> + let json = """{ "maybe" : null, "must": "must value"}""" + + let decoder = + Decode.Auto.generateDecoder<_> ( + caseStrategy = CamelCase + ) + + let actual = + runner.Decode.fromString + decoder + json + + let expected = + Ok( + { + Maybe = None + Must = "must value" + } + : TestMaybeRecord + ) + + equal expected actual + + // testCase "Auto decoder works for records with `null` for the optional field value on classes" <| fun _ -> + // let json = """{ "maybeClass" : null, "must": "must value"}""" + // let decoder = Decode.Auto.generateDecoder(caseStrategy=CamelCase) + // let actual = + // json + // |> runner.Decode.fromString decoder + // let expected = + // Ok ({ MaybeClass = None + // Must = "must value" } : RecordWithOptionalClass) + // equal expected actual + + // testCase "Auto.fromString works for records missing optional field value on classes" <| fun _ -> + // let json = """{ "must": "must value"}""" + // let actual = Decode.Auto.fromString(json, caseStrategy=CamelCase) + // let expected = + // Ok ({ MaybeClass = None + // Must = "must value" } : RecordWithOptionalClass) + // equal expected actual + + // testCase "Auto.generateDecoder throws for field using a non optional class" <| fun _ -> + // let expected = """Cannot generate auto decoder for Tests.Types.BaseClass. Please pass an extra decoder. + + // Documentation available at: https://thoth-org.github.io/Thoth.Json/documentation/auto/extra-coders.html#ready-to-use-extra-coders""" + + // let errorMsg = + // try + // let decoder = Decode.Auto.generateDecoder(caseStrategy=CamelCase) + // "" + // with ex -> + // ex.Message + // errorMsg.Replace("+", ".") |> equal expected + + // testCase "Auto.fromString works for Class marked as optional" <| fun _ -> + // let json = """null""" + + // let decoder = Decode.Auto.generateDecoder<_>(caseStrategy=CamelCase) + // let actual = + // json + // |> runner.Decode.fromString decoder + // let expected = Ok None + // equal expected actual + + // testCase "Auto.generateDecoder throws for Class" <| fun _ -> + // let expected = """Cannot generate auto decoder for Tests.Types.BaseClass. Please pass an extra decoder. + + // Documentation available at: https://thoth-org.github.io/Thoth.Json/documentation/auto/extra-coders.html#ready-to-use-extra-coders""" + + // let errorMsg = + // try + // let decoder = Decode.Auto.generateDecoder(caseStrategy=CamelCase) + // "" + // with ex -> + // ex.Message + // errorMsg.Replace("+", ".") |> equal expected + + testCase + "Auto decoder works for records missing an optional field" + <| fun _ -> + let json = """{ "must": "must value"}""" + + let decoder = + Decode.Auto.generateDecoder ( + caseStrategy = CamelCase + ) + + let actual = + json + |> runner.Decode.fromString decoder + + let expected = + Ok( + { + Maybe = None + Must = "must value" + } + : TestMaybeRecord + ) + + equal actual expected + + testCase "Auto decoder works with maps encoded as objects" + <| fun _ -> + let expected = + Map + [ + ("oh", + { + a = 2. + b = 2. + }) + ("ah", + { + a = -1.5 + b = 0. + }) + ] + + let json = + """{"ah":{"a":-1.5,"b":0},"oh":{"a":2,"b":2}}""" + + let decoder = Decode.Auto.generateDecoder () + let actual = json |> runner.Decode.fromString decoder + + equal actual (Ok expected) + + testCase "Auto decoder works with maps encoded as arrays" + <| fun _ -> + let expected = + Map + [ + ({ + a = 2. + b = 2. + }, + "oh") + ({ + a = -1.5 + b = 0. + }, + "ah") + ] + + let json = + """[[{"a":-1.5,"b":0},"ah"],[{"a":2,"b":2},"oh"]]""" + + let decoder = Decode.Auto.generateDecoder () + let actual = json |> runner.Decode.fromString decoder + equal (Ok expected) actual + +#if !FABLE_COMPILER_PYTHON + testCase "Auto decoder works with bigint extra" + <| fun _ -> + let extra = Extra.empty |> Extra.withBigInt + + let expected = + { + bigintField = 9999999999999999999999I + } + + let decoder = + Decode.Auto.generateDecoder (extra = extra) + + let actual = + """{"bigintField":"9999999999999999999999"}""" + |> runner.Decode.fromString decoder + + equal actual (Ok expected) +#endif + + testCase "Auto decoder works with custom extra" + <| fun _ -> + let extra = + Extra.empty + |> Extra.withCustom + ChildType.Encode + ChildType.Decoder + + let expected = + { + ParentField = + { + ChildField = "bumbabon" + } + } + + let decoder = + Decode.Auto.generateDecoder (extra = extra) + + let actual = + """{"ParentField":"bumbabon"}""" + |> runner.Decode.fromString decoder + + equal (Ok expected) actual + + testCase + "Auto decoder works with records with private constructors" + <| fun _ -> + let json = """{ "foo1": 5, "foo2": 7.8 }""" + + let decoder = + Decode.Auto.generateDecoder ( + caseStrategy = CamelCase + ) + + let actual = json |> runner.Decode.fromString decoder + + equal + actual + (Ok( + { + Foo1 = 5 + Foo2 = 7.8 + } + : RecordWithPrivateConstructor + )) + + testCase + "Auto decoder works with unions with private constructors" + <| fun _ -> + let json = """[ "Baz", ["Bar", "foo"]]""" + + let decoder = + Decode.Auto.generateDecoder ( + caseStrategy = CamelCase + ) + + let actual = + runner.Decode.fromString + decoder + json + + equal + actual + (Ok + [ + Baz + Bar "foo" + ]) + + testCase + "Auto decoder works gives proper error for wrong union fields" + <| fun _ -> + let json = """["Multi", "bar", "foo", "zas"]""" + + let decoder = + Decode.Auto.generateDecoder ( + caseStrategy = CamelCase + ) + + let actual = + runner.Decode.fromString + decoder + json + + equal + actual + (Error + "Error at: `$.[2]`\nExpecting an int but instead got: \"foo\"") + + // // TODO: Should we allow shorter arrays when last fields are options? + // testCase "Auto.fromString works gives proper error for wrong array length" <| fun _ -> + // let json = """["Multi", "bar", 1]""" + // Decode.Auto.fromString(json, caseStrategy=CamelCase) + // |> equal (Error "Error at: `$`\nThe following `failure` occurred with the decoder: Expected array of length 4 but got 3") + + testCase + "Auto decoder works gives proper error for wrong array length when no fields" + <| fun _ -> + let json = """["Multi"]""" + + let decoder = + Decode.Auto.generateDecoder ( + caseStrategy = CamelCase + ) + + let actual = + runner.Decode.fromString + decoder + json + + equal + actual + (Error + "Error at: `$.[1]`\nExpecting a longer array. Need index `1` but there are only `1` entries.\n[\n \"Multi\"\n]") + + testCase + "Auto decoder works gives proper error for wrong case name" + <| fun _ -> + let json = """[1]""" + + let decoder = + Decode.Auto.generateDecoder ( + caseStrategy = CamelCase + ) + + let actual = + runner.Decode.fromString + decoder + json + + equal + actual + (Error + "Error at: `$.[0]`\nExpecting a string but instead got: 1") + + testCase "Auto.generateDecoderCached works" + <| fun _ -> + let expected = + Ok + { + Id = 0 + Name = "maxime" + Email = "mail@domain.com" + Followers = 0 + } + + let json = + """{ "id" : 0, "name": "maxime", "email": "mail@domain.com", "followers": 0 }""" + + let decoder1 = + Decode.Auto.generateDecoderCached ( + caseStrategy = CamelCase + ) + + let decoder2 = + Decode.Auto.generateDecoderCached ( + caseStrategy = CamelCase + ) + + let actual1 = runner.Decode.fromString decoder1 json + let actual2 = runner.Decode.fromString decoder2 json + equal expected actual1 + equal expected actual2 + equal actual1 actual2 + + // testCase "Auto.fromString works with strange types if they are None" <| fun _ -> + // let json = """{"Id":0}""" + // Decode.Auto.fromString(json) + // |> equal (Ok { Id = 0; Thread = None }) + + testCase "Auto decoder works with recursive types" + <| fun _ -> + let vater = + { + Name = "Alfonso" + Children = + [ + { + Name = "Narumi" + Children = [] + } + { + Name = "Takumi" + Children = [] + } + ] + } + + let json = + """{"Name":"Alfonso","Children":[{"Name":"Narumi","Children":[]},{"Name":"Takumi","Children":[]}]}""" + + let decoder = Decode.Auto.generateDecoder () + + let actual = + runner.Decode.fromString decoder json + + equal actual (Ok vater) + + testCase "Auto decoder works for unit" + <| fun _ -> + let json = Encode.unit () |> runner.Encode.toString 4 + let decoder = Decode.Auto.generateDecoder () + + let actual = + runner.Decode.unsafeFromString decoder json + + equal actual () + +#if !FABLE_COMPILER_PYTHON + testCase "Auto decoder works for erased single-case DU" + <| fun _ -> + let expected = NoAllocAttributeId(Guid.NewGuid()) + + let json = + expected + |> Encode.Auto.generateEncoder () + |> runner.Encode.toString 4 + + let decoder = Decode.Auto.generateDecoder () + + let actual = + json + |> runner.Decode.unsafeFromString + decoder + + equal expected actual +#endif + + testCase + "Auto.unsafeFromString works with HTML inside of a string" + <| fun _ -> + let expected = + { + FeedName = "Ars" + Content = + "
\"How

Enlarge (credit: Getty / Aurich Lawson)

In 2005, Apple contacted Qualcomm as a potential supplier for modem chips in the first iPhone. Qualcomm's response was unusual: a letter demanding that Apple sign a patent licensing agreement before Qualcomm would even consider supplying chips.

\"I'd spent 20 years in the industry, I had never seen a letter like this,\" said Tony Blevins, Apple's vice president of procurement.

Most suppliers are eager to talk to new customers—especially customers as big and prestigious as Apple. But Qualcomm wasn't like other suppliers; it enjoyed a dominant position in the market for cellular chips. That gave Qualcomm a lot of leverage, and the company wasn't afraid to use it.

Read 70 remaining paragraphs | Comments

" + } + + let articleJson = + """ + { + "FeedName": "Ars", + "Content": "
\"How

Enlarge (credit: Getty / Aurich Lawson)

In 2005, Apple contacted Qualcomm as a potential supplier for modem chips in the first iPhone. Qualcomm's response was unusual: a letter demanding that Apple sign a patent licensing agreement before Qualcomm would even consider supplying chips.

\"I'd spent 20 years in the industry, I had never seen a letter like this,\" said Tony Blevins, Apple's vice president of procurement.

Most suppliers are eager to talk to new customers—especially customers as big and prestigious as Apple. But Qualcomm wasn't like other suppliers; it enjoyed a dominant position in the market for cellular chips. That gave Qualcomm a lot of leverage, and the company wasn't afraid to use it.

Read 70 remaining paragraphs | Comments

" + } + """ + + let decoder = Decode.Auto.generateDecoder () + + let actual: TestStringWithHTML = + articleJson + |> runner.Decode.unsafeFromString decoder + + equal expected actual + ] ] diff --git a/tests/Thoth.Json.Tests/Encoders.fs b/tests/Thoth.Json.Tests/Encoders.fs index a156872..a871950 100644 --- a/tests/Thoth.Json.Tests/Encoders.fs +++ b/tests/Thoth.Json.Tests/Encoders.fs @@ -4,6 +4,7 @@ open Thoth.Json.Tests.Testing open System open Thoth.Json.Tests.Types open Thoth.Json.Core +open Thoth.Json.Auto open Fable.Pyxpecto type RecordWithPrivateConstructor = @@ -698,292 +699,622 @@ let tests (runner: TestRunner<_>) = equal actual expected - // testCase "by default, we keep the case defined in type" <| fun _ -> - // let expected = - // """{"Id":0,"Name":"Maxime","Email":"mail@test.com","followers":33}""" - // let value = - // { Id = 0 - // Name = "Maxime" - // Email = "mail@test.com" - // followers = 33 } - - // let actual = Encode.Auto.toString(0, value) - // equal actual expected - - // testCase "force_snake_case works" <| fun _ -> - // let expected = - // """{"one":1,"two_part":2,"three_part_field":3}""" - // let value = { One = 1; TwoPart = 2; ThreePartField = 3 } - // let actual = Encode.Auto.toString(0, value, SnakeCase) - // equal actual expected - - // testCase "forceCamelCase works" <| fun _ -> - // let expected = - // """{"id":0,"name":"Maxime","email":"mail@test.com","followers":33}""" - // let value = - // { Id = 0 - // Name = "Maxime" - // Email = "mail@test.com" - // followers = 33 } - - // let actual = Encode.Auto.toString(0, value, CamelCase) - // equal actual expected - - // testCase "Encode.Auto.generateEncoder works" <| fun _ -> - // let value = - // { - // a = 5 - // b = "bar" - // c = [false, 3; true, 5; false, 10] - // d = [|Some(Foo 14); None|] - // e = Map [("oh", { a = 2.; b = 2. }); ("ah", { a = -1.5; b = 0. })] - // f = DateTime(2018, 11, 28, 11, 10, 29, DateTimeKind.Utc) - // g = set [{ a = 2.; b = 2. }; { a = -1.5; b = 0. }] - // h = TimeSpan.FromSeconds(5.) - // i = 120y - // j = 120uy - // k = 250s - // l = 250us - // m = 99u - // n = 99L - // o = 999UL - // p = () - // r = Map [( {a = 1.; b = 2.}, "value 1"); ( {a = -2.5; b = 22.1}, "value 2")] - // s = 'z' - // // s = seq [ "item n°1"; "item n°2"] - // } - // let extra = - // Extra.empty - // |> Extra.withInt64 - // |> Extra.withUInt64 - // let encoder = Encode.Auto.generateEncoder(extra = extra) - // let actual = encoder value |> runner.Encode.toString 0 - // let expected = """{"a":5,"b":"bar","c":[[false,3],[true,5],[false,10]],"d":[["Foo",14],null],"e":{"ah":{"a":-1.5,"b":0},"oh":{"a":2,"b":2}},"f":"2018-11-28T11:10:29Z","g":[{"a":-1.5,"b":0},{"a":2,"b":2}],"h":"00:00:05","i":120,"j":120,"k":250,"l":250,"m":99,"n":"99","o":"999","r":[[{"a":-2.5,"b":22.1},"value 2"],[{"a":1,"b":2},"value 1"]],"s":"z"}""" - // // Don't fail because of non-meaningful decimal digits ("2" vs "2.0") - // let actual = System.Text.RegularExpressions.Regex.Replace(actual, @"\.0+(?!\d)", "") - // equal actual expected - - // testCase "Encode.Auto.generateEncoderCached works" <| fun _ -> - // let value = - // { - // a = 5 - // b = "bar" - // c = [false, 3; true, 5; false, 10] - // d = [|Some(Foo 14); None|] - // e = Map [("oh", { a = 2.; b = 2. }); ("ah", { a = -1.5; b = 0. })] - // f = DateTime(2018, 11, 28, 11, 10, 29, DateTimeKind.Utc) - // g = set [{ a = 2.; b = 2. }; { a = -1.5; b = 0. }] - // h = TimeSpan.FromSeconds(5.) - // i = 120y - // j = 120uy - // k = 250s - // l = 250us - // m = 99u - // n = 99L - // o = 999UL - // p = () - // r = Map [( {a = 1.; b = 2.}, "value 1"); ( {a = -2.5; b = 22.1}, "value 2")] - // s = 'z' - // // s = seq [ "item n°1"; "item n°2"] - // } - // let extra = - // Extra.empty - // |> Extra.withInt64 - // |> Extra.withUInt64 - // let encoder1 = Encode.Auto.generateEncoderCached(extra = extra) - // let encoder2 = Encode.Auto.generateEncoderCached(extra = extra) - // let actual1 = encoder1 value |> runner.Encode.toString 0 - // let actual2 = encoder2 value |> runner.Encode.toString 0 - // let expected = """{"a":5,"b":"bar","c":[[false,3],[true,5],[false,10]],"d":[["Foo",14],null],"e":{"ah":{"a":-1.5,"b":0},"oh":{"a":2,"b":2}},"f":"2018-11-28T11:10:29Z","g":[{"a":-1.5,"b":0},{"a":2,"b":2}],"h":"00:00:05","i":120,"j":120,"k":250,"l":250,"m":99,"n":"99","o":"999","r":[[{"a":-2.5,"b":22.1},"value 2"],[{"a":1,"b":2},"value 1"]],"s":"z"}""" - // // Don't fail because of non-meaningful decimal digits ("2" vs "2.0") - // let actual1 = System.Text.RegularExpressions.Regex.Replace(actual1, @"\.0+(?!\d)", "") - // let actual2 = System.Text.RegularExpressions.Regex.Replace(actual2, @"\.0+(?!\d)", "") - // equal actual expected 1 - // equal actual expected 2 - // equal actual1 actual2 - - // testCase "Encode.Auto.toString emit null field if setted for" <| fun _ -> - // let value = { fieldA = null } - // let expected = """{"fieldA":null}""" - // let actual = Encode.Auto.toString(0, value, skipNullField = false) - // equal actual expected - - // testCase "Encode.Auto.toString works with bigint extra" <| fun _ -> - // let extra = - // Extra.empty - // |> Extra.withBigInt - // let expected = """{"bigintField":"9999999999999999999999"}""" - // let value = { bigintField = 9999999999999999999999I } - // let actual = Encode.Auto.toString(0, value, extra=extra) - // equal actual expected - - // testCase "Encode.Auto.toString works with custom extra" <| fun _ -> - // let extra = - // Extra.empty - // |> Extra.withCustom ChildType.Encode ChildType.Decoder - // let expected = """{"ParentField":"bumbabon"}""" - // let value = { ParentField = { ChildField = "bumbabon" } } - // let actual = Encode.Auto.toString(0, value, extra=extra) - // equal actual expected - - // testCase "Encode.Auto.toString serializes maps with Guid keys as JSON objects" <| fun _ -> - // let m = Map [Guid.NewGuid(), 1; Guid.NewGuid(), 2] - // let json = Encode.Auto.toString(0, m) - // json.[0] = '{' |> equal true - - // testCase "Encode.Auto.toString works with records with private constructors" <| fun _ -> - // let expected = """{"foo1":5,"foo2":7.8}""" - // let x = { Foo1 = 5; Foo2 = 7.8 }: RecordWithPrivateConstructor - // Encode.Auto.toString(0, x, caseStrategy=CamelCase) - // |> equal expected - - // testCase "Encode.Auto.toString works with unions with private constructors" <| fun _ -> - // let expected = """["Baz",["Bar","foo"]]""" - // let x = [Baz; Bar "foo"] - // Encode.Auto.toString(0, x, caseStrategy=CamelCase) - // |> equal expected - - // testCase "Encode.Auto.toString works with strange types if they are None" <| fun _ -> - // let expected = - // """{"Id":0}""" - - // let value = - // { Id = 0 - // Thread = None } - - // Encode.Auto.toString(0, value) - // |> equal expected - - // testCase "Encode.Auto.toString works with interfaces if they are None" <| fun _ -> - // let expected = - // """{"Id":0}""" - - // let value = - // { Id = 0 - // Interface = None } - - // Encode.Auto.toString(0, value) - // |> equal expected - - // testCase "Encode.Auto.toString works with recursive types" <| fun _ -> - // let vater = - // { Name = "Alfonso" - // Children = [ { Name = "Narumi"; Children = [] } - // { Name = "Takumi"; Children = [] } ] } - // let json = """{"Name":"Alfonso","Children":[{"Name":"Narumi","Children":[]},{"Name":"Takumi","Children":[]}]}""" - // Encode.Auto.toString(0, vater) - // |> equal json - - // #if !NETFRAMEWORK - // testCase "Encode.Auto.toString works with []" <| fun _ -> - // let expected = "\"firstPerson\"" - // let actual = Encode.Auto.toString(0, Camera.FirstPerson) - // equal actual expected - - // testCase "Encode.Auto.toString works with []" <| fun _ -> - // let expected = "\"react\"" - // let actual = Encode.Auto.toString(0, Framework.React) - // equal actual expected - - // testCase "Encode.Auto.toString works with []" <| fun _ -> - // let expected = "\"Fsharp\"" - // let actual = Encode.Auto.toString(0, Language.Fsharp) - // equal actual expected - - // testCase "Encode.Auto.toString works with [] + []" <| fun _ -> - // let expected = "\"C#\"" - // let actual = Encode.Auto.toString(0, Language.Csharp) - // equal actual expected - // #endif - - // testCase "Encode.Auto.toString works with normal Enums" <| fun _ -> - // let expected = "2" - // let actual = Encode.Auto.toString(0, Enum_Int.Two) - // equal actual expected - - // testCase "Encode.Auto.toString works with System.DayOfWeek" <| fun _ -> - // let expected = "2" - // let actual = Encode.Auto.toString(0, DayOfWeek.Tuesday) - // equal actual expected - - // testCase "Encode.Auto.toString generate `null` if skipNullField is true and the optional field value of type classes is None" <| fun _ -> - // let value = - // { - // MaybeClass = None - // Must = "must value" - // } : RecordWithOptionalClass - - // let actual = Encode.Auto.toString(0, value, caseStrategy = CamelCase, skipNullField = false) - // let expected = - // """{"maybeClass":null,"must":"must value"}""" - // equal actual expected - - // testCase "Encode.Auto.toString doesn't generate the optional field of type classe if it's value is None" <| fun _ -> - // let value = - // { - // MaybeClass = None - // Must = "must value" - // } : RecordWithOptionalClass - - // let actual = Encode.Auto.toString(0, value, caseStrategy = CamelCase) - // let expected = - // """{"must":"must value"}""" - // equal actual expected - - // testCase "Encode.Auto.generateEncoder throws for field using a non optional class" <| fun _ -> - // let expected = """Cannot generate auto encoder for Tests.Types.BaseClass. Please pass an extra encoder. - - // Documentation available at: https://thoth-org.github.io/Thoth.Json/documentation/auto/extra-coders.html#ready-to-use-extra-coders""" - - // let errorMsg = - // try - // let encoder = Encode.Auto.generateEncoder(caseStrategy = CamelCase) - // "" - // with ex -> - // ex.Message - // errorMsg.Replace("+", ".") |> equal expected - - // testCase "Encode.Auto allows to re-define primitive types" <| fun _ -> - // let customIntEncoder (value : int) = - // Encode.object [ - // "type", Encode.string "customInt" - // "value", Encode.int value - // ] - - // let customIntDecoder = - // Decode.field "type" Decode.string - // |> Decode.andThen (function - // | "customInt" -> - // Decode.field "value" Decode.int - - // | invalid -> - // Decode.fail "Invalid type for customInt" - // ) - - // let extra = - // Extra.empty - // |> Extra.withCustom customIntEncoder customIntDecoder - - // let actual = Encode.Auto.toString(0, 42, extra=extra) - - // let expected = - // """{"type":"customInt","value":42}""" - - // equal actual expected - - // testCase "Encode.Auto.toString(value, ...) is equivalent to Encode.Auto.toString(0, value, ...)" <| fun _ -> - // let expected = Encode.Auto.toString(0, {| Name = "Maxime" |}) - // let actual = Encode.Auto.toString({| Name = "Maxime" |}) - // equal actual expected - - (* + testCase "by default, we keep the case defined in type" + <| fun _ -> + let expected = + """{"Id":0,"Name":"Maxime","Email":"mail@test.com","Followers":33}""" + + let value = + { + Id = 0 + Name = "Maxime" + Email = "mail@test.com" + Followers = 33 + } + + let actual = + value + |> Encode.Auto.generateEncoder () + |> runner.Encode.toString 0 + + equal actual expected + + testCase "force_snake_case works" + <| fun _ -> + let expected = + """{"one":1,"two_part":2,"three_part_field":3}""" + + let value = + { + One = 1 + TwoPart = 2 + ThreePartField = 3 + } + + let actual = + value + |> Encode.Auto.generateEncoder (SnakeCase) + |> runner.Encode.toString 0 + + equal actual expected + + testCase "forceCamelCase works" + <| fun _ -> + let expected = + """{"id":0,"name":"Maxime","email":"mail@test.com","followers":33}""" + + let value = + { + Id = 0 + Name = "Maxime" + Email = "mail@test.com" + Followers = 33 + } + + let actual = + value + |> Encode.Auto.generateEncoder (CamelCase) + |> runner.Encode.toString 0 + + equal actual expected + + testCase "Encode.Auto.generateEncoder works" + <| fun _ -> + let value = + { + a = 5 + b = "bar" + c = + [ + false, 3 + true, 5 + false, 10 + ] + d = + [| + Some(Foo 14) + None + |] + e = + Map + [ + ("oh", + { + a = 2. + b = 2. + }) + ("ah", + { + a = -1.5 + b = 0. + }) + ] + f = + DateTime( + 2018, + 11, + 28, + 11, + 10, + 29, + DateTimeKind.Utc + ) + g = + set + [ + { + a = 2. + b = 2. + } + { + a = -1.5 + b = 0. + } + ] + h = TimeSpan.FromSeconds(5.) + i = 120y + j = 120uy + k = 250s + l = 250us + m = 99u + n = 99L + o = 999UL + p = () + r = + Map + [ + ({ + a = 1. + b = 2. + }, + "value 1") + ({ + a = -2.5 + b = 22.1 + }, + "value 2") + ] + s = 'z' + // s = seq [ "item n°1"; "item n°2"] + } + + let extra = + Extra.empty |> Extra.withInt64 |> Extra.withUInt64 + + let encoder = + Encode.Auto.generateEncoder (extra = extra) + + let actual = encoder value |> runner.Encode.toString 0 + + let expected = + """{"a":5,"b":"bar","c":[[false,3],[true,5],[false,10]],"d":[["Foo",14],null],"e":{"ah":{"a":-1.5,"b":0},"oh":{"a":2,"b":2}},"f":"2018-11-28T11:10:29Z","g":[{"a":-1.5,"b":0},{"a":2,"b":2}],"h":"00:00:05","i":120,"j":120,"k":250,"l":250,"m":99,"n":"99","o":"999","r":[[{"a":-2.5,"b":22.1},"value 2"],[{"a":1,"b":2},"value 1"]],"s":"z"}""" + // Don't fail because of non-meaningful decimal digits ("2" vs "2.0") + let actual = + System.Text.RegularExpressions.Regex.Replace( + actual, + @"\.0+(?!\d)", + "" + ) + + equal actual expected + + testCase "Encode.Auto.generateEncoderCached works" + <| fun _ -> + let value = + { + a = 5 + b = "bar" + c = + [ + false, 3 + true, 5 + false, 10 + ] + d = + [| + Some(Foo 14) + None + |] + e = + Map + [ + ("oh", + { + a = 2. + b = 2. + }) + ("ah", + { + a = -1.5 + b = 0. + }) + ] + f = + DateTime( + 2018, + 11, + 28, + 11, + 10, + 29, + DateTimeKind.Utc + ) + g = + set + [ + { + a = 2. + b = 2. + } + { + a = -1.5 + b = 0. + } + ] + h = TimeSpan.FromSeconds(5.) + i = 120y + j = 120uy + k = 250s + l = 250us + m = 99u + n = 99L + o = 999UL + p = () + r = + Map + [ + ({ + a = 1. + b = 2. + }, + "value 1") + ({ + a = -2.5 + b = 22.1 + }, + "value 2") + ] + s = 'z' + // s = seq [ "item n°1"; "item n°2"] + } + + let extra = + Extra.empty |> Extra.withInt64 |> Extra.withUInt64 + + let encoder1 = + Encode.Auto.generateEncoderCached ( + extra = extra + ) + + let encoder2 = + Encode.Auto.generateEncoderCached ( + extra = extra + ) + + let actual1 = encoder1 value |> runner.Encode.toString 0 + let actual2 = encoder2 value |> runner.Encode.toString 0 + + let expected = + """{"a":5,"b":"bar","c":[[false,3],[true,5],[false,10]],"d":[["Foo",14],null],"e":{"ah":{"a":-1.5,"b":0},"oh":{"a":2,"b":2}},"f":"2018-11-28T11:10:29Z","g":[{"a":-1.5,"b":0},{"a":2,"b":2}],"h":"00:00:05","i":120,"j":120,"k":250,"l":250,"m":99,"n":"99","o":"999","r":[[{"a":-2.5,"b":22.1},"value 2"],[{"a":1,"b":2},"value 1"]],"s":"z"}""" + // Don't fail because of non-meaningful decimal digits ("2" vs "2.0") + let actual1 = + System.Text.RegularExpressions.Regex.Replace( + actual1, + @"\.0+(?!\d)", + "" + ) + + let actual2 = + System.Text.RegularExpressions.Regex.Replace( + actual2, + @"\.0+(?!\d)", + "" + ) + + equal actual1 expected + equal actual2 expected + equal actual1 actual2 + +#if !FABLE_COMPILER_PYTHON + testCase "Encode.Auto emit null field if setted for" + <| fun _ -> + let value = + { + fieldA = null + } + + let expected = """{"fieldA":null}""" + + let actual = + value + |> Encode.Auto.generateEncoder ( + skipNullField = false + ) + |> runner.Encode.toString 0 + + equal actual expected +#endif + +#if !FABLE_COMPILER_PYTHON + testCase "Encode.Auto works with bigint extra" + <| fun _ -> + let extra = Extra.empty |> Extra.withBigInt + + let expected = + """{"bigintField":"9999999999999999999999"}""" + + let value = + { + bigintField = 9999999999999999999999I + } + + let actual = + value + |> Encode.Auto.generateEncoder (extra = extra) + |> runner.Encode.toString 0 + + equal actual expected +#endif + + testCase "Encode.Auto works with custom extra" + <| fun _ -> + let extra = + Extra.empty + |> Extra.withCustom + ChildType.Encode + ChildType.Decoder + + let expected = """{"ParentField":"bumbabon"}""" + + let value = + { + ParentField = + { + ChildField = "bumbabon" + } + } + + let actual = + value + |> Encode.Auto.generateEncoder (extra = extra) + |> runner.Encode.toString 0 + + equal actual expected + + testCase + "Encode.Auto serializes maps with Guid keys as JSON objects" + <| fun _ -> + let m = + Map + [ + Guid.NewGuid(), 1 + Guid.NewGuid(), 2 + ] + + let json = + m + |> Encode.Auto.generateEncoder () + |> runner.Encode.toString 0 + + json.[0] = '{' |> equal true + + testCase + "Encode.Auto works with records with private constructors" + <| fun _ -> + let expected = """{"foo1":5,"foo2":7.8}""" + + let x = + { + Foo1 = 5 + Foo2 = 7.8 + } + : RecordWithPrivateConstructor + + let actual = + x + |> Encode.Auto.generateEncoder (CamelCase) + |> runner.Encode.toString 0 + + equal actual expected + + testCase + "Encode.Auto works with unions with private constructors" + <| fun _ -> + let expected = """["Baz",["Bar","foo"]]""" + + let x = + [ + Baz + Bar "foo" + ] + + let actual = + x + |> Encode.Auto.generateEncoder (PascalCase) + |> runner.Encode.toString 0 + + equal actual expected + + // TODO: Should we generate encoders for arbitrary types? + // testCase "Encode.Auto.toString works with strange types if they are None" <| fun _ -> + // let expected = + // """{"Id":0}""" + + // let value = + // { Id = 0 + // Thread = None } + + // Encode.Auto.toString(0, value) + // |> equal expected + + // TODO: Should we generate encoders for arbitrary interfaces? + // testCase "Encode.Auto.generateEncoder works with interfaces if they are None" <| fun _ -> + // let expected = + // """{"Id":0}""" + + // let value = + // { Id = 0 + // Interface = None } + + // let actual = + // value + // |> Encode.Auto.generateEncoder() + // |> runner.Encode.toString 0 + + // equal actual expected + + testCase + "Encode.Auto.generateEncoder works with recursive types" + <| fun _ -> + let value = + { + Name = "Alfonso" + Children = + [ + { + Name = "Narumi" + Children = [] + } + { + Name = "Takumi" + Children = [] + } + ] + } + + let json = + """{"Name":"Alfonso","Children":[{"Name":"Narumi","Children":[]},{"Name":"Takumi","Children":[]}]}""" + + let actual = + value + |> Encode.Auto.generateEncoder () + |> runner.Encode.toString 0 + + equal actual json + + // #if !NETFRAMEWORK + testCase "Encode.Auto.toString works with []" + <| fun _ -> + let expected = "\"firstPerson\"" + + let actual = + Camera.FirstPerson + |> Encode.Auto.generateEncoder () + |> runner.Encode.toString 0 + + equal actual expected + + testCase + "Encode.Auto.toString works with []" + <| fun _ -> + let expected = "\"react\"" + + let actual = + Framework.React + |> Encode.Auto.generateEncoder () + |> runner.Encode.toString 0 + + equal actual expected + + testCase + "Encode.Auto.toString works with []" + <| fun _ -> + let expected = "\"Fsharp\"" + + let actual = + Language.Fsharp + |> Encode.Auto.generateEncoder () + |> runner.Encode.toString 0 + + equal actual expected + + testCase + "Encode.Auto.toString works with [] + []" + <| fun _ -> + let expected = "\"C#\"" + + let actual = + Language.Csharp + |> Encode.Auto.generateEncoder () + |> runner.Encode.toString 0 + + equal actual expected + // #endif + + testCase "Encode.Auto works with normal Enums" + <| fun _ -> + let expected = "2" + + let actual = + Enum_Int.Two + |> Encode.Auto.generateEncoder () + |> runner.Encode.toString 0 + + equal actual expected + + testCase "Encode.Auto works with System.DayOfWeek" + <| fun _ -> + let expected = "2" + + let actual = + DayOfWeek.Tuesday + |> Encode.Auto.generateEncoder () + |> runner.Encode.toString 0 + + equal actual expected + + // TODO: Should we generate encoders for arbitrary classes? + // testCase "Encode.Auto.toString generate `null` if skipNullField is true and the optional field value of type classes is None" <| fun _ -> + // let value = + // { + // MaybeClass = None + // Must = "must value" + // } : RecordWithOptionalClass + + // let actual = Encode.Auto.toString(0, value, caseStrategy = CamelCase, skipNullField = false) + // let expected = + // """{"maybeClass":null,"must":"must value"}""" + // equal actual expected + + // TODO: Should we generate encoders for arbitrary classes? + // testCase "Encode.Auto.toString doesn't generate the optional field of type class if it's value is None" <| fun _ -> + // let value = + // { + // MaybeClass = None + // Must = "must value" + // } : RecordWithOptionalClass + + // let actual = Encode.Auto.toString(0, value, caseStrategy = CamelCase) + // let expected = + // """{"must":"must value"}""" + // equal actual expected + + // TODO: Should we generate encoders for arbitrary classes? + // testCase "Encode.Auto.generateEncoder throws for field using a non optional class" <| fun _ -> + // let expected = """Cannot generate auto encoder for Tests.Types.BaseClass. Please pass an extra encoder. + + // Documentation available at: https://thoth-org.github.io/Thoth.Json/documentation/auto/extra-coders.html#ready-to-use-extra-coders""" + + // let errorMsg = + // try + // let encoder = Encode.Auto.generateEncoder(caseStrategy = CamelCase) + // "" + // with ex -> + // ex.Message + // errorMsg.Replace("+", ".") |> equal expected + + testCase "Encode.Auto allows to re-define primitive types" + <| fun _ -> + let customIntEncoder (value: int) = + Encode.object + [ + "type", Encode.string "customInt" + "value", Encode.int value + ] + + let customIntDecoder = + Decode.field "type" Decode.string + |> Decode.andThen ( + function + | "customInt" -> Decode.field "value" Decode.int + + | invalid -> + Decode.fail "Invalid type for customInt" + ) + + let extra = + Extra.empty + |> Extra.withCustom + customIntEncoder + customIntDecoder + + let actual = + 42 + |> Encode.Auto.generateEncoder (extra = extra) + |> runner.Encode.toString 0 + + let expected = """{"type":"customInt","value":42}""" + + equal actual expected + + // TODO: Remove Encode.Auto.toString since it requires a backend? + // testCase "Encode.Auto.toString(value, ...) is equivalent to Encode.Auto.toString(0, value, ...)" <| fun _ -> + // let expected = Encode.Auto.toString(0, {| Name = "Maxime" |}) + // let actual = Encode.Auto.toString({| Name = "Maxime" |}) + // equal actual expected + #if NETFRAMEWORK - testCase "Encode.Auto.toString works with char based Enums" <| fun _ -> - let expected = ((int) 'A').ToString() // "65" - let actual = Encode.Auto.toString(0, CharEnum.A) - equal actual expected + testCase "Encode.Auto works with char based Enums" + <| fun _ -> + let expected = ((int) 'A').ToString() // "65" + + let actual = + CharEnum.A + |> Encode.Auto.generateEncoder () + |> Encode.Auto.toString 0 + + equal actual expected #endif - *) ] ] diff --git a/tests/Thoth.Json.Tests/Thoth.Json.Tests.fsproj b/tests/Thoth.Json.Tests/Thoth.Json.Tests.fsproj index a10abe2..44abc7c 100644 --- a/tests/Thoth.Json.Tests/Thoth.Json.Tests.fsproj +++ b/tests/Thoth.Json.Tests/Thoth.Json.Tests.fsproj @@ -10,6 +10,7 @@
+ diff --git a/tests/Thoth.Json.Tests/Types.fs b/tests/Thoth.Json.Tests/Types.fs index 93c2f90..23379fb 100644 --- a/tests/Thoth.Json.Tests/Types.fs +++ b/tests/Thoth.Json.Tests/Types.fs @@ -303,8 +303,9 @@ type ChildType = { ChildField: string } - // static member Encode(x: ChildType) = - // Encode.string x.ChildField + + static member Encode(x: ChildType) = Encode.string x.ChildField + static member Decoder = Decode.string |> Decode.map (fun x -> @@ -423,11 +424,11 @@ type RecordForCharacterCase = module IntAsRecord = let encode (value: int) = - // Encode.object [ - // "type", Encode.string "int" - // "value", Encode.int value - // ] - null + Encode.object + [ + "type", Encode.string "int" + "value", Encode.int value + ] let decode: Decoder = Decode.field "type" Decode.string diff --git a/tests/Thoth.Json.Tests/packages.lock.json b/tests/Thoth.Json.Tests/packages.lock.json index 637ceae..0de6dbc 100644 --- a/tests/Thoth.Json.Tests/packages.lock.json +++ b/tests/Thoth.Json.Tests/packages.lock.json @@ -97,6 +97,13 @@ "Microsoft.SourceLink.Common": "1.1.1" } }, + "thoth.json.auto": { + "type": "Project", + "dependencies": { + "FSharp.Core": "[5.0.0, )", + "Thoth.Json.Core": "[1.0.0, )" + } + }, "thoth.json.core": { "type": "Project", "dependencies": {