From 4ea6a531ed89e1ae6d55b8cfa444c7938c1a9c09 Mon Sep 17 00:00:00 2001 From: njlr Date: Sun, 14 Apr 2024 12:51:46 +0100 Subject: [PATCH 01/13] - Introduce Auto module - Re-enable many tests --- Thoth.Json.sln | 7 + packages/Thoth.Json.Auto/Casing.fs | 109 + packages/Thoth.Json.Auto/Decode.fs | 1525 ++++++++++++ packages/Thoth.Json.Auto/Domain.fs | 106 + packages/Thoth.Json.Auto/Encode.fs | 1126 +++++++++ packages/Thoth.Json.Auto/Prelude.fs | 435 ++++ .../Thoth.Json.Auto/Thoth.Json.Auto.fsproj | 20 + packages/Thoth.Json.Auto/packages.lock.json | 98 + packages/Thoth.Json.Core/Decode.fs | 93 +- packages/Thoth.Json.Core/Encode.fs | 6 +- .../Thoth.Json.Core/Thoth.Json.Core.fsproj | 1 - tests/Thoth.Json.Tests.JavaScript/Main.fs | 2 +- tests/Thoth.Json.Tests.Python/Main.fs | 2 +- tests/Thoth.Json.Tests/Decoders.fs | 2136 ++++++++++++----- tests/Thoth.Json.Tests/Encoders.fs | 902 ++++--- .../Thoth.Json.Tests/Thoth.Json.Tests.fsproj | 1 + tests/Thoth.Json.Tests/Types.fs | 18 +- 17 files changed, 5645 insertions(+), 942 deletions(-) create mode 100644 packages/Thoth.Json.Auto/Casing.fs create mode 100644 packages/Thoth.Json.Auto/Decode.fs create mode 100644 packages/Thoth.Json.Auto/Domain.fs create mode 100644 packages/Thoth.Json.Auto/Encode.fs create mode 100644 packages/Thoth.Json.Auto/Prelude.fs create mode 100644 packages/Thoth.Json.Auto/Thoth.Json.Auto.fsproj create mode 100644 packages/Thoth.Json.Auto/packages.lock.json diff --git a/Thoth.Json.sln b/Thoth.Json.sln index 2c04bcf..f750eb5 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\Build.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/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..91711a6 --- /dev/null +++ b/packages/Thoth.Json.Auto/Domain.fs @@ -0,0 +1,106 @@ +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 overrideEncoderImpl (typeKey : TypeKey) (encoder : obj) (opts : ExtraCoders) = + // { + // opts with + // EncoderOverrides = + // opts.EncoderOverrides + // |> Map.add typeKey encoder + // } + + // let inline overrideEncoder (encoder : Encoder<'t>) (opts : ExtraCoders) = + // overrideEncoderImpl (TypeKey.ofType typeof<'t>) encoder opts + + // let overrideDecoderImpl (typeKey : TypeKey) (decoder : obj) (opts : ExtraCoders) = + // { + // opts with + // DecoderOverrides = + // opts.DecoderOverrides + // |> Map.add typeKey decoder + // } + + // let inline overrideDecoder (decoder : Decoder<'t>) (opts : ExtraCoders) = + // overrideDecoderImpl (TypeKey.ofType typeof<'t>) decoder opts + + 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..7768f19 --- /dev/null +++ b/packages/Thoth.Json.Auto/Encode.fs @@ -0,0 +1,1126 @@ +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 -> Json = + 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 |]) + :?> Json + |> 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 -> Json = + 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 -> Json = + 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 |] + ) + :?> Json + |] + + 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 -> Json = 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 |]) + :?> Json + + 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..45b7650 --- /dev/null +++ b/packages/Thoth.Json.Auto/packages.lock.json @@ -0,0 +1,98 @@ +{ + "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.Core": { + "type": "CentralTransitive", + "requested": "[4.1.0, )", + "resolved": "4.1.0", + "contentHash": "NISAbAVGEcvH2s+vHLSOCzh98xMYx4aIadWacQdWPcQLploxpSQXLEe9SeszUBhbHa73KMiKREsH4/W3q4A4iA==" + } + } + } +} \ No newline at end of file diff --git a/packages/Thoth.Json.Core/Decode.fs b/packages/Thoth.Json.Core/Decode.fs index b270165..e7265e7 100644 --- a/packages/Thoth.Json.Core/Decode.fs +++ b/packages/Thoth.Json.Core/Decode.fs @@ -743,7 +743,6 @@ module Decode = ("", BadPrimitive("an array", value)) |> Error } - let keys: Decoder = { new Decoder with member _.Decode(helpers, value) = @@ -846,6 +845,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 /// /////////////////// @@ -1355,36 +1371,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 47d878b..babff7d 100644 --- a/packages/Thoth.Json.Core/Encode.fs +++ b/packages/Thoth.Json.Core/Encode.fs @@ -211,6 +211,8 @@ module Encode = let option (encoder: 'a -> Json) = Option.map encoder >> Option.defaultWith (fun _ -> nil) + let lazily (enc: Lazy>) : Encoder<'t> = fun x -> enc.Value x + let rec toJsonValue (helpers: IEncoderHelpers<'JsonValue>) (json: Json) = match json with | Json.String value -> helpers.encodeString value @@ -218,10 +220,8 @@ module Encode = | Json.Object values -> let o = helpers.createEmptyObject () - values - |> Seq.iter (fun (k, v) -> + for k, v in values do helpers.setPropertyOnObject (o, k, toJsonValue helpers v) - ) o | Json.Char value -> helpers.encodeChar value diff --git a/packages/Thoth.Json.Core/Thoth.Json.Core.fsproj b/packages/Thoth.Json.Core/Thoth.Json.Core.fsproj index ee2bd1a..0373ca6 100644 --- a/packages/Thoth.Json.Core/Thoth.Json.Core.fsproj +++ b/packages/Thoth.Json.Core/Thoth.Json.Core.fsproj @@ -16,7 +16,6 @@ If you are interested on using it against .NET Core or .NET Framework, please us - diff --git a/tests/Thoth.Json.Tests.JavaScript/Main.fs b/tests/Thoth.Json.Tests.JavaScript/Main.fs index 18dc42b..1a49cf2 100644 --- a/tests/Thoth.Json.Tests.JavaScript/Main.fs +++ b/tests/Thoth.Json.Tests.JavaScript/Main.fs @@ -25,7 +25,7 @@ type JavascriptTestRunner() = override _.testCase = testCase override _.ftestCase = ftestCase - override _.equal a b = Assert.AreEqual(b, a) + override _.equal actual expected = Assert.AreEqual(actual, expected) override _.Encode = JavaScriptEncode() diff --git a/tests/Thoth.Json.Tests.Python/Main.fs b/tests/Thoth.Json.Tests.Python/Main.fs index bf13b42..68d6070 100644 --- a/tests/Thoth.Json.Tests.Python/Main.fs +++ b/tests/Thoth.Json.Tests.Python/Main.fs @@ -27,7 +27,7 @@ type PythonTestRunner() = override _.testCase = testCase override _.ftestCase = ftestCase - override _.equal a b = Expect.equal b a "" + override _.equal actual expected = Expect.equal actual expected "" override _.Encode = PythonEncode() diff --git a/tests/Thoth.Json.Tests/Decoders.fs b/tests/Thoth.Json.Tests/Decoders.fs index bbe9217..15ea398 100644 --- a/tests/Thoth.Json.Tests/Decoders.fs +++ b/tests/Thoth.Json.Tests/Decoders.fs @@ -9,6 +9,7 @@ open Thoth.Json.Tests.Testing open System open Thoth.Json.Core +open Thoth.Json.Auto let jsonRecord = """{ "a": 1.0, @@ -80,16 +81,32 @@ let tests (runner: TestRunner<_, _>) = runner.equal expected actual - // runner.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"]""") + runner.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 (line 1 column 6)" +#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 () - // runner.equal expected actual + let actual = + """"Foo","42"]""" + |> runner.Decode.fromString decoder + + runner.equal expected actual runner.testCase "invalid json #3 - Special case for Thoth.Json.Net" @@ -98,7 +115,7 @@ let tests (runner: TestRunner<_, _>) = #if FABLE_COMPILER_JAVASCRIPT let expected: Result = Error - "Given an invalid JSON: Expected double-quoted property name in JSON at position 172" + "Given an invalid JSON: Expected double-quoted property name in JSON at position 172 (line 8 column 17)" #endif #if FABLE_COMPILER_PYTHON @@ -749,6 +766,7 @@ Expecting a bigint but instead got: "maxime" runner.equal expected actual +#if !FABLE_COMPILER_PYTHON runner.testCase "a string representing a DateTime should be accepted as a string" <| fun _ -> @@ -760,6 +778,7 @@ Expecting a bigint but instead got: "maxime" "\"2018-10-01T11:12:55.00Z\"" runner.equal (Ok expected) actual +#endif #if !FABLE_COMPILER_PYTHON runner.testCase "a datetime works" @@ -3819,631 +3838,1474 @@ Expecting a boolean but instead got: "not_a_boolean" runner.equal expected actual ] - // runner.testList "Auto" [ - // runner.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) - // runner.equal 5 r2.a - // runner.equal "bar" r2.b - // runner.equal [false, 3; true, 5; false, 10] r2.c - // runner.equal (Some(Foo 14)) r2.d.[0] - // runner.equal None r2.d.[1] - // runner.equal -1.5 (Map.find "ah" r2.e).a - // runner.equal 2. (Map.find "oh" r2.e).b - // runner.equal (now.ToString()) (value.f.ToString()) - // runner.equal true (Set.contains { a = -1.5; b = 0. } r2.g) - // runner.equal false (Set.contains { a = 1.5; b = 0. } r2.g) - // runner.equal 5000. value.h.TotalMilliseconds - // runner.equal 120y r2.i - // runner.equal 120uy r2.j - // runner.equal 250s r2.k - // runner.equal 250us r2.l - // runner.equal 99u r2.m - // runner.equal 99L r2.n - // runner.equal 999UL r2.o - // runner.equal () r2.p - // runner.equal (Map [( {a = 1.; b = 2.}, "value 1"); ( {a = -2.5; b = 22.1}, "value 2")]) r2.r - // runner.equal 'y' r2.s - // // runner.equal ((seq [ "item n°1"; "item n°2"]) |> Seq.toList) (r2.s |> Seq.toList) - - // runner.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 |> runner.equal 3 - // match li with - // | Cons(i1, Cons(i2, Cons(i3, Nil))) -> i1 + i2 + i3 - // | Cons(i,_) -> i - // | Nil -> 0 - // |> runner.equal 6 - - // runner.testCase "Auto decoders works for string" <| fun _ -> - // let value = "maxime" - // let json = Encode.Auto.toString(4, value) - // let res = Decode.Auto.unsafeFromString(json) - // runner.equal value res - - // runner.testCase "Auto decoders works for guid" <| fun _ -> - // let value = Guid.NewGuid() - // let json = Encode.Auto.toString(4, value) - // let res = Decode.Auto.unsafeFromString(json) - // runner.equal value res - - // runner.testCase "Auto decoders works for int" <| fun _ -> - // let value = 12 - // let json = Encode.Auto.toString(4, value) - // let res = Decode.Auto.unsafeFromString(json) - // runner.equal value res - - // runner.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) - // runner.equal value res - - // runner.testCase "Auto decoders works for uint32" <| fun _ -> - // let value = 12u - // let json = Encode.Auto.toString(4, value) - // let res = Decode.Auto.unsafeFromString(json) - // runner.equal value res - - // runner.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) - // runner.equal value res - - // runner.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) - // runner.equal value res - - // runner.testCase "Auto decoders works for bool" <| fun _ -> - // let value = false - // let json = Encode.Auto.toString(4, value) - // let res = Decode.Auto.unsafeFromString(json) - // runner.equal value res - - // runner.testCase "Auto decoders works for float" <| fun _ -> - // let value = 12. - // let json = Encode.Auto.toString(4, value) - // let res = Decode.Auto.unsafeFromString(json) - // runner.equal value res - - // runner.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) - // runner.equal value res - - // runner.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) - // runner.equal 12 res - - // // runner.testCase "Auto decoders works for datetime" <| fun _ -> - // // let value = DateTime.Now - // // let json = Encode.Auto.toString(4, value) - // // let res = Decode.Auto.unsafeFromString(json) - // // runner.equal value.Date res.Date - // // runner.equal value.Hour res.Hour - // // runner.equal value.Minute res.Minute - // // runner.equal value.Second res.Second - - // runner.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) - // runner.equal value.Date res.Date - // runner.equal value.Hour res.Hour - // runner.equal value.Minute res.Minute - // runner.equal value.Second res.Second - - // runner.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() - // runner.equal value.Date res.Date - // runner.equal value.Hour res.Hour - // runner.equal value.Minute res.Minute - // runner.equal value.Second res.Second - - // runner.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 - // runner.equal value.Date res.Date - // runner.equal value.Hour res.Hour - // runner.equal value.Minute res.Minute - // runner.equal value.Second res.Second - - // runner.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) - // runner.equal value.Days res.Days - // runner.equal value.Hours res.Hours - // runner.equal value.Minutes res.Minutes - // runner.equal value.Seconds res.Seconds - // runner.equal value.Milliseconds res.Milliseconds - - // runner.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) - // runner.equal value res - - // runner.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) - // runner.equal value res - - // runner.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) - // runner.equal value res - - // runner.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) - // runner.equal value res - - // runner.testCase "Auto decoders works for option None" <| fun _ -> - // let value = None - // let json = Encode.Auto.toString(4, value) - // let res = Decode.Auto.unsafeFromString(json) - // runner.equal value res - - // runner.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) - // runner.equal value res - - // runner.testCase "Auto decoders works for Unit" <| fun _ -> - // let value = () - // let json = Encode.Auto.toString(4, value) - // let res = Decode.Auto.unsafeFromString(json) - // runner.equal value res - - // runner.testCase "Auto decoders works for enum" <| fun _ -> - // let res = Decode.Auto.unsafeFromString("99") - // runner.equal Enum_Int8.NinetyNine res - - // runner.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") - // runner.equal value res - - // runner.testCase "Auto decoders works for enum" <| fun _ -> - // let res = Decode.Auto.unsafeFromString("99") - // runner.equal Enum_UInt8.NinetyNine res - - // runner.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") - // runner.equal value res - - // runner.testCase "Auto decoders works for enum" <| fun _ -> - // let res = Decode.Auto.unsafeFromString("99") - // runner.equal Enum_Int16.NinetyNine res - - // runner.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") - // runner.equal value res - - // runner.testCase "Auto decoders works for enum" <| fun _ -> - // let res = Decode.Auto.unsafeFromString("99") - // runner.equal Enum_UInt16.NinetyNine res - - // runner.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") - // runner.equal value res - - // runner.testCase "Auto decoders works for enum" <| fun _ -> - // let res = Decode.Auto.unsafeFromString("1") - // runner.equal Enum_Int.One res - - // runner.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") - // runner.equal value res - - // runner.testCase "Auto decoders works for enum" <| fun _ -> - // let res = Decode.Auto.unsafeFromString("99") - // runner.equal Enum_UInt32.NinetyNine res - - // runner.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") - // runner.equal value res - - // (* - // #if NETFRAMEWORK - // runner.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) - // runner.equal value res - // #endif - // *) - // runner.testCase "Auto decoders works for null" <| fun _ -> - // let value = null - // let json = Encode.Auto.toString(4, value) - // let res = Decode.Auto.unsafeFromString(json) - // runner.equal value res - - // runner.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) - // runner.equal value res - - // runner.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) - // runner.equal value res - - // runner.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) - // runner.equal value res - - // runner.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) - // runner.equal "maxime" user.Name - // runner.equal 0 user.Id - // runner.equal 0 user.Followers - // runner.equal "mail@domain.com" user.Email - - // runner.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 } - // runner.equal expected decoded - - // runner.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 } - // runner.equal expected user - - // runner.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) - // runner.equal expected actual - - // runner.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) - // runner.equal expected actual - - // runner.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) - // runner.equal expected actual - - // runner.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) - // runner.equal expected actual - - // runner.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("+", ".") |> runner.equal expected - - // runner.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 - // runner.equal expected actual - - // runner.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("+", ".") |> runner.equal expected - - // runner.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) - // runner.equal expected actual - - // runner.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 - // runner.equal (Ok expected) actual - - // runner.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 - // runner.equal (Ok expected) actual - - // runner.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) - // runner.equal (Ok expected) actual - - // runner.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) - // runner.equal (Ok expected) actual - - // runner.testCase "Auto.fromString works with records with private constructors" <| fun _ -> - // let json = """{ "foo1": 5, "foo2": 7.8 }""" - // Decode.Auto.fromString(json, caseStrategy=CamelCase) - // |> runner.equal (Ok ({ Foo1 = 5; Foo2 = 7.8 }: RecordWithPrivateConstructor)) - - // runner.testCase "Auto.fromString works with unions with private constructors" <| fun _ -> - // let json = """[ "Baz", ["Bar", "foo"]]""" - // Decode.Auto.fromString(json, caseStrategy=CamelCase) - // |> runner.equal (Ok [Baz; Bar "foo"]) - - // runner.testCase "Auto.fromString works gives proper error for wrong union fields" <| fun _ -> - // let json = """["Multi", "bar", "foo", "zas"]""" - // Decode.Auto.fromString(json, caseStrategy=CamelCase) - // |> runner.equal (Error "Error at: `$[2]`\nExpecting an int but instead got: \"foo\"") - - // // TODO: Should we allow shorter arrays when last fields are options? - // runner.testCase "Auto.fromString works gives proper error for wrong array length" <| fun _ -> - // let json = """["Multi", "bar", 1]""" - // Decode.Auto.fromString(json, caseStrategy=CamelCase) - // |> runner.equal (Error "Error at: `$`\nThe following `failure` occurred with the decoder: Expected array of length 4 but got 3") - - // runner.testCase "Auto.fromString works gives proper error for wrong array length when no fields" <| fun _ -> - // let json = """["Multi"]""" - // Decode.Auto.fromString(json, caseStrategy=CamelCase) - // |> runner.equal (Error "Error at: `$`\nThe following `failure` occurred with the decoder: Expected array of length 4 but got 1") - - // runner.testCase "Auto.fromString works gives proper error for wrong case name" <| fun _ -> - // let json = """[1]""" - // Decode.Auto.fromString(json, caseStrategy=CamelCase) - // |> runner.equal (Error "Error at: `$[0]`\nExpecting a string but instead got: 1") - - // runner.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 - // runner.equal expected actual1 - // runner.equal expected actual2 - // runner.equal actual1 actual2 - - // runner.testCase "Auto.fromString works with strange types if they are None" <| fun _ -> - // let json = """{"Id":0}""" - // Decode.Auto.fromString(json) - // |> runner.equal (Ok { Id = 0; Thread = None }) - - // runner.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) - // |> runner.equal (Ok vater) - - // runner.testCase "Auto.unsafeFromString works for unit" <| fun _ -> - // let json = Encode.unit () |> Encode.toString 4 - // let res = Decode.Auto.unsafeFromString(json) - // runner.equal () res - - // runner.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) - // runner.equal expected actual - - // runner.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) - // runner.equal expected actual - // ] + runner.testList + "Auto" + [ +#if !FABLE_COMPILER_PYTHON + runner.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 + + runner.equal 5 r2.a + runner.equal "bar" r2.b + + runner.equal + [ + false, 3 + true, 5 + false, 10 + ] + r2.c + + runner.equal (Some(Foo 14)) r2.d.[0] + runner.equal None r2.d.[1] + runner.equal -1.5 (Map.find "ah" r2.e).a + runner.equal 2. (Map.find "oh" r2.e).b + runner.equal (now.ToString()) (value.f.ToString()) + + runner.equal + true + (Set.contains + { + a = -1.5 + b = 0. + } + r2.g) + + runner.equal + false + (Set.contains + { + a = 1.5 + b = 0. + } + r2.g) + + runner.equal 5000. value.h.TotalMilliseconds + runner.equal 120y r2.i + runner.equal 120uy r2.j + runner.equal 250s r2.k + runner.equal 250us r2.l + runner.equal 99u r2.m + runner.equal 99L r2.n + runner.equal 999UL r2.o + runner.equal () r2.p + + runner.equal + (Map + [ + ({ + a = 1. + b = 2. + }, + "value 1") + ({ + a = -2.5 + b = 22.1 + }, + "value 2") + ]) + r2.r + + runner.equal 'y' r2.s + // runner.equal ((seq [ "item n°1"; "item n°2"]) |> Seq.toList) (r2.s |> Seq.toList) +#endif + + // runner.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 |> runner.equal 3 + // match li with + // | Cons(i1, Cons(i2, Cons(i3, Nil))) -> i1 + i2 + i3 + // | Cons(i,_) -> i + // | Nil -> 0 + // |> runner.equal 6 + + runner.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 () + ) + + runner.equal value res + + runner.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 () + ) + + runner.equal value res + + runner.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 () + ) + + runner.equal value res + + runner.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) + ) + + runner.equal value res + + runner.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 () + ) + + runner.equal value res + + runner.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) + ) + + runner.equal value res + + runner.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) + ) + + runner.equal value res + + runner.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 () + ) + + runner.equal value res + + runner.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 () + ) + + runner.equal value res + + runner.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) + ) + + runner.equal value res + + runner.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) + ) + + runner.equal res 12 + + // runner.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()) + // runner.equal value.Date res.Date + // runner.equal value.Hour res.Hour + // runner.equal value.Minute res.Minute + // runner.equal value.Second res.Second + +#if !FABLE_COMPILER_PYTHON + runner.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 () + ) + + runner.equal value.Date res.Date + runner.equal value.Hour res.Hour + runner.equal value.Minute res.Minute + runner.equal value.Second res.Second +#endif + +#if !FABLE_COMPILER_PYTHON + runner.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 () + ) + + runner.equal value.Date res.Date + runner.equal value.Hour res.Hour + runner.equal value.Minute res.Minute + runner.equal value.Second res.Second +#endif + +#if !FABLE_COMPILER_PYTHON + runner.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() + + runner.equal value.Date res.Date + runner.equal value.Hour res.Hour + runner.equal value.Minute res.Minute + runner.equal value.Second res.Second +#endif + + runner.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 () + ) + + runner.equal value.Days res.Days + runner.equal value.Hours res.Hours + runner.equal value.Minutes res.Minutes + runner.equal value.Seconds res.Seconds + runner.equal value.Milliseconds res.Milliseconds + + runner.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 () + ) + + runner.equal value res + + runner.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 () + ) + + runner.equal value res + + runner.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 () + ) + + runner.equal value res + + runner.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 () + ) + + runner.equal value res + + runner.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 () + ) + + runner.equal value res + + runner.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 () + ) + + runner.equal value res + + runner.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 () + ) + + runner.equal value res + + runner.testCase "Auto decoders works for enum" + <| fun _ -> + let decoder = Decode.Auto.generateDecoder () + + let res = + "99" + |> runner.Decode.unsafeFromString decoder + + runner.equal res Enum_Int8.NinetyNine + + runner.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" + + runner.equal res value + + runner.testCase "Auto decoders works for enum" + <| fun _ -> + let res = + runner.Decode.unsafeFromString + (Decode.Auto.generateDecoder ()) + "99" + + runner.equal Enum_UInt8.NinetyNine res + + runner.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" + + runner.equal value res + + runner.testCase "Auto decoders works for enum" + <| fun _ -> + let res = + runner.Decode.unsafeFromString + (Decode.Auto.generateDecoder ()) + "99" + + runner.equal Enum_Int16.NinetyNine res + + runner.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" + + runner.equal res value + + runner.testCase "Auto decoders works for enum" + <| fun _ -> + let res = + runner.Decode.unsafeFromString + (Decode.Auto.generateDecoder ()) + "99" + + runner.equal Enum_UInt16.NinetyNine res + + runner.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" + + runner.equal res value + + runner.testCase "Auto decoders works for enum" + <| fun _ -> + let decoder = Decode.Auto.generateDecoder () + + let res = + runner.Decode.unsafeFromString decoder "1" + + runner.equal Enum_Int.One res + + runner.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" + runner.equal res value + + runner.testCase "Auto decoders works for enum" + <| fun _ -> + let decoder = + Decode.Auto.generateDecoder () + + let res = + runner.Decode.unsafeFromString + decoder + "99" + + runner.equal res Enum_UInt32.NinetyNine + + runner.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" + + runner.equal res value + + // (* + // #if NETFRAMEWORK + // runner.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) + // runner.equal value res + // #endif + // *) + + // runner.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()) + // runner.equal value res + + runner.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 + + runner.equal res value + + runner.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 + + runner.equal res value + + runner.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 + + runner.equal res value + + runner.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 + + runner.equal "maxime" user.Name + runner.equal 0 user.Id + runner.equal 0 user.Followers + runner.equal "mail@domain.com" user.Email + + runner.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 + } + + runner.equal decoded expected + + runner.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 + } + + runner.equal decoded expected + + runner.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 + ) + + runner.equal expected actual + + runner.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 + ) + + runner.equal expected actual + + // runner.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) + // runner.equal expected actual + + // runner.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) + // runner.equal expected actual + + // runner.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("+", ".") |> runner.equal expected + + // runner.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 + // runner.equal expected actual + + // runner.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("+", ".") |> runner.equal expected + + runner.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 + ) + + runner.equal actual expected + + runner.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 + + runner.equal actual (Ok expected) + + runner.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 + runner.equal (Ok expected) actual + +#if !FABLE_COMPILER_PYTHON + runner.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 + + runner.equal actual (Ok expected) +#endif + + runner.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 + + runner.equal (Ok expected) actual + + runner.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 + + runner.equal + actual + (Ok( + { + Foo1 = 5 + Foo2 = 7.8 + } + : RecordWithPrivateConstructor + )) + + runner.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 + + runner.equal + actual + (Ok + [ + Baz + Bar "foo" + ]) + + runner.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 + + runner.equal + actual + (Error + "Error at: `$.[2]`\nExpecting an int but instead got: \"foo\"") + + // // TODO: Should we allow shorter arrays when last fields are options? + // runner.testCase "Auto.fromString works gives proper error for wrong array length" <| fun _ -> + // let json = """["Multi", "bar", 1]""" + // Decode.Auto.fromString(json, caseStrategy=CamelCase) + // |> runner.equal (Error "Error at: `$`\nThe following `failure` occurred with the decoder: Expected array of length 4 but got 3") + + runner.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 + + runner.equal + actual + (Error + "Error at: `$.[1]`\nExpecting a longer array. Need index `1` but there are only `1` entries.\n[\n \"Multi\"\n]") + + runner.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 + + runner.equal + actual + (Error + "Error at: `$.[0]`\nExpecting a string but instead got: 1") + + runner.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 + runner.equal expected actual1 + runner.equal expected actual2 + runner.equal actual1 actual2 + + // runner.testCase "Auto.fromString works with strange types if they are None" <| fun _ -> + // let json = """{"Id":0}""" + // Decode.Auto.fromString(json) + // |> runner.equal (Ok { Id = 0; Thread = None }) + + runner.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 + + runner.equal actual (Ok vater) + + runner.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 + + runner.equal actual () + +#if !FABLE_COMPILER_PYTHON + runner.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 + + runner.equal expected actual +#endif + + runner.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 + + runner.equal expected actual + ] ] diff --git a/tests/Thoth.Json.Tests/Encoders.fs b/tests/Thoth.Json.Tests/Encoders.fs index d34da64..08895c2 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 type RecordWithPrivateConstructor = private @@ -632,292 +633,625 @@ let tests (runner: TestRunner<_, _>) = runner.equal actual expected - // runner.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) - // runner.equal actual expected - - // runner.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) - // runner.equal actual expected - - // runner.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) - // runner.equal actual expected - - // runner.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)", "") - // runner.equal actual expected - - // runner.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)", "") - // runner.equal actual expected 1 - // runner.equal actual expected 2 - // runner.equal actual1 actual2 - - // runner.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) - // runner.equal actual expected - - // runner.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) - // runner.equal actual expected - - // runner.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) - // runner.equal actual expected - - // runner.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] = '{' |> runner.equal true - - // runner.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) - // |> runner.equal expected - - // runner.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) - // |> runner.equal expected - - // runner.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) - // |> runner.equal expected - - // runner.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) - // |> runner.equal expected - - // runner.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) - // |> runner.equal json - - // #if !NETFRAMEWORK - // runner.testCase "Encode.Auto.toString works with []" <| fun _ -> - // let expected = "\"firstPerson\"" - // let actual = Encode.Auto.toString(0, Camera.FirstPerson) - // runner.equal actual expected - - // runner.testCase "Encode.Auto.toString works with []" <| fun _ -> - // let expected = "\"react\"" - // let actual = Encode.Auto.toString(0, Framework.React) - // runner.equal actual expected - - // runner.testCase "Encode.Auto.toString works with []" <| fun _ -> - // let expected = "\"Fsharp\"" - // let actual = Encode.Auto.toString(0, Language.Fsharp) - // runner.equal actual expected - - // runner.testCase "Encode.Auto.toString works with [] + []" <| fun _ -> - // let expected = "\"C#\"" - // let actual = Encode.Auto.toString(0, Language.Csharp) - // runner.equal actual expected - // #endif - - // runner.testCase "Encode.Auto.toString works with normal Enums" <| fun _ -> - // let expected = "2" - // let actual = Encode.Auto.toString(0, Enum_Int.Two) - // runner.equal actual expected - - // runner.testCase "Encode.Auto.toString works with System.DayOfWeek" <| fun _ -> - // let expected = "2" - // let actual = Encode.Auto.toString(0, DayOfWeek.Tuesday) - // runner.equal actual expected - - // runner.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"}""" - // runner.equal actual expected - - // runner.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"}""" - // runner.equal actual expected - - // runner.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("+", ".") |> runner.equal expected - - // runner.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}""" - - // runner.equal actual expected - - // runner.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" |}) - // runner.equal actual expected - - (* + runner.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 + + runner.equal actual expected + + runner.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 + + runner.equal actual expected + + runner.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 + + runner.equal actual expected + + runner.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)", + "" + ) + + runner.equal actual expected + + runner.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)", + "" + ) + + runner.equal actual1 expected + runner.equal actual2 expected + runner.equal actual1 actual2 + +#if !FABLE_COMPILER_PYTHON + runner.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 + + runner.equal actual expected +#endif + +#if !FABLE_COMPILER_PYTHON + runner.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 + + runner.equal actual expected +#endif + + runner.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 + + runner.equal actual expected + + runner.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] = '{' |> runner.equal true + + runner.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 + + runner.equal actual expected + + runner.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 + + runner.equal actual expected + + // TODO: Should we generate encoders for arbitrary types? + // runner.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) + // |> runner.equal expected + + // TODO: Should we generate encoders for arbitrary interfaces? + // runner.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 + + // runner.equal actual expected + + runner.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 + + runner.equal actual json + + // #if !NETFRAMEWORK + runner.testCase + "Encode.Auto.toString works with []" + <| fun _ -> + let expected = "\"firstPerson\"" + + let actual = + Camera.FirstPerson + |> Encode.Auto.generateEncoder () + |> runner.Encode.toString 0 + + runner.equal actual expected + + runner.testCase + "Encode.Auto.toString works with []" + <| fun _ -> + let expected = "\"react\"" + + let actual = + Framework.React + |> Encode.Auto.generateEncoder () + |> runner.Encode.toString 0 + + runner.equal actual expected + + runner.testCase + "Encode.Auto.toString works with []" + <| fun _ -> + let expected = "\"Fsharp\"" + + let actual = + Language.Fsharp + |> Encode.Auto.generateEncoder () + |> runner.Encode.toString 0 + + runner.equal actual expected + + runner.testCase + "Encode.Auto.toString works with [] + []" + <| fun _ -> + let expected = "\"C#\"" + + let actual = + Language.Csharp + |> Encode.Auto.generateEncoder () + |> runner.Encode.toString 0 + + runner.equal actual expected + // #endif + + runner.testCase "Encode.Auto works with normal Enums" + <| fun _ -> + let expected = "2" + + let actual = + Enum_Int.Two + |> Encode.Auto.generateEncoder () + |> runner.Encode.toString 0 + + runner.equal actual expected + + runner.testCase "Encode.Auto works with System.DayOfWeek" + <| fun _ -> + let expected = "2" + + let actual = + DayOfWeek.Tuesday + |> Encode.Auto.generateEncoder () + |> runner.Encode.toString 0 + + runner.equal actual expected + + // TODO: Should we generate encoders for arbitrary classes? + // runner.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"}""" + // runner.equal actual expected + + // TODO: Should we generate encoders for arbitrary classes? + // runner.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"}""" + // runner.equal actual expected + + // TODO: Should we generate encoders for arbitrary classes? + // runner.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("+", ".") |> runner.equal expected + + runner.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}""" + + runner.equal actual expected + + // TODO: Remove Encode.Auto.toString since it requires a backend? + // runner.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" |}) + // runner.equal actual expected + #if NETFRAMEWORK - runner.testCase "Encode.Auto.toString works with char based Enums" <| fun _ -> - let expected = ((int) 'A').ToString() // "65" - let actual = Encode.Auto.toString(0, CharEnum.A) - runner.equal actual expected + runner.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 + + runner.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 5db2e47..7eb703d 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..5bc54ec 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 -> @@ -374,7 +375,7 @@ type Language = #endif type Enum_Int8 = - | Zero = 0y + | Zero = (0y: int8) | NinetyNine = 99y type Enum_UInt8 = @@ -423,11 +424,12 @@ 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 + ] + // null let decode: Decoder = Decode.field "type" Decode.string From 09f6a63b6e260f59eb0dd614e0f5c7e7c131a36b Mon Sep 17 00:00:00 2001 From: njlr Date: Sun, 14 Apr 2024 12:55:11 +0100 Subject: [PATCH 02/13] Remove dead code --- packages/Thoth.Json.Auto/Domain.fs | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/packages/Thoth.Json.Auto/Domain.fs b/packages/Thoth.Json.Auto/Domain.fs index 91711a6..1663222 100644 --- a/packages/Thoth.Json.Auto/Domain.fs +++ b/packages/Thoth.Json.Auto/Domain.fs @@ -56,28 +56,6 @@ module Extra = DecoderOverrides = Map.empty } - // let overrideEncoderImpl (typeKey : TypeKey) (encoder : obj) (opts : ExtraCoders) = - // { - // opts with - // EncoderOverrides = - // opts.EncoderOverrides - // |> Map.add typeKey encoder - // } - - // let inline overrideEncoder (encoder : Encoder<'t>) (opts : ExtraCoders) = - // overrideEncoderImpl (TypeKey.ofType typeof<'t>) encoder opts - - // let overrideDecoderImpl (typeKey : TypeKey) (decoder : obj) (opts : ExtraCoders) = - // { - // opts with - // DecoderOverrides = - // opts.DecoderOverrides - // |> Map.add typeKey decoder - // } - - // let inline overrideDecoder (decoder : Decoder<'t>) (opts : ExtraCoders) = - // overrideDecoderImpl (TypeKey.ofType typeof<'t>) decoder opts - let inline withCustom (encoder: Encoder<'t>) (decoder: Decoder<'t>) From bac9fc6ca809d235220a8669d99c0c7f1ccf8819 Mon Sep 17 00:00:00 2001 From: njlr Date: Sun, 14 Apr 2024 12:57:06 +0100 Subject: [PATCH 03/13] Tidy up comments --- tests/Thoth.Json.Tests/Types.fs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/Thoth.Json.Tests/Types.fs b/tests/Thoth.Json.Tests/Types.fs index 5bc54ec..23379fb 100644 --- a/tests/Thoth.Json.Tests/Types.fs +++ b/tests/Thoth.Json.Tests/Types.fs @@ -375,7 +375,7 @@ type Language = #endif type Enum_Int8 = - | Zero = (0y: int8) + | Zero = 0y | NinetyNine = 99y type Enum_UInt8 = @@ -429,7 +429,6 @@ module IntAsRecord = "type", Encode.string "int" "value", Encode.int value ] - // null let decode: Decoder = Decode.field "type" Decode.string From dc9094dbcf7aeaa31df3c39bbec0f621de46ecc5 Mon Sep 17 00:00:00 2001 From: njlr Date: Sat, 4 May 2024 17:42:31 +0100 Subject: [PATCH 04/13] chore: Merge branch 'main' of https://github.com/thoth-org/Thoth.Json into HEAD --- .config/dotnet-tools.json | 8 +- .github/workflows/ci.yml | 2 +- .husky/commit-msg | 22 + build/Build.fsproj | 2 +- build/Test/JavaScript.fs | 1 + build/Test/Legacy.fs | 1 + build/Test/Python.fs | 1 + build/packages.lock.json | 2 +- commit.config.json | 19 + docs/documentation/advanced/unkown-fields.fsx | 24 +- packages/Thoth.Json.Core/CHANGELOG.md | 9 + packages/Thoth.Json.Core/Encode.fs | 182 ++++-- packages/Thoth.Json.Core/Types.fs | 16 +- packages/Thoth.Json.JavaScript/CHANGELOG.md | 9 + packages/Thoth.Json.JavaScript/Encode.fs | 15 +- packages/Thoth.Json.Newtonsoft/CHANGELOG.md | 9 + packages/Thoth.Json.Newtonsoft/Encode.fs | 22 +- packages/Thoth.Json.Python/CHANGELOG.md | 16 + packages/Thoth.Json.Python/Encode.fs | 28 +- packages/Thoth.Json.Python/Python.fs | 6 +- pnpm-lock.yaml | 566 +++++++++++++++++- tests/Thoth.Json.Tests/Encoders.fs | 35 ++ tests/Thoth.Json.Tests/Util.fs | 2 +- 23 files changed, 891 insertions(+), 106 deletions(-) create mode 100755 .husky/commit-msg create mode 100644 commit.config.json diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index fde43f9..2baae51 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "fable": { - "version": "4.9.0", + "version": "4.17.0", "commands": [ "fable" ] @@ -19,6 +19,12 @@ "commands": [ "husky" ] + }, + "easybuild.commitlinter": { + "version": "0.1.0", + "commands": [ + "commit-linter" + ] } } } \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c1c89a0..f4702cb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,7 @@ jobs: if: matrix.platform == 'windows-latest' uses: actions/setup-python@v4 with: - python-version: '3.10' + python-version: '3.12' - run: python --version - name: Test (Unix) if: matrix.platform == 'ubuntu-latest' || matrix.platform == 'macOS-latest' diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100755 index 0000000..2fb195e --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1,22 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +## husky task runner examples ------------------- +## Note : for local installation use 'dotnet' prefix. e.g. 'dotnet husky' + +## run all tasks +#husky run + +### run all tasks with group: 'group-name' +#husky run --group group-name + +## run task with name: 'task-name' +#husky run --name task-name + +## pass hook arguments to task +#husky run --args "$1" "$2" + +## or put your custom commands ------------------- +#echo 'Husky.Net is awesome!' + +dotnet commit-linter -c commit.config.json "$1" diff --git a/build/Build.fsproj b/build/Build.fsproj index 2d7d641..caeec54 100644 --- a/build/Build.fsproj +++ b/build/Build.fsproj @@ -1,7 +1,7 @@ Exe - net6.0 + net8.0 diff --git a/build/Test/JavaScript.fs b/build/Test/JavaScript.fs index 7039e09..bab42b1 100644 --- a/build/Test/JavaScript.fs +++ b/build/Test/JavaScript.fs @@ -24,6 +24,7 @@ let handle (args: string list) = |> CmdLine.appendRaw "fable" |> CmdLine.appendPrefix "--outDir" outDir |> CmdLine.appendRaw "--noCache" + |> CmdLine.appendRaw "--test:MSBuildCracker" if isWatch then CmdLine.empty diff --git a/build/Test/Legacy.fs b/build/Test/Legacy.fs index 07fda1d..02ca863 100644 --- a/build/Test/Legacy.fs +++ b/build/Test/Legacy.fs @@ -24,6 +24,7 @@ let handle (args: string list) = |> CmdLine.appendRaw "fable" |> CmdLine.appendPrefix "--outDir" outDir |> CmdLine.appendRaw "--noCache" + |> CmdLine.appendRaw "--test:MSBuildCracker" if isWatch then CmdLine.empty diff --git a/build/Test/Python.fs b/build/Test/Python.fs index c0e7063..fbf76c4 100644 --- a/build/Test/Python.fs +++ b/build/Test/Python.fs @@ -22,6 +22,7 @@ let handle (args: string list) = |> CmdLine.appendPrefix "--outDir" outDir |> CmdLine.appendPrefix "--lang" "python" |> CmdLine.appendRaw "--noCache" + |> CmdLine.appendRaw "--test:MSBuildCracker" |> CmdLine.appendIf isWatch "--watch" |> CmdLine.appendRaw runArg |> CmdLine.appendRaw "python" diff --git a/build/packages.lock.json b/build/packages.lock.json index f6e9354..8ad2e13 100644 --- a/build/packages.lock.json +++ b/build/packages.lock.json @@ -1,7 +1,7 @@ { "version": 2, "dependencies": { - "net6.0": { + "net8.0": { "BlackFox.CommandLine": { "type": "Direct", "requested": "[1.0.0, )", diff --git a/commit.config.json b/commit.config.json new file mode 100644 index 0000000..1162ef0 --- /dev/null +++ b/commit.config.json @@ -0,0 +1,19 @@ +{ + "types": [ + { "name": "feat", "description": "A new feature", "skipTagLine": false }, + { "name": "fix", "description": "A bug fix", "skipTagLine": false }, + { "name": "docs", "description": "Documentation changes", "skipTagLine": false }, + { "name": "test", "description": "Adding missing tests or correcting existing tests", "skipTagLine": false }, + { "name": "style", "description": "Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)", "skipTagLine": false }, + { "name": "refactor", "description": "A code change that neither fixes a bug nor adds a feature", "skipTagLine": false }, + { "name": "ci", "description": "Changes to CI/CD configuration" }, + { "name": "chore", "description": "Changes to the build process or auxiliary tools and libraries such as documentation generation" } + ], + "tags": [ + "legacy", + "core", + "javascript", + "python", + "newtonsoft" + ] +} diff --git a/docs/documentation/advanced/unkown-fields.fsx b/docs/documentation/advanced/unkown-fields.fsx index c7a9a5e..050674f 100644 --- a/docs/documentation/advanced/unkown-fields.fsx +++ b/docs/documentation/advanced/unkown-fields.fsx @@ -30,9 +30,9 @@ In this example, we know that: - The `ts` field required - There is an unkown number of `Rate` fields which consists of: - The key property which has the format: - 1. `sourceCurrency` + 1. `baseCurrency` 2. `_` - 3. `targetCurrency` + 3. `quoteCurrency` - The value should have a `rate` field which is a `decimal` We are now going to write a decoder capable of handling such a JSON. @@ -115,8 +115,8 @@ This type contains the name of the 2 currencies and the rate. type Rate = { - SourceCurrency: string - TargetCurrency: string + BaseCurrency: string + QuoteCurrency: string Rate: decimal } @@ -142,14 +142,14 @@ module Rates = |> List.map (fun (fieldName, (RateObject rate)) -> // We consider the fieldName valid if it contains a `_` - // The format is [sourceCurrency]_[targetCurrency] + // The format is [baseCurrency]_[quoteCurrency] match fieldName.Split('_') with - | [| sourceCurrency; targetCurrency |] -> + | [| baseCurrency; quoteCurrency |] -> // The fieldName is valid, we can build the Rate record Some { - SourceCurrency = sourceCurrency - TargetCurrency = targetCurrency + BaseCurrency = baseCurrency + QuoteCurrency = quoteCurrency Rate = rate } // If the fieldName is invalid @@ -214,13 +214,13 @@ Decode.fromString ExchangeRate.decoder jsonWithError // Time = System.DateTime(2020, 1, 1, 0, 0, 0, 0, System.DateTimeKind.Utc) // Rates = [ // { -// SourceCurrency = "EUR" -// TargetCurrency = "PLN" +// BaseCurrency = "EUR" +// QuoteCurrency = "PLN" // Rate = 4.55m // } // { -// SourceCurrency = "USD" -// TargetCurrency = "PLN" +// BaseCurrency = "USD" +// QuoteCurrency = "PLN" // Rate = 4.01m // } // ] diff --git a/packages/Thoth.Json.Core/CHANGELOG.md b/packages/Thoth.Json.Core/CHANGELOG.md index c680a45..da302bd 100644 --- a/packages/Thoth.Json.Core/CHANGELOG.md +++ b/packages/Thoth.Json.Core/CHANGELOG.md @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Changed + +* Rework encoder API to not need a custom DU ([GH-188](https://github.com/thoth-org/Thoth.Json/pull/188/)) +* Rework object encoder helper to support JSON backends not allowing mutating a JSON object + +### Fixed + +* Encoding negative integers should keep their sign ([GH-187](https://github.com/thoth-org/Thoth.Json/issues/187)) + ## 0.2.1 - 2023-12-12 ### Fixed diff --git a/packages/Thoth.Json.Core/Encode.fs b/packages/Thoth.Json.Core/Encode.fs index babff7d..3b27e5f 100644 --- a/packages/Thoth.Json.Core/Encode.fs +++ b/packages/Thoth.Json.Core/Encode.fs @@ -7,24 +7,72 @@ open System [] module Encode = - let inline string value = Json.String value - let inline char value = Json.Char value + let inline string value = + { new IEncodable with + member _.Encode(helpers) = helpers.encodeString value + } + + let inline char value = + { new IEncodable with + member _.Encode(helpers) = helpers.encodeChar value + } + let inline guid value = value.ToString() |> string - let inline float value = Json.DecimalNumber value - let float32 (value: float32) = - Json.DecimalNumber(Operators.float value) + let inline float value = + { new IEncodable with + member _.Encode(helpers) = helpers.encodeDecimalNumber value + } + + let float32 (value: float32) = float (Operators.float value) let inline decimal (value: decimal) = value.ToString(CultureInfo.InvariantCulture) |> string - let inline nil<'T> = Json.Null - let inline bool value = Json.Boolean value - let inline object values = Json.Object values - let inline array values = Json.Array values - let list values = Json.Array(values |> List.toArray) - let seq values = Json.Array(values |> Seq.toArray) - let dict (values: Map) : Json = values |> Map.toList |> object + let inline nil<'T> = + { new IEncodable with + member _.Encode(helpers) = helpers.encodeNull () + } + + let inline bool value = + { new IEncodable with + member _.Encode(helpers) = helpers.encodeBool value + } + + let inline object (values: seq) = + { new IEncodable with + member _.Encode(helpers) = + values + |> Seq.map (fun (k, v) -> (k, v.Encode(helpers))) + |> helpers.encodeObject + } + + let inline array (values: IEncodable array) = + { new IEncodable with + member _.Encode(helpers) = + values + |> Array.map (fun v -> v.Encode(helpers)) + |> helpers.encodeArray + } + + let list (values: IEncodable list) = + { new IEncodable with + member _.Encode(helpers) = + values + |> List.map (fun v -> v.Encode(helpers)) + |> helpers.encodeList + } + + let seq (values: IEncodable seq) = + { new IEncodable with + member _.Encode(helpers) = + values + |> Seq.map (fun v -> v.Encode(helpers)) + |> helpers.encodeSeq + } + + let dict (values: Map) : IEncodable = + values |> Map.toSeq |> object let inline bigint (value: bigint) = value.ToString() |> string @@ -36,12 +84,41 @@ module Encode = let inline datetime (value: DateTime) = value.ToString("O", CultureInfo.InvariantCulture) |> string - let inline sbyte (value: sbyte) = Json.IntegralNumber(uint32 value) - let inline byte (value: byte) = Json.IntegralNumber(uint32 value) - let inline int16 (value: int16) = Json.IntegralNumber(uint32 value) - let inline uint16 (value: uint16) = Json.IntegralNumber(uint32 value) - let inline int (value: int) = Json.IntegralNumber(uint32 value) - let inline uint32 (value: uint32) = Json.IntegralNumber value + let inline sbyte (value: sbyte) = + { new IEncodable with + member _.Encode(helpers) = + helpers.encodeSignedIntegralNumber (int32 value) + } + + let inline byte (value: byte) = + { new IEncodable with + member _.Encode(helpers) = + helpers.encodeUnsignedIntegralNumber (uint32 value) + } + + let inline int16 (value: int16) = + { new IEncodable with + member _.Encode(helpers) = + helpers.encodeSignedIntegralNumber (int32 value) + } + + let inline uint16 (value: uint16) = + { new IEncodable with + member _.Encode(helpers) = + helpers.encodeUnsignedIntegralNumber (uint32 value) + } + + let inline int (value: int) = + { new IEncodable with + member _.Encode(helpers) = + helpers.encodeSignedIntegralNumber value + } + + let inline uint32 (value: uint32) = + { new IEncodable with + member _.Encode(helpers) = + helpers.encodeUnsignedIntegralNumber value + } let inline int64 (value: int64) = value.ToString(CultureInfo.InvariantCulture) |> string @@ -49,9 +126,9 @@ module Encode = let inline uint64 (value: uint64) = value.ToString(CultureInfo.InvariantCulture) |> string - let inline unit () = Json.Unit + let inline unit () = nil - let tuple2 (enc1: Encoder<'T1>) (enc2: Encoder<'T2>) (v1, v2) : Json = + let tuple2 (enc1: Encoder<'T1>) (enc2: Encoder<'T2>) (v1, v2) : IEncodable = array [| enc1 v1 @@ -63,7 +140,7 @@ module Encode = (enc2: Encoder<'T2>) (enc3: Encoder<'T3>) (v1, v2, v3) - : Json + : IEncodable = array [| @@ -78,7 +155,7 @@ module Encode = (enc3: Encoder<'T3>) (enc4: Encoder<'T4>) (v1, v2, v3, v4) - : Json + : IEncodable = array [| @@ -95,7 +172,7 @@ module Encode = (enc4: Encoder<'T4>) (enc5: Encoder<'T5>) (v1, v2, v3, v4, v5) - : Json + : IEncodable = array [| @@ -114,7 +191,7 @@ module Encode = (enc5: Encoder<'T5>) (enc6: Encoder<'T6>) (v1, v2, v3, v4, v5, v6) - : Json + : IEncodable = array [| @@ -135,7 +212,7 @@ module Encode = (enc6: Encoder<'T6>) (enc7: Encoder<'T7>) (v1, v2, v3, v4, v5, v6, v7) - : Json + : IEncodable = array [| @@ -158,7 +235,7 @@ module Encode = (enc7: Encoder<'T7>) (enc8: Encoder<'T8>) (v1, v2, v3, v4, v5, v6, v7, v8) - : Json + : IEncodable = array [| @@ -177,7 +254,7 @@ module Encode = (keyEncoder: Encoder<'key>) (valueEncoder: Encoder<'value>) (values: Map<'key, 'value>) - : Json + : IEncodable = values |> Map.toList @@ -190,44 +267,41 @@ module Encode = module Enum = - let byte<'TEnum when 'TEnum: enum> (value: 'TEnum) : Json = + let byte<'TEnum when 'TEnum: enum> (value: 'TEnum) : IEncodable = LanguagePrimitives.EnumToValue value |> byte - let sbyte<'TEnum when 'TEnum: enum> (value: 'TEnum) : Json = + let sbyte<'TEnum when 'TEnum: enum> + (value: 'TEnum) + : IEncodable + = LanguagePrimitives.EnumToValue value |> sbyte - let int16<'TEnum when 'TEnum: enum> (value: 'TEnum) : Json = + let int16<'TEnum when 'TEnum: enum> + (value: 'TEnum) + : IEncodable + = LanguagePrimitives.EnumToValue value |> int16 - let uint16<'TEnum when 'TEnum: enum> (value: 'TEnum) : Json = + let uint16<'TEnum when 'TEnum: enum> + (value: 'TEnum) + : IEncodable + = LanguagePrimitives.EnumToValue value |> uint16 - let int<'TEnum when 'TEnum: enum> (value: 'TEnum) : Json = + let int<'TEnum when 'TEnum: enum> (value: 'TEnum) : IEncodable = LanguagePrimitives.EnumToValue value |> int - let uint32<'TEnum when 'TEnum: enum> (value: 'TEnum) : Json = + let uint32<'TEnum when 'TEnum: enum> + (value: 'TEnum) + : IEncodable + = LanguagePrimitives.EnumToValue value |> uint32 - let option (encoder: 'a -> Json) = + let option (encoder: Encoder<'a>) = Option.map encoder >> Option.defaultWith (fun _ -> nil) - let lazily (enc: Lazy>) : Encoder<'t> = fun x -> enc.Value x - - let rec toJsonValue (helpers: IEncoderHelpers<'JsonValue>) (json: Json) = - match json with - | Json.String value -> helpers.encodeString value - | Json.IntegralNumber value -> helpers.encodeIntegralNumber value - | Json.Object values -> - let o = helpers.createEmptyObject () - - for k, v in values do - helpers.setPropertyOnObject (o, k, toJsonValue helpers v) - - o - | Json.Char value -> helpers.encodeChar value - | Json.DecimalNumber value -> helpers.encodeDecimalNumber value - | Json.Null -> helpers.encodeNull () - | Json.Boolean value -> helpers.encodeBool value - | Json.Array value -> - value |> Array.map (toJsonValue helpers) |> helpers.encodeArray - | Json.Unit -> helpers.encodeNull () + let inline toJsonValue + (helpers: IEncoderHelpers<'JsonValue>) + (json: IEncodable) + = + json.Encode(helpers) diff --git a/packages/Thoth.Json.Core/Types.fs b/packages/Thoth.Json.Core/Types.fs index d2a23fc..776d321 100644 --- a/packages/Thoth.Json.Core/Types.fs +++ b/packages/Thoth.Json.Core/Types.fs @@ -25,13 +25,15 @@ type IEncoderHelpers<'JsonValue> = abstract encodeDecimalNumber: float -> 'JsonValue abstract encodeBool: bool -> 'JsonValue abstract encodeNull: unit -> 'JsonValue - abstract createEmptyObject: unit -> 'JsonValue - abstract setPropertyOnObject: 'JsonValue * string * 'JsonValue -> unit + abstract encodeObject: (string * 'JsonValue) seq -> 'JsonValue abstract encodeArray: 'JsonValue array -> 'JsonValue abstract encodeList: 'JsonValue list -> 'JsonValue abstract encodeSeq: 'JsonValue seq -> 'JsonValue - abstract encodeIntegralNumber: uint32 -> 'JsonValue - + // See https://github.com/thoth-org/Thoth.Json/issues/187 for more information + // about why we make a distinction between signed and unsigned integral numbers + // when encoding them. + abstract encodeSignedIntegralNumber: int32 -> 'JsonValue + abstract encodeUnsignedIntegralNumber: uint32 -> 'JsonValue type ErrorReason<'JsonValue> = | BadPrimitive of string * 'JsonValue @@ -73,4 +75,8 @@ type Json = | IntegralNumber of uint32 | Unit -type Encoder<'T> = 'T -> Json +type IEncodable = + abstract member Encode<'JsonValue> : + helpers: IEncoderHelpers<'JsonValue> -> 'JsonValue + +type Encoder<'T> = 'T -> IEncodable diff --git a/packages/Thoth.Json.JavaScript/CHANGELOG.md b/packages/Thoth.Json.JavaScript/CHANGELOG.md index d29c3fe..58c32a4 100644 --- a/packages/Thoth.Json.JavaScript/CHANGELOG.md +++ b/packages/Thoth.Json.JavaScript/CHANGELOG.md @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Changed + +* Rework encoder API to not need a custom DU ([GH-188](https://github.com/thoth-org/Thoth.Json/pull/188/)) +* Rework object encoder helper to support JSON backends not allowing mutating a JSON object + +### Fixed + +* Encoding negative integers should keep their sign ([GH-187](https://github.com/thoth-org/Thoth.Json/issues/187)) + ### Added * `Decode.unsafeFromString` diff --git a/packages/Thoth.Json.JavaScript/Encode.fs b/packages/Thoth.Json.JavaScript/Encode.fs index d3152b4..d58f56a 100644 --- a/packages/Thoth.Json.JavaScript/Encode.fs +++ b/packages/Thoth.Json.JavaScript/Encode.fs @@ -15,18 +15,23 @@ module Encode = member _.encodeDecimalNumber value = box value member _.encodeBool value = box value member _.encodeNull() = box null - member _.createEmptyObject() = obj () - member _.setPropertyOnObject(o: obj, key: string, value: obj) = - o?(key) <- value + member _.encodeObject(values) = + let o = obj () + + for key, value in values do + o?(key) <- value + + o member _.encodeArray values = JS.Constructors.Array.from values member _.encodeList values = JS.Constructors.Array.from values member _.encodeSeq values = JS.Constructors.Array.from values - member _.encodeIntegralNumber value = box value + member _.encodeSignedIntegralNumber value = box value + member _.encodeUnsignedIntegralNumber value = box value } - let toString (space: int) (value: Json) : string = + let toString (space: int) (value: IEncodable) : string = let json = Encode.toJsonValue helpers value JS.JSON.stringify (json, space = space) diff --git a/packages/Thoth.Json.Newtonsoft/CHANGELOG.md b/packages/Thoth.Json.Newtonsoft/CHANGELOG.md index d29c3fe..58c32a4 100644 --- a/packages/Thoth.Json.Newtonsoft/CHANGELOG.md +++ b/packages/Thoth.Json.Newtonsoft/CHANGELOG.md @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Changed + +* Rework encoder API to not need a custom DU ([GH-188](https://github.com/thoth-org/Thoth.Json/pull/188/)) +* Rework object encoder helper to support JSON backends not allowing mutating a JSON object + +### Fixed + +* Encoding negative integers should keep their sign ([GH-187](https://github.com/thoth-org/Thoth.Json/issues/187)) + ### Added * `Decode.unsafeFromString` diff --git a/packages/Thoth.Json.Newtonsoft/Encode.fs b/packages/Thoth.Json.Newtonsoft/Encode.fs index d7b78a3..bbfd1e8 100644 --- a/packages/Thoth.Json.Newtonsoft/Encode.fs +++ b/packages/Thoth.Json.Newtonsoft/Encode.fs @@ -15,29 +15,29 @@ module Encode = member _.encodeDecimalNumber value = JValue(value) member _.encodeBool value = JValue(value) member _.encodeNull() = JValue.CreateNull() - member _.createEmptyObject() = JObject() - member _.setPropertyOnObject - ( - o: JToken, - key: string, - value: JToken - ) - = - o[key] <- value + member _.encodeObject(values) = + let o = JObject() + + for key, value in values do + o.[key] <- value + + o member _.encodeArray values = JArray(values) member _.encodeList values = JArray(values) member _.encodeSeq values = JArray(values) - member _.encodeIntegralNumber(value: uint32) = + member _.encodeSignedIntegralNumber(value: int32) = JValue(value) + + member _.encodeUnsignedIntegralNumber(value: uint32) = // We need to force the cast to uint64 here, otherwise // Newtonsoft resolve the constructor to JValue(decimal) // when we actually want to output a number without decimals JValue(uint64 value) } - let toString (space: int) (value: Json) : string = + let toString (space: int) (value: IEncodable) : string = let format = if space = 0 then Formatting.None diff --git a/packages/Thoth.Json.Python/CHANGELOG.md b/packages/Thoth.Json.Python/CHANGELOG.md index d29c3fe..bd1d682 100644 --- a/packages/Thoth.Json.Python/CHANGELOG.md +++ b/packages/Thoth.Json.Python/CHANGELOG.md @@ -7,10 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Changed + +* Rework encoder API to not need a custom DU ([GH-188](https://github.com/thoth-org/Thoth.Json/pull/188/)) +* Rework object encoder helper to support JSON backends not allowing mutating a JSON object + +### Fixed + +* Encoding negative integers should keep their sign ([GH-187](https://github.com/thoth-org/Thoth.Json/issues/187)) +* Emit non ascii characters as is when encoding to JSON (JSON specs advice to use utf-8 string and not limit to ascii) (align non compacted JSON behaviour with compacted JSON) + +## 0.2.0 - 2024-04-03 + ### Added * `Decode.unsafeFromString` +### Fixed + +* Emit non ascii characters as is when encoding to JSON (JSON specs advice to use utf-8 string and not limit to ascii) + ## 0.1.0 - 2023-12-12 ### Added diff --git a/packages/Thoth.Json.Python/Encode.fs b/packages/Thoth.Json.Python/Encode.fs index a961925..6006ea4 100644 --- a/packages/Thoth.Json.Python/Encode.fs +++ b/packages/Thoth.Json.Python/Encode.fs @@ -15,19 +15,28 @@ module Encode = member _.encodeDecimalNumber value = box value member _.encodeBool value = box value member _.encodeNull() = box null - member _.createEmptyObject() = emitPyExpr () "{}" - member _.setPropertyOnObject(o, key: string, value: obj) = - o?(key) <- value + member _.encodeObject(values) = + let o = emitPyExpr () "{}" + + for key, value in values do + o?(key) <- value + + o member _.encodeArray values = values - member _.encodeList values = JS.Constructors.Array.from values - member _.encodeSeq values = JS.Constructors.Array.from values - member _.encodeIntegralNumber value = box value + member this.encodeList values = + values |> List.toArray |> this.encodeArray + + member this.encodeSeq values = + values |> Seq.toArray |> this.encodeArray + + member _.encodeSignedIntegralNumber value = box value + member _.encodeUnsignedIntegralNumber value = box value } - let toString (space: int) (value: Json) : string = + let toString (space: int) (value: IEncodable) : string = let json = Encode.toJsonValue helpers value // If we pass an indention of 0 to Python's json.dumps, it will // insert newlines, between each element instead of compressing @@ -45,7 +54,8 @@ module Encode = [| "," ":" - |] + |], + ensure_ascii = false ) else - Python.Json.json.dumps (json, indent = space) + Python.Json.json.dumps (json, indent = space, ensure_ascii = false) diff --git a/packages/Thoth.Json.Python/Python.fs b/packages/Thoth.Json.Python/Python.fs index d413c06..b66cc35 100644 --- a/packages/Thoth.Json.Python/Python.fs +++ b/packages/Thoth.Json.Python/Python.fs @@ -9,7 +9,11 @@ module Json = type IExports = [] abstract dumps: - obj: obj * ?separators: string array * ?indent: int -> string + obj: obj * + ?separators: string array * + ?indent: int * + ?ensure_ascii: bool -> + string [] let json: IExports = nativeOnly diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7522ca0..48cee6c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,7 +16,7 @@ devDependencies: version: 6.5.1 '@mangelmaxime/eleventy-layout-nacara': specifier: ^0.1.0 - version: link:../../MangelMaxime/eleventy-layout-nacara + version: 0.1.0(@types/markdown-it@14.1.1)(markdown-it@13.0.2) '@mangelmaxime/eleventy-plugin-fsharp': specifier: ^1.4.0 version: 1.4.0(@11ty/eleventy@2.0.1) @@ -99,7 +99,7 @@ packages: cross-spawn: 7.0.3 debug: 4.3.4(supports-color@8.1.1) dependency-graph: 0.11.0 - ejs: 3.1.9 + ejs: 3.1.10 fast-glob: 3.3.2 graceful-fs: 4.2.11 gray-matter: 4.0.3 @@ -174,6 +174,33 @@ packages: resolution: {integrity: sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==} dev: true + /@mangelmaxime/eleventy-layout-nacara@0.1.0(@types/markdown-it@14.1.1)(markdown-it@13.0.2): + resolution: {integrity: sha512-C8kMHm+ogiPUGWOymJRHZ+RC/whd7qmcbvP9Kl6ZhJpvHD4Osm6aCA7abVevIMCGljmR0mW7CtAYlISUc1qWwg==} + dependencies: + '@11ty/eleventy': 2.0.1 + '@svgdotjs/svg.js': 3.2.0 + bulma: 0.9.4 + cross-spawn: 7.0.3 + dayjs: 1.11.11 + eleventy-sass: 2.2.4(@11ty/eleventy@2.0.1) + fs-extra: 11.2.0 + lucide-static: 0.294.0 + markdown-it-anchor: 8.6.7(@types/markdown-it@14.1.1)(markdown-it@13.0.2) + nano-jsx: 0.1.0 + sass: 1.76.0 + simple-icons: 10.4.0 + slugify: 1.6.6 + svgdom: 0.1.13 + transitivePeerDependencies: + - '@types/markdown-it' + - bufferutil + - eleventy-plugin-clean + - eleventy-plugin-rev + - markdown-it + - supports-color + - utf-8-validate + dev: true + /@mangelmaxime/eleventy-plugin-fsharp@1.4.0(@11ty/eleventy@2.0.1): resolution: {integrity: sha512-Z1Lo1crsHBkMiV0MNatyTgsr8EYSwvFavGAQjadazsOIwZaK/LNrqZtmrRLQXK9FMf2EwsWXHaaEcucpGBE2/g==} peerDependencies: @@ -220,6 +247,31 @@ packages: lodash.deburr: 4.1.0 dev: true + /@svgdotjs/svg.js@3.2.0: + resolution: {integrity: sha512-Tr8p+QVP7y+QT1GBlq1Tt57IvedVH8zCPoYxdHLX0Oof3a/PqnC/tXAkVufv1JQJfsDHlH/UrjcDfgxSofqSNA==} + dev: true + + /@swc/helpers@0.3.17: + resolution: {integrity: sha512-tb7Iu+oZ+zWJZ3HJqwx8oNwSDIU440hmVMDPhpACWQWnrZHK99Bxs70gT1L2dnr5Hg50ZRWEFkQCAnOVVV0z1Q==} + dependencies: + tslib: 2.6.2 + dev: true + + /@types/linkify-it@5.0.0: + resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} + dev: true + + /@types/markdown-it@14.1.1: + resolution: {integrity: sha512-4NpsnpYl2Gt1ljyBGrKMxFYAYvpqbnnkgP/i/g+NLpjEUa3obn1XJCur9YbEXKDAkaXqsR1LbDnGEJ0MmKFxfg==} + dependencies: + '@types/linkify-it': 5.0.0 + '@types/mdurl': 2.0.0 + dev: true + + /@types/mdurl@2.0.0: + resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} + dev: true + /@types/minimatch@3.0.5: resolution: {integrity: sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==} dev: true @@ -273,6 +325,14 @@ packages: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} dev: true + /array-buffer-byte-length@1.0.1: + resolution: {integrity: sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + is-array-buffer: 3.0.4 + dev: true + /array-differ@1.0.0: resolution: {integrity: sha512-LeZY+DZDRnvP7eMuQ6LHfCzUGxAAIViUBliK24P3hWXL6y4SortgR6Nim6xrkfSLlmH0+k+9NYNwVC2s53ZrYQ==} engines: {node: '>=0.10.0'} @@ -322,6 +382,13 @@ packages: resolution: {integrity: sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==} dev: true + /available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + dependencies: + possible-typed-array-names: 1.0.0 + dev: true + /babel-walk@3.0.0-canary-5: resolution: {integrity: sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==} engines: {node: '>= 10.0.0'} @@ -333,6 +400,10 @@ packages: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} dev: true + /base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + dev: true + /bcp-47-match@1.0.3: resolution: {integrity: sha512-LggQ4YTdjWQSKELZF5JwchnBa1u0pIQSZf5lSdOHEdbVP55h0qICA/FUp3+W99q0xqxYa1ZQizTUH87gecII5w==} dev: true @@ -377,6 +448,12 @@ packages: fill-range: 7.0.1 dev: true + /brotli@1.3.3: + resolution: {integrity: sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==} + dependencies: + base64-js: 1.5.1 + dev: true + /browser-stdout@1.3.1: resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==} dev: true @@ -393,6 +470,17 @@ packages: set-function-length: 1.1.1 dev: true + /call-bind@1.0.7: + resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} + engines: {node: '>= 0.4'} + dependencies: + es-define-property: 1.0.0 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + set-function-length: 1.2.2 + dev: true + /camelcase@6.3.0: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} @@ -449,6 +537,11 @@ packages: wrap-ansi: 7.0.0 dev: true + /clone@2.1.2: + resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==} + engines: {node: '>=0.8'} + dev: true + /color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -499,6 +592,10 @@ packages: which: 2.0.2 dev: true + /dayjs@1.11.11: + resolution: {integrity: sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg==} + dev: true + /debug@2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} peerDependencies: @@ -528,6 +625,30 @@ packages: engines: {node: '>=10'} dev: true + /deep-equal@2.2.3: + resolution: {integrity: sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.1 + call-bind: 1.0.5 + es-get-iterator: 1.1.3 + get-intrinsic: 1.2.2 + is-arguments: 1.1.1 + is-array-buffer: 3.0.4 + is-date-object: 1.0.5 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.3 + isarray: 2.0.5 + object-is: 1.1.6 + object-keys: 1.1.1 + object.assign: 4.1.5 + regexp.prototype.flags: 1.5.2 + side-channel: 1.0.6 + which-boxed-primitive: 1.0.2 + which-collection: 1.0.2 + which-typed-array: 1.1.15 + dev: true + /define-data-property@1.1.1: resolution: {integrity: sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==} engines: {node: '>= 0.4'} @@ -537,6 +658,24 @@ packages: has-property-descriptors: 1.0.1 dev: true + /define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + dependencies: + es-define-property: 1.0.0 + es-errors: 1.3.0 + gopd: 1.0.1 + dev: true + + /define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.1 + has-property-descriptors: 1.0.1 + object-keys: 1.1.1 + dev: true + /dependency-graph@0.11.0: resolution: {integrity: sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==} engines: {node: '>= 0.6.0'} @@ -548,6 +687,10 @@ packages: hasBin: true dev: true + /dfa@1.2.0: + resolution: {integrity: sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==} + dev: true + /diff@5.0.0: resolution: {integrity: sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==} engines: {node: '>=0.3.1'} @@ -588,14 +731,34 @@ packages: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} dev: true - /ejs@3.1.9: - resolution: {integrity: sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==} + /ejs@3.1.10: + resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} engines: {node: '>=0.10.0'} hasBin: true dependencies: jake: 10.8.7 dev: true + /eleventy-sass@2.2.4(@11ty/eleventy@2.0.1): + resolution: {integrity: sha512-lVGk6rqw4CEzmYEafB+K7liH6JTtiWg1kW+wDbeniAbdNqHfwjgADzlIZXO0O74QMJQUmRiRFwx+FD47brnEew==} + peerDependencies: + '@11ty/eleventy': ^1.0.0 || ^2.0.0-canary.12 || ^2.0.0-beta.1 + eleventy-plugin-clean: ^1.1.0 + eleventy-plugin-rev: ^2.0.0 + peerDependenciesMeta: + eleventy-plugin-clean: + optional: true + eleventy-plugin-rev: + optional: true + dependencies: + '@11ty/eleventy': 2.0.1 + debug: 4.3.4(supports-color@8.1.1) + kleur: 4.1.5 + sass: 1.76.0 + transitivePeerDependencies: + - supports-color + dev: true + /email-addresses@5.0.0: resolution: {integrity: sha512-4OIPYlA6JXqtVn8zpHpGiI7vE6EQOAg16aGnDMIAlZVinnoZ8208tW1hAbjWydgN/4PLTT9q+O1K6AH/vALJGw==} dev: true @@ -625,6 +788,32 @@ packages: prr: 1.0.1 dev: true + /es-define-property@1.0.0: + resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.4 + dev: true + + /es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + dev: true + + /es-get-iterator@1.1.3: + resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==} + dependencies: + call-bind: 1.0.5 + get-intrinsic: 1.2.2 + has-symbols: 1.0.3 + is-arguments: 1.1.1 + is-map: 2.0.3 + is-set: 2.0.3 + is-string: 1.0.7 + isarray: 2.0.5 + stop-iteration-iterator: 1.0.0 + dev: true + /escalade@3.1.1: resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} engines: {node: '>=6'} @@ -755,6 +944,26 @@ packages: hasBin: true dev: true + /fontkit@1.9.0: + resolution: {integrity: sha512-HkW/8Lrk8jl18kzQHvAw9aTHe1cqsyx5sDnxncx652+CIfhawokEPkeM3BoIC+z/Xv7a0yMr0f3pRRwhGH455g==} + dependencies: + '@swc/helpers': 0.3.17 + brotli: 1.3.3 + clone: 2.1.2 + deep-equal: 2.2.3 + dfa: 1.2.0 + restructure: 2.0.1 + tiny-inflate: 1.0.3 + unicode-properties: 1.4.1 + unicode-trie: 2.0.0 + dev: true + + /for-each@0.3.3: + resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + dependencies: + is-callable: 1.2.7 + dev: true + /fs-extra@11.2.0: resolution: {integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==} engines: {node: '>=14.14'} @@ -780,6 +989,10 @@ packages: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} dev: true + /functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + dev: true + /get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} @@ -794,6 +1007,17 @@ packages: hasown: 2.0.0 dev: true + /get-intrinsic@1.2.4: + resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + has-proto: 1.0.1 + has-symbols: 1.0.3 + hasown: 2.0.0 + dev: true + /gh-pages@6.1.0: resolution: {integrity: sha512-MdXigvqN3I66Y+tAZsQJMzpBWQOI1snD6BYuECmP+GEdryYMMOQvzn4AConk/+qNg/XIuQhB1xNGrl3Rmj1iow==} engines: {node: '>=10'} @@ -885,6 +1109,10 @@ packages: uglify-js: 3.17.4 dev: true + /has-bigints@1.0.2: + resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} + dev: true + /has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -896,6 +1124,12 @@ packages: get-intrinsic: 1.2.2 dev: true + /has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + dependencies: + es-define-property: 1.0.0 + dev: true + /has-proto@1.0.1: resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} engines: {node: '>= 0.4'} @@ -913,6 +1147,13 @@ packages: has-symbols: 1.0.3 dev: true + /has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: true + /hasown@2.0.0: resolution: {integrity: sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==} engines: {node: '>= 0.4'} @@ -939,6 +1180,18 @@ packages: engines: {node: '>= 0.10'} dev: true + /image-size@1.1.1: + resolution: {integrity: sha512-541xKlUw6jr/6gGuk92F+mYM5zaFAc5ahphvkqvNe2bQ6gVBkd6bfrmVJ2t4KDAfikAYZyIqTnktX3i6/aQDrQ==} + engines: {node: '>=16.x'} + hasBin: true + dependencies: + queue: 6.0.2 + dev: true + + /immutable@4.3.5: + resolution: {integrity: sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw==} + dev: true + /inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} dependencies: @@ -950,6 +1203,15 @@ packages: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} dev: true + /internal-slot@1.0.7: + resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + hasown: 2.0.0 + side-channel: 1.0.6 + dev: true + /is-alphabetical@1.0.4: resolution: {integrity: sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==} dev: true @@ -961,6 +1223,28 @@ packages: is-decimal: 1.0.4 dev: true + /is-arguments@1.1.1: + resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + has-tostringtag: 1.0.0 + dev: true + + /is-array-buffer@3.0.4: + resolution: {integrity: sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + get-intrinsic: 1.2.2 + dev: true + + /is-bigint@1.0.4: + resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} + dependencies: + has-bigints: 1.0.2 + dev: true + /is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} @@ -968,12 +1252,32 @@ packages: binary-extensions: 2.2.0 dev: true + /is-boolean-object@1.1.2: + resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + has-tostringtag: 1.0.0 + dev: true + + /is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + dev: true + /is-core-module@2.13.1: resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} dependencies: hasown: 2.0.0 dev: true + /is-date-object@1.0.5: + resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: true + /is-decimal@1.0.4: resolution: {integrity: sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==} dev: true @@ -1011,6 +1315,18 @@ packages: resolution: {integrity: sha512-6BEnpVn1rcf3ngfmViLM6vjUjGErbdrL4rwlv+u1NO1XO8kqT4YGL8+19Q+Z/bas8tY90BTWMk2+fW1g6hQjbA==} dev: true + /is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + dev: true + + /is-number-object@1.0.7: + resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: true + /is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} @@ -1033,11 +1349,54 @@ packages: has-tostringtag: 1.0.0 dev: true + /is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + dev: true + + /is-shared-array-buffer@1.0.3: + resolution: {integrity: sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + dev: true + + /is-string@1.0.7: + resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: true + + /is-symbol@1.0.4: + resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: true + /is-unicode-supported@0.1.0: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} dev: true + /is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + dev: true + + /is-weakset@2.0.3: + resolution: {integrity: sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + get-intrinsic: 1.2.4 + dev: true + + /isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + dev: true + /isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} dev: true @@ -1162,6 +1521,10 @@ packages: yallist: 4.0.0 dev: true + /lucide-static@0.294.0: + resolution: {integrity: sha512-iygXxjW9CYJBwUA5wR/jtLK70fIVOzaH7eGtUyWm4pS36DuctXu3D46pLofetA3xnu1VL6JnEIqyfBh13d0U3w==} + dev: true + /luxon@3.4.4: resolution: {integrity: sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==} engines: {node: '>=12'} @@ -1174,6 +1537,16 @@ packages: semver: 6.3.1 dev: true + /markdown-it-anchor@8.6.7(@types/markdown-it@14.1.1)(markdown-it@13.0.2): + resolution: {integrity: sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==} + peerDependencies: + '@types/markdown-it': '*' + markdown-it: '*' + dependencies: + '@types/markdown-it': 14.1.1 + markdown-it: 13.0.2 + dev: true + /markdown-it@13.0.2: resolution: {integrity: sha512-FtwnEuuK+2yVU7goGn/MJ0WBZMM9ZPgU9spqlFs7/A/pDIUNSOQZhUgOqYCficIuR2QaFnrt8LHqBWsbTAoI5w==} hasBin: true @@ -1313,6 +1686,11 @@ packages: hasBin: true dev: true + /nano-jsx@0.1.0: + resolution: {integrity: sha512-S4qJM9ayruMdDnn3hiHNK6kq0ZvCaNrDL3RD5jc4AVhmsW1Ufk3xE64Q6xrjAzq1Gff+6VZ5+Au8For4FT/6LA==} + engines: {node: '>=16'} + dev: true + /nanoid@3.3.3: resolution: {integrity: sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -1349,6 +1727,33 @@ packages: engines: {node: '>=0.10.0'} dev: true + /object-inspect@1.13.1: + resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} + dev: true + + /object-is@1.1.6: + resolution: {integrity: sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + dev: true + + /object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + dev: true + + /object.assign@4.1.5: + resolution: {integrity: sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + has-symbols: 1.0.3 + object-keys: 1.1.1 + dev: true + /on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -1395,6 +1800,10 @@ packages: engines: {node: '>=6'} dev: true + /pako@0.2.9: + resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} + dev: true + /parse-srcset@1.0.2: resolution: {integrity: sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==} dev: true @@ -1462,6 +1871,11 @@ packages: semver-compare: 1.0.0 dev: true + /possible-typed-array-names@1.0.0: + resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} + engines: {node: '>= 0.4'} + dev: true + /posthtml-parser@0.11.0: resolution: {integrity: sha512-QecJtfLekJbWVo/dMAA+OSwY79wpRmbqS5TeXvXSX+f0c6pW4/SE6inzZ2qkU7oAMCPqIDkZDvd/bQsSFUnKyw==} engines: {node: '>=12'} @@ -1610,6 +2024,12 @@ packages: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} dev: true + /queue@6.0.2: + resolution: {integrity: sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==} + dependencies: + inherits: 2.0.4 + dev: true + /randombytes@2.1.0: resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} dependencies: @@ -1637,6 +2057,16 @@ packages: slash: 1.0.0 dev: true + /regexp.prototype.flags@1.5.2: + resolution: {integrity: sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-errors: 1.3.0 + set-function-name: 2.0.2 + dev: true + /remove-markdown@0.5.0: resolution: {integrity: sha512-x917M80K97K5IN1L8lUvFehsfhR8cYjGQ/yAMRI9E7JIKivtl5Emo5iD13DhMr+VojzMCiYk8V2byNPwT/oapg==} dev: true @@ -1655,6 +2085,10 @@ packages: supports-preserve-symlinks-flag: 1.0.0 dev: true + /restructure@2.0.1: + resolution: {integrity: sha512-e0dOpjm5DseomnXx2M5lpdZ5zoHqF1+bqdMJUohoYVVQa7cBdnk7fdmeI6byNWP/kiME72EeTiSypTCVnpLiDg==} + dev: true + /reusify@1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -1677,6 +2111,20 @@ packages: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} dev: true + /sass@1.76.0: + resolution: {integrity: sha512-nc3LeqvF2FNW5xGF1zxZifdW3ffIz5aBb7I7tSvOoNu7z1RQ6pFt9MBuiPtjgaI62YWrM/txjWlOCFiGtf2xpw==} + engines: {node: '>=14.0.0'} + hasBin: true + dependencies: + chokidar: 3.5.3 + immutable: 4.3.5 + source-map-js: 1.2.0 + dev: true + + /sax@1.3.0: + resolution: {integrity: sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==} + dev: true + /section-matter@1.0.0: resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} engines: {node: '>=4'} @@ -1718,6 +2166,28 @@ packages: has-property-descriptors: 1.0.1 dev: true + /set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + gopd: 1.0.1 + has-property-descriptors: 1.0.2 + dev: true + + /set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + dev: true + /shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -1730,6 +2200,21 @@ packages: engines: {node: '>=8'} dev: true + /side-channel@1.0.6: + resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + object-inspect: 1.13.1 + dev: true + + /simple-icons@10.4.0: + resolution: {integrity: sha512-XBoU1ljCsWjw59IVkaQ1nKc0PiaDAAKNFVx59ueC0tBy4WY/I4Q040sGj6ok2cZRLT8zBzL1HaTubi8MRqmojQ==} + engines: {node: '>=0.12.18'} + dev: true + /slash@1.0.0: resolution: {integrity: sha512-3TYDR7xWt4dIqV2JauJr+EJeW356RXijHeUlO+8djJ+uBXPn8/2dpzBc8yQhh583sVvc9CvFAeQVgijsH+PNNg==} engines: {node: '>=0.10.0'} @@ -1740,6 +2225,11 @@ packages: engines: {node: '>=8.0.0'} dev: true + /source-map-js@1.2.0: + resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} + engines: {node: '>=0.10.0'} + dev: true + /source-map@0.6.1: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} @@ -1761,6 +2251,13 @@ packages: engines: {node: '>= 0.8'} dev: true + /stop-iteration-iterator@1.0.0: + resolution: {integrity: sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==} + engines: {node: '>= 0.4'} + dependencies: + internal-slot: 1.0.7 + dev: true + /string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -1813,6 +2310,18 @@ packages: engines: {node: '>= 0.4'} dev: true + /svgdom@0.1.13: + resolution: {integrity: sha512-qcpPCOAtbQjG5ZuY28kN3F03+slpFwN5n0w3dsMCEpXu8sPhb+wuWBwHQHs7Ayvh4Ou0QIE6WH7mq/viZHdcxg==} + dependencies: + fontkit: 1.9.0 + image-size: 1.1.1 + sax: 1.3.0 + dev: true + + /tiny-inflate@1.0.3: + resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} + dev: true + /to-fast-properties@2.0.0: resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} engines: {node: '>=4'} @@ -1836,6 +2345,10 @@ packages: escape-string-regexp: 1.0.5 dev: true + /tslib@2.6.2: + resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + dev: true + /uc.micro@1.0.6: resolution: {integrity: sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==} dev: true @@ -1848,6 +2361,20 @@ packages: dev: true optional: true + /unicode-properties@1.4.1: + resolution: {integrity: sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==} + dependencies: + base64-js: 1.5.1 + unicode-trie: 2.0.0 + dev: true + + /unicode-trie@2.0.0: + resolution: {integrity: sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==} + dependencies: + pako: 0.2.9 + tiny-inflate: 1.0.3 + dev: true + /universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} @@ -1863,6 +2390,37 @@ packages: engines: {node: '>=0.10.0'} dev: true + /which-boxed-primitive@1.0.2: + resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} + dependencies: + is-bigint: 1.0.4 + is-boolean-object: 1.1.2 + is-number-object: 1.0.7 + is-string: 1.0.7 + is-symbol: 1.0.4 + dev: true + + /which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.3 + dev: true + + /which-typed-array@1.1.15: + resolution: {integrity: sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.2 + dev: true + /which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} diff --git a/tests/Thoth.Json.Tests/Encoders.fs b/tests/Thoth.Json.Tests/Encoders.fs index 08895c2..af9403e 100644 --- a/tests/Thoth.Json.Tests/Encoders.fs +++ b/tests/Thoth.Json.Tests/Encoders.fs @@ -72,6 +72,17 @@ let tests (runner: TestRunner<_, _>) = runner.equal actual expected + runner.testCase + "a string with non ascii characters works returns the characters as is" + <| fun _ -> + let expected = "\"Timo Mühlhaus\"" + + let actual = + Encode.string "Timo Mühlhaus" + |> runner.Encode.toString 0 + + runner.equal actual expected + runner.testCase "a char works" <| fun _ -> let expected = "\"a\"" @@ -85,6 +96,12 @@ let tests (runner: TestRunner<_, _>) = let actual = Encode.int 1 |> runner.Encode.toString 0 runner.equal actual expected + runner.testCase "negative int keeps the sign" + <| fun _ -> + let expected = "-1" + let actual = Encode.int -1 |> runner.Encode.toString 0 + runner.equal actual expected + runner.testCase "a float works" <| fun _ -> let expected = "1.2" @@ -295,6 +312,15 @@ let tests (runner: TestRunner<_, _>) = runner.equal actual expected + runner.testCase "negative sbyte keeps the sign" + <| fun _ -> + let expected = "-99" + + let actual = + -99y |> Encode.sbyte |> runner.Encode.toString 0 + + runner.equal actual expected + runner.testCase "an int16 works" <| fun _ -> let expected = "99" @@ -304,6 +330,15 @@ let tests (runner: TestRunner<_, _>) = runner.equal actual expected + runner.testCase "negative int16 keeps the sign" + <| fun _ -> + let expected = "-99" + + let actual = + -99s |> Encode.int16 |> runner.Encode.toString 0 + + runner.equal actual expected + runner.testCase "an uint16 works" <| fun _ -> let expected = "99" diff --git a/tests/Thoth.Json.Tests/Util.fs b/tests/Thoth.Json.Tests/Util.fs index 489fe53..b53d116 100644 --- a/tests/Thoth.Json.Tests/Util.fs +++ b/tests/Thoth.Json.Tests/Util.fs @@ -25,7 +25,7 @@ open Thoth.Json.Core // abstract type IEncode = - abstract toString: int -> Json -> string + abstract toString: int -> IEncodable -> string type IDecode = abstract fromString<'T> : Decoder<'T> -> string -> Result<'T, string> From 26d8c8acc9f597cca7b6581b1f61f0ccbff338e0 Mon Sep 17 00:00:00 2001 From: njlr Date: Sat, 4 May 2024 18:02:51 +0100 Subject: [PATCH 05/13] chore: Switch to IEncodable --- packages/Thoth.Json.Auto/Encode.fs | 24 +++++++++++++----------- packages/Thoth.Json.Core/Encode.fs | 3 +++ 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/packages/Thoth.Json.Auto/Encode.fs b/packages/Thoth.Json.Auto/Encode.fs index 7768f19..447893b 100644 --- a/packages/Thoth.Json.Auto/Encode.fs +++ b/packages/Thoth.Json.Auto/Encode.fs @@ -417,7 +417,7 @@ module Encode = #if !FABLE_COMPILER let private makeEncoderType (ty: Type) : Type = - FSharpType.MakeFunctionType(ty, typeof) + FSharpType.MakeFunctionType(ty, typeof) // typedefof>.MakeGenericType(ty) #endif @@ -602,7 +602,7 @@ module Encode = | _ -> failwith $"Expected an F# record type" for pi in recordFields do - let fieldEncoder: obj -> Json = + let fieldEncoder: obj -> IEncodable = generateEncoder caseStyle existingEncoders @@ -691,7 +691,8 @@ module Encode = let invokeMethodInfo = fieldEncoder.GetType().GetMethods() |> Array.find (fun x -> - x.Name = "Invoke" && x.ReturnType = typedefof + x.Name = "Invoke" + && x.ReturnType = typedefof ) let reader = FSharpValue.PreComputeRecordFieldReader(pi) @@ -703,7 +704,7 @@ module Encode = None else invokeMethodInfo.Invoke(fieldEncoder, [| value |]) - :?> Json + :?> IEncodable |> Some pi.Name, readAndEncode @@ -789,7 +790,7 @@ module Encode = let fieldEncoders = [| for pi in unionCase.GetFields() do - let encoder: obj -> Json = + let encoder: obj -> IEncodable = generateEncoder caseStyle existingEncoders @@ -812,7 +813,7 @@ module Encode = for i = 0 to n do let value = values[i] - let encoder: obj -> Json = + let encoder: obj -> IEncodable = unbox fieldEncoders[i] encoder value @@ -917,7 +918,7 @@ module Encode = encoder.GetType().GetMethods() |> Array.find (fun x -> x.Name = "Invoke" - && x.ReturnType = typeof + && x.ReturnType = typeof ) fun o -> @@ -925,7 +926,7 @@ module Encode = encoder, [| o |] ) - :?> Json + :?> IEncodable |] let n = Array.length fieldEncoders - 1 @@ -990,7 +991,7 @@ module Encode = [| for i = 0 to Array.length encoders - 1 do let value = unbox values[i] - let encode: obj -> Json = unbox encoders[i] + let encode: obj -> IEncodable = unbox encoders[i] encode value |] @@ -1014,12 +1015,13 @@ module Encode = let invokeMethodInfo = elementEncoder.GetType().GetMethods() |> Array.find (fun x -> - x.Name = "Invoke" && x.ReturnType = typedefof + x.Name = "Invoke" + && x.ReturnType = typedefof ) let encode (value: obj) = invokeMethodInfo.Invoke(elementEncoder, [| value |]) - :?> Json + :?> IEncodable encode |] diff --git a/packages/Thoth.Json.Core/Encode.fs b/packages/Thoth.Json.Core/Encode.fs index 3b27e5f..4c4bb02 100644 --- a/packages/Thoth.Json.Core/Encode.fs +++ b/packages/Thoth.Json.Core/Encode.fs @@ -261,6 +261,9 @@ module Encode = |> List.map (tuple2 keyEncoder valueEncoder) |> list + let lazily<'t> (enc: Lazy>) : Encoder<'t> = + fun (x: 't) -> enc.Value x + //////////// /// Enum /// /////////// From 2db87ce3b078451041957a5e77cb94bbb03f52ab Mon Sep 17 00:00:00 2001 From: njlr Date: Mon, 5 Aug 2024 18:47:07 +0100 Subject: [PATCH 06/13] chore: Update package locks --- tests/Thoth.Json.Tests.JavaScript/packages.lock.json | 8 ++++++++ tests/Thoth.Json.Tests.Newtonsoft/packages.lock.json | 8 ++++++++ tests/Thoth.Json.Tests.Python/packages.lock.json | 8 ++++++++ tests/Thoth.Json.Tests/packages.lock.json | 7 +++++++ 4 files changed, 31 insertions(+) diff --git a/tests/Thoth.Json.Tests.JavaScript/packages.lock.json b/tests/Thoth.Json.Tests.JavaScript/packages.lock.json index 0c27e7a..0fb721f 100644 --- a/tests/Thoth.Json.Tests.JavaScript/packages.lock.json +++ b/tests/Thoth.Json.Tests.JavaScript/packages.lock.json @@ -96,6 +96,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": { @@ -116,6 +123,7 @@ "dependencies": { "FSharp.Core": "[5.0.0, )", "Fable.Core": "[4.1.0, )", + "Thoth.Json.Auto": "[1.0.0, )", "Thoth.Json.Core": "[1.0.0, )" } } diff --git a/tests/Thoth.Json.Tests.Newtonsoft/packages.lock.json b/tests/Thoth.Json.Tests.Newtonsoft/packages.lock.json index 5039532..f49b077 100644 --- a/tests/Thoth.Json.Tests.Newtonsoft/packages.lock.json +++ b/tests/Thoth.Json.Tests.Newtonsoft/packages.lock.json @@ -81,6 +81,13 @@ "resolved": "0.11.3", "contentHash": "DNYE+io5XfEE8+E+5padThTPHJARJHbz1mhbhMPNrrWGKVKKqj/KEeLvbawAmbIcT73NuxLV7itHZaYCZcVWGg==" }, + "thoth.json.auto": { + "type": "Project", + "dependencies": { + "FSharp.Core": "[5.0.0, )", + "Thoth.Json.Core": "[1.0.0, )" + } + }, "thoth.json.core": { "type": "Project", "dependencies": { @@ -102,6 +109,7 @@ "dependencies": { "FSharp.Core": "[5.0.0, )", "Fable.Core": "[4.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 3e4994f..4423443 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": { @@ -104,6 +111,7 @@ "dependencies": { "FSharp.Core": "[5.0.0, )", "Fable.Core": "[4.1.0, )", + "Thoth.Json.Auto": "[1.0.0, )", "Thoth.Json.Core": "[1.0.0, )" } }, diff --git a/tests/Thoth.Json.Tests/packages.lock.json b/tests/Thoth.Json.Tests/packages.lock.json index 19f0875..07a977f 100644 --- a/tests/Thoth.Json.Tests/packages.lock.json +++ b/tests/Thoth.Json.Tests/packages.lock.json @@ -86,6 +86,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": { From 3f0478312484ec0fdb64da980aa70be0c169c82e Mon Sep 17 00:00:00 2001 From: njlr Date: Tue, 6 Aug 2024 18:26:34 +0100 Subject: [PATCH 07/13] chore: Fix tests --- README.md | 2 ++ flake.lock | 25 +++++++++++++++++ flake.nix | 34 +++++++++++++++++++++++ tests/Thoth.Json.Tests.Legacy/Decoders.fs | 8 +++--- tests/Thoth.Json.Tests/Decoders.fs | 10 +++---- 5 files changed, 70 insertions(+), 9 deletions(-) create mode 100644 flake.lock create mode 100644 flake.nix 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/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/tests/Thoth.Json.Tests.Legacy/Decoders.fs b/tests/Thoth.Json.Tests.Legacy/Decoders.fs index a4433c4..7e45005 100644 --- a/tests/Thoth.Json.Tests.Legacy/Decoders.fs +++ b/tests/Thoth.Json.Tests.Legacy/Decoders.fs @@ -78,7 +78,7 @@ let tests: Test = #if FABLE_COMPILER let expected: Result = Error - "Given an invalid JSON: Unexpected token m in JSON at position 0" + "Given an invalid JSON: Unexpected token 'm', \"maxime\" is not valid JSON" #else let expected: Result = Error @@ -94,7 +94,7 @@ let tests: Test = #if FABLE_COMPILER let expected: Result = Error - "Given an invalid JSON: Unexpected token , in JSON at position 5" + "Given an invalid JSON: Unexpected non-whitespace character after JSON at position 5" #else let expected: Result = Error @@ -111,7 +111,7 @@ let tests: Test = #if FABLE_COMPILER let expected: Result = Error - "Given an invalid JSON: Unexpected end of JSON input" + "Given an invalid JSON: Expected double-quoted property name in JSON at position 172" #else let expected: Result = Error @@ -2196,7 +2196,7 @@ Expecting an object with a field named `height` but instead got: #if FABLE_COMPILER let expected = Error( - "Given an invalid JSON: Unexpected token m in JSON at position 0" + "Given an invalid JSON: Unexpected token 'm', \"maxime\" is not valid JSON" ) #else let expected = diff --git a/tests/Thoth.Json.Tests/Decoders.fs b/tests/Thoth.Json.Tests/Decoders.fs index e5cb034..14c3596 100644 --- a/tests/Thoth.Json.Tests/Decoders.fs +++ b/tests/Thoth.Json.Tests/Decoders.fs @@ -61,7 +61,7 @@ let tests (runner: TestRunner<_>) = #if FABLE_COMPILER_JAVASCRIPT let expected: Result = Error - "Given an invalid JSON: Unexpected token m in JSON at position 0" + "Given an invalid JSON: Unexpected token 'm', \"maxime\" is not valid JSON" #endif #if FABLE_COMPILER_PYTHON @@ -92,7 +92,7 @@ let tests (runner: TestRunner<_>) = #if FABLE_COMPILER let expected: Result = Error - "Given an invalid JSON: Unexpected token , in JSON at position 5" + "Given an invalid JSON: Unexpected non-whitespace character after JSON at position 5" #else let expected: Result = Error @@ -113,7 +113,7 @@ let tests (runner: TestRunner<_>) = #if FABLE_COMPILER_JAVASCRIPT let expected: Result = Error - "Given an invalid JSON: Unexpected end of JSON input" + "Given an invalid JSON: Expected double-quoted property name in JSON at position 172" #endif #if FABLE_COMPILER_PYTHON @@ -223,7 +223,7 @@ let tests (runner: TestRunner<_>) = <| fun _ -> #if FABLE_COMPILER_JAVASCRIPT let expected = - "Given an invalid JSON: Unexpected token m in JSON at position 0" + "Given an invalid JSON: Unexpected token 'm', \"maxime\" is not valid JSON" #endif #if FABLE_COMPILER_PYTHON @@ -2443,7 +2443,7 @@ Expecting an object with a field named `height` but instead got: #if FABLE_COMPILER_JAVASCRIPT let expected = Error( - "Given an invalid JSON: Unexpected token m in JSON at position 0" + "Given an invalid JSON: Unexpected token 'm', \"maxime\" is not valid JSON" ) #endif From 2996c6028a03bacf166253f3c9d2e1e122552616 Mon Sep 17 00:00:00 2001 From: njlr Date: Tue, 6 Aug 2024 18:30:12 +0100 Subject: [PATCH 08/13] chore: Update lock --- packages/Thoth.Json.Auto/packages.lock.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/Thoth.Json.Auto/packages.lock.json b/packages/Thoth.Json.Auto/packages.lock.json index 45b7650..de129a1 100644 --- a/packages/Thoth.Json.Auto/packages.lock.json +++ b/packages/Thoth.Json.Auto/packages.lock.json @@ -84,7 +84,8 @@ "type": "Project", "dependencies": { "FSharp.Core": "[5.0.0, )", - "Fable.Core": "[4.1.0, )" + "Fable.Core": "[4.1.0, )", + "Fable.Package.SDK": "[0.1.0, )" } }, "Fable.Core": { @@ -92,6 +93,12 @@ "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==" } } } From 2d8ebedc9b3aa1407eed079b930de30b6db9f11e Mon Sep 17 00:00:00 2001 From: njlr Date: Tue, 6 Aug 2024 18:33:33 +0100 Subject: [PATCH 09/13] Update CHANGELOG.md --- packages/Thoth.Json.JavaScript/CHANGELOG.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/Thoth.Json.JavaScript/CHANGELOG.md b/packages/Thoth.Json.JavaScript/CHANGELOG.md index 3ec411c..55cc8cc 100644 --- a/packages/Thoth.Json.JavaScript/CHANGELOG.md +++ b/packages/Thoth.Json.JavaScript/CHANGELOG.md @@ -12,10 +12,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Rework encoder API to not need a custom DU ([GH-188](https://github.com/thoth-org/Thoth.Json/pull/188/)) * Rework object encoder helper to support JSON backends not allowing mutating a JSON object -### Fixed - -* Encoding negative integers should keep their sign ([GH-187](https://github.com/thoth-org/Thoth.Json/issues/187)) - ## 0.2.0 - 2024-06-15 ### Changed From 25a7e29e0da5b1d8ed15cff8276f51615a695dac Mon Sep 17 00:00:00 2001 From: njlr Date: Tue, 6 Aug 2024 18:40:36 +0100 Subject: [PATCH 10/13] chore: Fix changelog --- packages/Thoth.Json.Python/CHANGELOG.md | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/packages/Thoth.Json.Python/CHANGELOG.md b/packages/Thoth.Json.Python/CHANGELOG.md index d09a0af..7f95693 100644 --- a/packages/Thoth.Json.Python/CHANGELOG.md +++ b/packages/Thoth.Json.Python/CHANGELOG.md @@ -7,18 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased -### Changed - -* Rework encoder API to not need a custom DU ([GH-188](https://github.com/thoth-org/Thoth.Json/pull/188/)) -* Rework object encoder helper to support JSON backends not allowing mutating a JSON object - -### Fixed - -* Encoding negative integers should keep their sign ([GH-187](https://github.com/thoth-org/Thoth.Json/issues/187)) -* Emit non ascii characters as is when encoding to JSON (JSON specs advice to use utf-8 string and not limit to ascii) (align non compacted JSON behaviour with compacted JSON) - -## 0.2.0 - 2024-04-03 - ## 0.3.0 - 2024-06-15 ### Changed From 4631620a634606a709013ef6130e0fd92e19d709 Mon Sep 17 00:00:00 2001 From: njlr Date: Tue, 6 Aug 2024 18:41:16 +0100 Subject: [PATCH 11/13] chore: Fix changelog --- packages/Thoth.Json.Newtonsoft/CHANGELOG.md | 9 --------- 1 file changed, 9 deletions(-) diff --git a/packages/Thoth.Json.Newtonsoft/CHANGELOG.md b/packages/Thoth.Json.Newtonsoft/CHANGELOG.md index 3ec411c..fb8d5cb 100644 --- a/packages/Thoth.Json.Newtonsoft/CHANGELOG.md +++ b/packages/Thoth.Json.Newtonsoft/CHANGELOG.md @@ -7,15 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased -### Changed - -* Rework encoder API to not need a custom DU ([GH-188](https://github.com/thoth-org/Thoth.Json/pull/188/)) -* Rework object encoder helper to support JSON backends not allowing mutating a JSON object - -### Fixed - -* Encoding negative integers should keep their sign ([GH-187](https://github.com/thoth-org/Thoth.Json/issues/187)) - ## 0.2.0 - 2024-06-15 ### Changed From 2bcbbdc2776b13ce736fa4b8ab914bbe240f823e Mon Sep 17 00:00:00 2001 From: njlr Date: Tue, 6 Aug 2024 18:41:40 +0100 Subject: [PATCH 12/13] chore: Fix changelog --- packages/Thoth.Json.JavaScript/CHANGELOG.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/Thoth.Json.JavaScript/CHANGELOG.md b/packages/Thoth.Json.JavaScript/CHANGELOG.md index 55cc8cc..fb8d5cb 100644 --- a/packages/Thoth.Json.JavaScript/CHANGELOG.md +++ b/packages/Thoth.Json.JavaScript/CHANGELOG.md @@ -7,11 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased -### Changed - -* Rework encoder API to not need a custom DU ([GH-188](https://github.com/thoth-org/Thoth.Json/pull/188/)) -* Rework object encoder helper to support JSON backends not allowing mutating a JSON object - ## 0.2.0 - 2024-06-15 ### Changed From 9c3bdf04ad64603f47f9e1f4519d7752ba861860 Mon Sep 17 00:00:00 2001 From: njlr Date: Tue, 6 Aug 2024 18:42:28 +0100 Subject: [PATCH 13/13] chore: Fix changelog --- packages/Thoth.Json.Core/CHANGELOG.md | 9 --------- 1 file changed, 9 deletions(-) diff --git a/packages/Thoth.Json.Core/CHANGELOG.md b/packages/Thoth.Json.Core/CHANGELOG.md index 15b94d2..3d15d41 100644 --- a/packages/Thoth.Json.Core/CHANGELOG.md +++ b/packages/Thoth.Json.Core/CHANGELOG.md @@ -7,15 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased -### Changed - -* Rework encoder API to not need a custom DU ([GH-188](https://github.com/thoth-org/Thoth.Json/pull/188/)) -* Rework object encoder helper to support JSON backends not allowing mutating a JSON object - -### Fixed - -* Encoding negative integers should keep their sign ([GH-187](https://github.com/thoth-org/Thoth.Json/issues/187)) - ## 0.3.0 - 2024-06-15 ### Added