From c4669978cac019cba8c90db7dd82f09797a738c3 Mon Sep 17 00:00:00 2001 From: "Valber M. Silva de Souza" Date: Tue, 7 Feb 2023 01:25:44 +0100 Subject: [PATCH] custom json converters --- graphql-ws.fsharp.sln | 9 ++++ src/Main/GraphQLWebsocketMiddleware.fs | 8 +-- src/Main/JsonConverters.fs | 72 ++++++++++++++++++++++++++ src/Main/Main.fsproj | 1 + src/Main/WebSocketMessages.fs | 7 ++- tests/unit-tests/Main.UnitTests.fsproj | 32 ++++++++++++ tests/unit-tests/Program.fs | 1 + tests/unit-tests/Tests.fs | 30 +++++++++++ 8 files changed, 155 insertions(+), 5 deletions(-) create mode 100644 src/Main/JsonConverters.fs create mode 100644 tests/unit-tests/Main.UnitTests.fsproj create mode 100644 tests/unit-tests/Program.fs create mode 100644 tests/unit-tests/Tests.fs diff --git a/graphql-ws.fsharp.sln b/graphql-ws.fsharp.sln index ed38b3a..18819b5 100644 --- a/graphql-ws.fsharp.sln +++ b/graphql-ws.fsharp.sln @@ -7,6 +7,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{2BBAEFA0-4E9 EndProject Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Main", "src\Main\Main.fsproj", "{72471B86-58DF-42DC-8220-4E9E658AE781}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{E2ECB101-4BA2-4884-9AE2-012BF8F8F433}" +EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Main.UnitTests", "tests\unit-tests\Main.UnitTests.fsproj", "{85297F41-0524-400A-89EC-E4360CB033D9}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -20,8 +24,13 @@ Global {72471B86-58DF-42DC-8220-4E9E658AE781}.Debug|Any CPU.Build.0 = Debug|Any CPU {72471B86-58DF-42DC-8220-4E9E658AE781}.Release|Any CPU.ActiveCfg = Release|Any CPU {72471B86-58DF-42DC-8220-4E9E658AE781}.Release|Any CPU.Build.0 = Release|Any CPU + {85297F41-0524-400A-89EC-E4360CB033D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {85297F41-0524-400A-89EC-E4360CB033D9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {85297F41-0524-400A-89EC-E4360CB033D9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {85297F41-0524-400A-89EC-E4360CB033D9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {72471B86-58DF-42DC-8220-4E9E658AE781} = {2BBAEFA0-4E9F-467B-ABEA-A9854E821495} + {85297F41-0524-400A-89EC-E4360CB033D9} = {E2ECB101-4BA2-4884-9AE2-012BF8F8F433} EndGlobalSection EndGlobal diff --git a/src/Main/GraphQLWebsocketMiddleware.fs b/src/Main/GraphQLWebsocketMiddleware.fs index 2381f66..b25fadd 100644 --- a/src/Main/GraphQLWebsocketMiddleware.fs +++ b/src/Main/GraphQLWebsocketMiddleware.fs @@ -46,7 +46,7 @@ module internal GraphQLSubscriptionsManagement = type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifetime : IHostApplicationLifetime, jsonOptions : IOptions, executor : Executor<'Root>, root : unit -> 'Root, url: string) = - let serializeServerMessage (jsonOptions: JsonOptions) obj = + let serializeServerMessage (jsonOptions: JsonOptions) serverMessage = task { return "dummySerializedServerMessage" } let deserializeClientMessage (jsonOptions: JsonOptions) (msg: string) = @@ -153,6 +153,7 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti let logMsgWithIdReceived id msgAsStr = printfn "%s (id: %s)" msgAsStr id + let mutable socketClosed = false // <-------------- // <-- Helpers --| // <-------------- @@ -162,7 +163,7 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti // -------> task { try - while not cancellationToken.IsCancellationRequested do + while not cancellationToken.IsCancellationRequested && not socketClosed do let! receivedMessage = safe_Receive() match receivedMessage with | None -> @@ -192,7 +193,8 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti do! Complete id |> safe_Send | InvalidMessage explanation -> "InvalidMessage" |> logMsgReceivedWithOptionalPayload None - do! Error (None, explanation) |> safe_Send + do! socket.CloseAsync(enum CustomWebSocketStatus.invalidMessage, explanation, cancellationToken) + socketClosed <- true with // TODO: MAKE A PROPER GRAPHQL ERROR HANDLING! | ex -> printfn "Unexpected exception \"%s\" in GraphQLWebsocketMiddleware (handleMessages). More:\n%s" (ex.GetType().Name) (ex.ToString()) diff --git a/src/Main/JsonConverters.fs b/src/Main/JsonConverters.fs new file mode 100644 index 0000000..9047bad --- /dev/null +++ b/src/Main/JsonConverters.fs @@ -0,0 +1,72 @@ +namespace GraphQLTransportWS + +open System +open System.Text.Json +open System.Text.Json.Serialization + +[] +type WebSocketServerMessageConverter() = + inherit JsonConverter() + + override __.Read(reader: byref, typeToConvert: Type, options: JsonSerializerOptions) = + ConnectionAck + + override __.Write(writer: Utf8JsonWriter, value: WebSocketServerMessage, options: JsonSerializerOptions) = + let thenAddProperty (propName: string) (propStrValue: string) = + writer.WritePropertyName(propName) + writer.WriteStringValue(propStrValue) + + let beginSerializedObjWith (propName: string) (propStrValue: string) = + writer.WriteStartObject() + thenAddProperty propName propStrValue + + let endSerializedObj () = + writer.WriteEndObject() + + let addOptionalPayloadProperty (optionalPayload: string option) = + match optionalPayload with + | Some p -> + thenAddProperty "payload" p + | None -> + () + + match value with + | Error (id, errs) -> + beginSerializedObjWith "type" "error" + thenAddProperty "id" id + writer.WriteStartArray("payload") + errs + |> List.iter + (fun err -> + writer.WriteStartObject() + writer.WritePropertyName("message") + writer.WriteStringValue(err) + writer.WriteEndObject() + ) + writer.WriteEndArray() + endSerializedObj() + | ConnectionAck -> + beginSerializedObjWith "type" "connection_ack" + endSerializedObj() + | ServerPing p -> + beginSerializedObjWith "type" "ping" + addOptionalPayloadProperty p + endSerializedObj() + | ServerPong p -> + beginSerializedObjWith "type" "pong" + addOptionalPayloadProperty p + endSerializedObj() + | Next(id, result) -> + beginSerializedObjWith "type" "next" + thenAddProperty "id" id + writer.WritePropertyName("payload") + writer.WriteRawValue(JsonSerializer.Serialize(result), skipInputValidation = true) + endSerializedObj() + | Complete(id) -> + beginSerializedObjWith "type" "complete" + thenAddProperty "id" id + endSerializedObj() + + + + // writer.WriteStartObject(propertyName = ) \ No newline at end of file diff --git a/src/Main/Main.fsproj b/src/Main/Main.fsproj index 2cf75a5..fed7c19 100644 --- a/src/Main/Main.fsproj +++ b/src/Main/Main.fsproj @@ -13,6 +13,7 @@ + diff --git a/src/Main/WebSocketMessages.fs b/src/Main/WebSocketMessages.fs index d133b70..c3f5f6b 100644 --- a/src/Main/WebSocketMessages.fs +++ b/src/Main/WebSocketMessages.fs @@ -20,5 +20,8 @@ type WebSocketServerMessage = | ServerPing of payload: string option | ServerPong of payload: string option | Next of id : string * payload : Output - | Error of id : string option * err : string - | Complete of id : string \ No newline at end of file + | Error of id : string * err : string list + | Complete of id : string + +module CustomWebSocketStatus = + let invalidMessage = 4400 \ No newline at end of file diff --git a/tests/unit-tests/Main.UnitTests.fsproj b/tests/unit-tests/Main.UnitTests.fsproj new file mode 100644 index 0000000..a8235f2 --- /dev/null +++ b/tests/unit-tests/Main.UnitTests.fsproj @@ -0,0 +1,32 @@ + + + + net7.0 + + false + false + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/tests/unit-tests/Program.fs b/tests/unit-tests/Program.fs new file mode 100644 index 0000000..fdc31cd --- /dev/null +++ b/tests/unit-tests/Program.fs @@ -0,0 +1 @@ +module Program = let [] main _ = 0 diff --git a/tests/unit-tests/Tests.fs b/tests/unit-tests/Tests.fs new file mode 100644 index 0000000..f3c5dbd --- /dev/null +++ b/tests/unit-tests/Tests.fs @@ -0,0 +1,30 @@ +module Tests + +open GraphQLTransportWS +open System +open System.Text.Json +open Xunit + +[] +let ``Serializes ServerPing correctly`` () = + let jsonOptions = new JsonSerializerOptions() + jsonOptions.Converters.Add(new WebSocketServerMessageConverter()) + + let input: WebSocketServerMessage = + ServerPing (Some "Peekaboo!") + + let result = JsonSerializer.Serialize(input, jsonOptions) + + Assert.Equal("{\"type\":\"ping\",\"payload\":\"Peekaboo!\"}", result) + +[] +let ``Serializes Error correctly`` () = + let jsonOptions = new JsonSerializerOptions() + jsonOptions.Converters.Add(new WebSocketServerMessageConverter()) + + let input: WebSocketServerMessage = + Error ("myId", [ "An error ocurred during GraphQL execution" ]) + + let result = JsonSerializer.Serialize(input, jsonOptions) + + Assert.Equal("{\"type\":\"error\",\"id\":\"myId\",\"payload\":[{\"message\":\"An error ocurred during GraphQL execution\"}]}", result)