From 398cd04a2a73e35bb1f40a4cc12db9dacbf8e713 Mon Sep 17 00:00:00 2001 From: Patrick Stevens <3138005+Smaug123@users.noreply.github.com> Date: Mon, 1 Jul 2024 18:08:09 +0100 Subject: [PATCH] Support DateTimeOffset in JSON generators (#179) --- ConsumePlugin/GeneratedSerde.fs | 38 ++++++++++ .../SerializationAndDeserialization.fs | 2 + .../TestJsonSerialize/TestJsonSerde.fs | 76 +++++++++++++++++++ WoofWare.Myriad.Plugins/JsonParseGenerator.fs | 4 + .../JsonSerializeGenerator.fs | 11 +++ WoofWare.Myriad.Plugins/SynExpr/SynType.fs | 9 +++ 6 files changed, 140 insertions(+) diff --git a/ConsumePlugin/GeneratedSerde.fs b/ConsumePlugin/GeneratedSerde.fs index a2c3ff2..3c57fb7 100644 --- a/ConsumePlugin/GeneratedSerde.fs +++ b/ConsumePlugin/GeneratedSerde.fs @@ -204,6 +204,12 @@ module JsonRecordTypeWithBothJsonSerializeExtension = node.Add ("enum", (input.Enum |> SomeEnum.toJsonNode)) + node.Add ( + "timestamp", + (input.Timestamp + |> (fun field -> field.ToString "o" |> System.Text.Json.Nodes.JsonValue.Create)) + ) + node :> _ namespace ConsumePlugin @@ -418,6 +424,19 @@ module JsonRecordTypeWithBothJsonParseExtension = /// Parse from a JSON node. static member jsonParse (node : System.Text.Json.Nodes.JsonNode) : JsonRecordTypeWithBoth = + let arg_20 = + (match node.["timestamp"] with + | null -> + raise ( + System.Collections.Generic.KeyNotFoundException ( + sprintf "Required key '%s' not found on JSON object" ("timestamp") + ) + ) + | v -> v) + .AsValue() + .GetValue () + |> System.DateTimeOffset.Parse + let arg_19 = SomeEnum.jsonParse ( match node.["enum"] with @@ -685,6 +704,7 @@ module JsonRecordTypeWithBothJsonParseExtension = IntMeasureOption = arg_17 IntMeasureNullable = arg_18 Enum = arg_19 + Timestamp = arg_20 } namespace ConsumePlugin @@ -804,3 +824,21 @@ module HeaderAndValueJsonParseExtension = Header = arg_0 Value = arg_1 } +namespace ConsumePlugin + +/// Module containing JSON parsing extension members for the Foo type +[] +module FooJsonParseExtension = + /// Extension methods for JSON parsing + type Foo with + + /// Parse from a JSON node. + static member jsonParse (node : System.Text.Json.Nodes.JsonNode) : Foo = + let arg_0 = + match node.["message"] with + | null -> None + | v -> HeaderAndValue.jsonParse v |> Some + + { + Message = arg_0 + } diff --git a/ConsumePlugin/SerializationAndDeserialization.fs b/ConsumePlugin/SerializationAndDeserialization.fs index 720d1a1..96e6fee 100644 --- a/ConsumePlugin/SerializationAndDeserialization.fs +++ b/ConsumePlugin/SerializationAndDeserialization.fs @@ -49,6 +49,7 @@ type JsonRecordTypeWithBoth = IntMeasureOption : int option IntMeasureNullable : int Nullable Enum : SomeEnum + Timestamp : DateTimeOffset } [] @@ -67,6 +68,7 @@ type HeaderAndValue = } [] +[] type Foo = { Message : HeaderAndValue option diff --git a/WoofWare.Myriad.Plugins.Test/TestJsonSerialize/TestJsonSerde.fs b/WoofWare.Myriad.Plugins.Test/TestJsonSerialize/TestJsonSerde.fs index a58ccdc..d67607d 100644 --- a/WoofWare.Myriad.Plugins.Test/TestJsonSerialize/TestJsonSerde.fs +++ b/WoofWare.Myriad.Plugins.Test/TestJsonSerialize/TestJsonSerde.fs @@ -92,6 +92,7 @@ module TestJsonSerde = let! intMeasureOption = Arb.generate let! intMeasureNullable = Arb.generate let! someEnum = Gen.choose (0, 1) + let! timestamp = Arb.generate return { @@ -115,6 +116,7 @@ module TestJsonSerde = IntMeasureOption = intMeasureOption IntMeasureNullable = intMeasureNullable Enum = enum someEnum + Timestamp = timestamp } } @@ -132,6 +134,80 @@ module TestJsonSerde = property |> Prop.forAll (Arb.fromGen outerGen) |> Check.QuickThrowOnFailure + [] + let ``Single example of big record`` () = + let guid = Guid.Parse "dfe24db5-9f8d-447b-8463-4c0bcf1166d5" + + let data = + { + A = 3 + B = "hello!" + C = [ 1 ; -9 ] + D = + { + Thing = guid + Map = Map.ofList [] + ReadOnlyDict = readOnlyDict [] + Dict = dict [] + ConcreteDict = Dictionary () + } + E = [| "I'm-a-string" |] + Arr = [| -18883 ; 9100 |] + Byte = 87uy + Sbyte = 89y + I = 199993345 + I32 = -485832 + I64 = -13458625689L + U = 458582u + U32 = 857362147u + U64 = 1234567892123414596UL + F = 8833345667.1 + F32 = 1000.98f + Single = 0.334f + IntMeasureOption = Some 981 + IntMeasureNullable = Nullable -883 + Enum = enum 1 + Timestamp = DateTimeOffset (2024, 07, 01, 17, 54, 00, TimeSpan.FromHours 1.0) + } + + let expected = + """{ + "a": 3, + "b": "hello!", + "c": [1, -9], + "d": { + "it\u0027s-a-me": "dfe24db5-9f8d-447b-8463-4c0bcf1166d5", + "map": {}, + "readOnlyDict": {}, + "dict": {}, + "concreteDict": {} + }, + "e": ["I\u0027m-a-string"], + "arr": [-18883, 9100], + "byte": 87, + "sbyte": 89, + "i": 199993345, + "i32": -485832, + "i64": -13458625689, + "u": 458582, + "u32": 857362147, + "u64": 1234567892123414596, + "f": 8833345667.1, + "f32": 1000.98, + "single": 0.334, + "intMeasureOption": 981, + "intMeasureNullable": -883, + "enum": 1, + "timestamp": "2024-07-01T17:54:00.0000000\u002B01:00" +} +""" + |> fun s -> s.ToCharArray () + |> Array.filter (fun c -> not (Char.IsWhiteSpace c)) + |> fun s -> new String (s) + + JsonRecordTypeWithBoth.toJsonNode(data).ToJsonString () |> shouldEqual expected + JsonRecordTypeWithBoth.jsonParse (JsonNode.Parse expected) |> shouldEqual data + [] let ``Guids are treated just like strings`` () = let guidStr = "b1e7496e-6e79-4158-8579-a01de355d3b2" diff --git a/WoofWare.Myriad.Plugins/JsonParseGenerator.fs b/WoofWare.Myriad.Plugins/JsonParseGenerator.fs index cf92eaf..4181af9 100644 --- a/WoofWare.Myriad.Plugins/JsonParseGenerator.fs +++ b/WoofWare.Myriad.Plugins/JsonParseGenerator.fs @@ -192,6 +192,10 @@ module internal JsonParseGenerator = node |> asValueGetValue propertyName "string" |> SynExpr.pipeThroughFunction (SynExpr.createLongIdent [ "System" ; "DateTime" ; "Parse" ]) + | DateTimeOffset -> + node + |> asValueGetValue propertyName "string" + |> SynExpr.pipeThroughFunction (SynExpr.createLongIdent [ "System" ; "DateTimeOffset" ; "Parse" ]) | NumberType typeName -> parseNumberType options propertyName node typeName | PrimitiveType typeName -> asValueGetValueIdent propertyName typeName node | OptionType ty -> diff --git a/WoofWare.Myriad.Plugins/JsonSerializeGenerator.fs b/WoofWare.Myriad.Plugins/JsonSerializeGenerator.fs index e395066..32a4ca6 100644 --- a/WoofWare.Myriad.Plugins/JsonSerializeGenerator.fs +++ b/WoofWare.Myriad.Plugins/JsonSerializeGenerator.fs @@ -38,6 +38,17 @@ module internal JsonSerializeGenerator = SynExpr.createLongIdent [ "System" ; "Text" ; "Json" ; "Nodes" ; "JsonValue" ; "Create" ] |> SynExpr.typeApp [ fieldType ] |> fun e -> e, false + | DateTimeOffset -> + // fun field -> field.ToString("o") |> JsonValue.Create + let create = + SynExpr.createLongIdent [ "System" ; "Text" ; "Json" ; "Nodes" ; "JsonValue" ; "Create" ] + |> SynExpr.typeApp [ SynType.named "string" ] + + SynExpr.createIdent "field" + |> SynExpr.callMethodArg "ToString" (SynExpr.CreateConst "o") + |> SynExpr.pipeThroughFunction create + |> SynExpr.createLambda "field" + |> fun e -> e, false | NullableType ty -> // fun field -> if field.HasValue then {serializeNode ty} field.Value else JsonValue.Create null let inner, innerIsJsonNode = serializeNode ty diff --git a/WoofWare.Myriad.Plugins/SynExpr/SynType.fs b/WoofWare.Myriad.Plugins/SynExpr/SynType.fs index 5994406..07536a9 100644 --- a/WoofWare.Myriad.Plugins/SynExpr/SynType.fs +++ b/WoofWare.Myriad.Plugins/SynExpr/SynType.fs @@ -241,6 +241,15 @@ module internal SynTypePatterns = | _ -> None | _ -> None + let (|DateTimeOffset|_|) (fieldType : SynType) = + match fieldType with + | SynType.LongIdent (SynLongIdent.SynLongIdent (ident, _, _)) -> + match ident |> List.map (fun i -> i.idText) with + | [ "System" ; "DateTimeOffset" ] + | [ "DateTimeOffset" ] -> Some () + | _ -> None + | _ -> None + let (|Uri|_|) (fieldType : SynType) = match fieldType with | SynType.LongIdent (SynLongIdent.SynLongIdent (ident, _, _)) ->