Skip to content
This repository has been archived by the owner on Apr 1, 2024. It is now read-only.

Commit

Permalink
incoporated MessageMapping into the RawMessageConverter
Browse files Browse the repository at this point in the history
 I was originally thinking about having a dumbed down converter which
was easy to test, but as a matter of fact it makes no sense to pass
around an object which is not completely deserialized.
 The undeserialized payload is now deliberate, since it's now up to the
client to define how to deserialize his own custom payload. The
possibility to configure this deserialization (which will happen
actually during the handling of a message like "connection_init" or
"pong") will come next.
  • Loading branch information
valbers committed Feb 9, 2023
1 parent 19477c8 commit 7d2ed59
Show file tree
Hide file tree
Showing 7 changed files with 161 additions and 192 deletions.
31 changes: 18 additions & 13 deletions src/Main/GraphQLWebsocketMiddleware.fs
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,20 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti
return JsonSerializer.Serialize(raw, jsonSerializerOptions)
}

let deserializeClientMessage (serializerOptions : JsonSerializerOptions) (executor : Executor<'Root>) (msg: string) =
let deserializeClientMessage (serializerOptions : JsonSerializerOptions) (msg: string) =
task {
return
JsonSerializer.Deserialize<RawMessage>(msg, serializerOptions)
|> MessageMapping.toClientMessage serializerOptions executor
try
return
JsonSerializer.Deserialize<ClientMessage>(msg, serializerOptions)
|> succeed
with
| :? InvalidMessageException as e ->
return
fail <| InvalidMessage(4400, e.ToString())
| :? JsonException as e ->
printfn "%s" (e.ToString())
return
fail <| InvalidMessage(4400, "invalid json in client message")
}

let isSocketOpen (theSocket : WebSocket) =
Expand Down Expand Up @@ -122,14 +131,10 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti
if String.IsNullOrWhiteSpace message then
return None
else
try
let! result =
message
|> deserializeClientMessage serializerOptions executor
return Some result
with :? JsonException as e ->
printfn "%s" (e.ToString())
return Some (MessageMapping.invalidMsg <| "invalid json in client message")
let! result =
message
|> deserializeClientMessage serializerOptions
return Some result
}

let sendMessageViaSocket (jsonSerializerOptions) (socket : WebSocket) (message : ServerMessage) =
Expand Down Expand Up @@ -359,7 +364,7 @@ type GraphQLWebSocketMiddleware<'Root>(next : RequestDelegate, applicationLifeti
task {
let jsonOptions = new JsonOptions()
jsonOptions.SerializerOptions.PropertyNameCaseInsensitive <- true
jsonOptions.SerializerOptions.Converters.Add(new RawMessageConverter())
jsonOptions.SerializerOptions.Converters.Add(new ClientMessageConverter<'Root>(options.SchemaExecutor))
jsonOptions.SerializerOptions.Converters.Add(new RawServerMessageConverter())
let serializerOptions = jsonOptions.SerializerOptions
if false && not (ctx.Request.Path = PathString (options.EndpointUrl)) then
Expand Down
1 change: 0 additions & 1 deletion src/Main/Main.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
<Compile Include="Rop/RopAsync.fs" />
<Compile Include="Exceptions.fs" />
<Compile Include="Messages.fs" />
<Compile Include="MessageMapping.fs" />
<Compile Include="GraphQLWebsocketMiddlewareOptions.fs" />
<Compile Include="RawMessageConverter.fs" />
<Compile Include="GraphQLWebsocketMiddleware.fs" />
Expand Down
114 changes: 0 additions & 114 deletions src/Main/MessageMapping.fs

This file was deleted.

2 changes: 1 addition & 1 deletion src/Main/Messages.fs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ type RawSubscribePayload =

type RawMessage =
{ Id : string option
Type : string option
Type : string
Payload : JsonDocument option }

type ServerRawPayload =
Expand Down
128 changes: 122 additions & 6 deletions src/Main/RawMessageConverter.fs
Original file line number Diff line number Diff line change
@@ -1,16 +1,31 @@
namespace GraphQLTransportWS

open FSharp.Data.GraphQL
open Rop
open System
open System.Text.Json
open System.Text.Json.Serialization

[<Sealed>]
type RawMessageConverter() =
inherit JsonConverter<RawMessage>()
type ClientMessageConverter<'Root>(executor : Executor<'Root>) =
inherit JsonConverter<ClientMessage>()

let raiseInvalidMsg explanation =
raise <| InvalidMessageException explanation

/// From the spec: "Receiving a message of a type or format which is not specified in this document will result in an immediate socket closure with the event 4400: &lt;error-message&gt;.
/// The &lt;error-message&gt; can be vaguely descriptive on why the received message is invalid."
let invalidMsg (explanation : string) =
InvalidMessage (4400, explanation)
|> fail

let unpackRopResult ropResult =
match ropResult with
| Success (x, _) -> x
| Failure (failures : ClientMessageProtocolFailure list) ->
System.String.Join("\n\n", (failures |> Seq.map (fun (InvalidMessage (_, explanation)) -> explanation)))
|> raiseInvalidMsg

let getOptionalString (reader : byref<Utf8JsonReader>) =
if reader.TokenType.Equals(JsonTokenType.Null) then
None
Expand All @@ -23,7 +38,78 @@ type RawMessageConverter() =
else
raiseInvalidMsg <| sprintf "was expecting a value for property \"%s\"" propertyName

override __.Read(reader : byref<Utf8JsonReader>, typeToConvert: Type, options: JsonSerializerOptions) : RawMessage =
let requireId (raw : RawMessage) : RopResult<string, ClientMessageProtocolFailure> =
match raw.Id with
| Some s -> succeed s
| None -> invalidMsg <| "property \"id\" is required for this message but was not present."

let resolveVariables (serializerOptions : JsonSerializerOptions) (expectedVariables : Types.VarDef list) (variableValuesObj : JsonDocument) =
try
if (not (variableValuesObj.RootElement.ValueKind.Equals(JsonValueKind.Object))) then
let offendingValueKind = variableValuesObj.RootElement.ValueKind
fail (sprintf "\"variables\" must be an object, but here it is \"%A\" instead" offendingValueKind)
else
let providedVariableValues = variableValuesObj.RootElement.EnumerateObject() |> List.ofSeq
expectedVariables
|> List.choose
(fun expectedVariable ->
providedVariableValues
|> List.tryFind(fun x -> x.Name = expectedVariable.Name)
|> Option.map
(fun providedValue ->
let boxedValue =
if providedValue.Value.ValueKind.Equals(JsonValueKind.Null) then
null :> obj
elif providedValue.Value.ValueKind.Equals(JsonValueKind.String) then
providedValue.Value.GetString() :> obj
else
JsonSerializer.Deserialize(providedValue.Value, serializerOptions) :> obj
(expectedVariable.Name, boxedValue)
)
)
|> succeed
finally
variableValuesObj.Dispose()

let decodeGraphQLQuery (serializerOptions : JsonSerializerOptions) (executor : Executor<'a>) (operationName : string option) (variables : JsonDocument option) (query : string) =
let executionPlan =
match operationName with
| Some operationName ->
executor.CreateExecutionPlan(query, operationName = operationName)
| None ->
executor.CreateExecutionPlan(query)
let variablesResult : RopResult<Map<string, obj>, ClientMessageProtocolFailure> =
match variables with
| None -> succeed <| Map.empty // it's none of our business here if some variables are expected. If that's the case, execution of the ExecutionPlan will take care of that later (and issue an error).
| Some variableValuesObj ->
variableValuesObj
|> resolveVariables serializerOptions executionPlan.Variables
|> mapMessagesR (fun errMsg -> InvalidMessage (CustomWebSocketStatus.invalidMessage, errMsg))
|> mapR Map.ofList
variablesResult
|> mapR (fun variables ->
{ ExecutionPlan = executionPlan
Variables = variables })

let requireSubscribePayload (serializerOptions : JsonSerializerOptions) (executor : Executor<'a>) (payload : JsonDocument option) : RopResult<GraphQLQuery, ClientMessageProtocolFailure> =
match payload with
| None ->
invalidMsg <| "payload is required for this message, but none was present."
| Some p ->
let rawSubsPayload = JsonSerializer.Deserialize<RawSubscribePayload option>(p, serializerOptions)
match rawSubsPayload with
| None ->
invalidMsg <| "payload is required for this message, but none was present."
| Some subscribePayload ->
match subscribePayload.Query with
| None ->
invalidMsg <| sprintf "there was no query in the client's subscribe message!"
| Some query ->
query
|> decodeGraphQLQuery serializerOptions executor subscribePayload.OperationName subscribePayload.Variables


let readRawMessage (reader : byref<Utf8JsonReader>, options: JsonSerializerOptions) : RawMessage =
if not (reader.TokenType.Equals(JsonTokenType.StartObject))
then raise (new JsonException((sprintf "reader's first token was not \"%A\", but \"%A\"" JsonTokenType.StartObject reader.TokenType)))
else
Expand All @@ -44,12 +130,42 @@ type RawMessageConverter() =
match theType with
| None ->
raiseInvalidMsg "property \"type\" is missing"
| Some _ ->
| Some msgType ->
{ Id = id
Type = theType
Type = msgType
Payload = payload }

override __.Write(writer : Utf8JsonWriter, value : RawMessage, options : JsonSerializerOptions) =
override __.Read(reader : byref<Utf8JsonReader>, typeToConvert: Type, options: JsonSerializerOptions) : ClientMessage =
let raw = readRawMessage(&reader, options)
match raw.Type with
| "connection_init" ->
ConnectionInit raw.Payload
| "ping" ->
ClientPing raw.Payload
| "pong" ->
ClientPong raw.Payload
| "complete" ->
raw
|> requireId
|> mapR ClientComplete
|> unpackRopResult
| "subscribe" ->
raw
|> requireId
|> bindR
(fun id ->
raw.Payload
|> requireSubscribePayload options executor
|> mapR (fun payload -> (id, payload))
)
|> mapR Subscribe
|> unpackRopResult
| other ->
raiseInvalidMsg <| sprintf "invalid type \"%s\" specified by client." other



override __.Write(writer : Utf8JsonWriter, value : ClientMessage, options : JsonSerializerOptions) =
failwith "serializing a WebSocketClientMessage is not supported (yet(?))"

[<Sealed>]
Expand Down
17 changes: 3 additions & 14 deletions tests/unit-tests/InvalidMessageTests.fs
Original file line number Diff line number Diff line change
@@ -1,34 +1,23 @@
module InvalidMessageTests

open GraphQLTransportWS.Rop
open UnitTest
open GraphQLTransportWS
open System
open System.Text.Json
open Xunit
open FSharp.Data.GraphQL.Ast

let toClientMessage (theInput : string) =
let serializerOptions = new JsonSerializerOptions()
serializerOptions.PropertyNameCaseInsensitive <- true
serializerOptions.Converters.Add(new RawMessageConverter())
serializerOptions.Converters.Add(new ClientMessageConverter<Root>(TestSchema.executor))
serializerOptions.Converters.Add(new RawServerMessageConverter())
JsonSerializer.Deserialize<RawMessage>(theInput, serializerOptions)
|> MessageMapping.toClientMessage serializerOptions TestSchema.executor
JsonSerializer.Deserialize<ClientMessage>(theInput, serializerOptions)

let willResultInInvalidMessage expectedExplanation input =
try
let result =
input
|> toClientMessage
match result with
| Failure msgs ->
match msgs |> List.head with
| InvalidMessage (code, explanation) ->
Assert.Equal(4400, code)
Assert.Equal(expectedExplanation, explanation)
| other ->
Assert.Fail(sprintf "unexpected actual value: '%A'" other)
Assert.Fail(sprintf "should have failed, but succeeded with result: '%A'" result)
with
| :? JsonException as ex ->
Assert.Equal(expectedExplanation, ex.Message)
Expand Down
Loading

0 comments on commit 7d2ed59

Please sign in to comment.