implement Session request handling

This commit is contained in:
Christopher Willis-Ford 2022-04-21 09:29:24 -07:00
parent ac6c51474e
commit c85b67b0e5
5 changed files with 253 additions and 46 deletions

View file

@ -0,0 +1,103 @@
// <copyright file="JsonRpc2Error.cs" company="Scratch Foundation">
// Copyright (c) Scratch Foundation. All rights reserved.
// </copyright>
namespace ScratchLink.JsonRpc;
using System.Text.Json.Serialization;
/// <summary>
/// Data class representing a JSON-RPC 2.0 Error object.
/// </summary>
internal class JsonRpc2Error
{
/// <summary>
/// Gets or sets the numeric error code for this error.
/// </summary>
[JsonPropertyName("code")]
public int Code { get; set; }
/// <summary>
/// Gets or sets a string providing a short description of the error.
/// </summary>
[JsonPropertyName("message")]
public string Message { get; set; }
/// <summary>
/// Gets or sets an optional value containing additional information about the error.
/// </summary>
[JsonPropertyName("data")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public object Data { get; set; }
/// <summary>
/// Creates an Error object representing a Parse Error.
/// </summary>
/// <param name="data">An optional value containing additional information about the error.</param>
/// <returns>A new Error object.</returns>
public static JsonRpc2Error ParseError(object data = null)
{
return new JsonRpc2Error { Code = -32700, Message = "Parse Error", Data = data };
}
/// <summary>
/// Creates an Error object representing an Invalid Request error.
/// </summary>
/// <param name="data">An optional value containing additional information about the error.</param>
/// <returns>A new Error object.</returns>
public static JsonRpc2Error InvalidRequest(object data = null)
{
return new JsonRpc2Error { Code = -32600, Message = "Invalid Request", Data = data };
}
/// <summary>
/// Creates an Error object representing a Method Not Found error.
/// </summary>
/// <param name="data">An optional value containing additional information about the error.</param>
/// <returns>A new Error object.</returns>
public static JsonRpc2Error MethodNotFound(object data = null)
{
return new JsonRpc2Error { Code = -32601, Message = "Method Not Found", Data = data };
}
/// <summary>
/// Creates an Error object representing an Invalid Params error.
/// </summary>
/// <param name="data">An optional value containing additional information about the error.</param>
/// <returns>A new Error object.</returns>
public static JsonRpc2Error InvalidParams(object data = null)
{
return new JsonRpc2Error { Code = -32602, Message = "Invalid Params", Data = data };
}
/// <summary>
/// Creates an Error object representing an Internal Error.
/// </summary>
/// <param name="data">An optional value containing additional information about the error.</param>
/// <returns>A new Error object.</returns>
public static JsonRpc2Error InternalError(object data = null)
{
return new JsonRpc2Error { Code = -32603, Message = "Internal Error", Data = data };
}
/// <summary>
/// Creates an Error object representing a Server Error.
/// </summary>
/// <param name="code">A numeric code for this error. Should be between -32000 and -32099, inclusive.</param>
/// <param name="data">An optional value containing additional information about the error.</param>
/// <returns>A new Error object.</returns>
public static JsonRpc2Error ServerError(int code, object data = null)
{
return new JsonRpc2Error { Code = code, Message = "Server Error", Data = data };
}
/// <summary>
/// Creates an Error object representing an Application Error.
/// </summary>
/// <param name="data">An optional value containing additional information about the error.</param>
/// <returns>A new Error object.</returns>
public static JsonRpc2Error ApplicationError(object data = null)
{
return new JsonRpc2Error { Code = -32500, Message = "Application Error", Data = data };
}
}

View file

@ -0,0 +1,27 @@
// <copyright file="JsonRpc2Exception.cs" company="Scratch Foundation">
// Copyright (c) Scratch Foundation. All rights reserved.
// </copyright>
namespace ScratchLink.JsonRpc;
using System;
/// <summary>
/// Exception class to hold a JSON-RPC 2.0 error.
/// </summary>
internal class JsonRpc2Exception : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="JsonRpc2Exception"/> class to report a <see cref="JsonRpc2Error"/>.
/// </summary>
/// <param name="error">The JSON-RPC error object to report.</param>
public JsonRpc2Exception(JsonRpc2Error error)
{
this.Error = error;
}
/// <summary>
/// Gets the <see cref="JsonRpc2Error"/> object associated with the thrown error.
/// </summary>
public JsonRpc2Error Error { get; init; }
}

View file

@ -1,28 +1,24 @@
// <copyright file="Request.cs" company="Scratch Foundation">
// <copyright file="JsonRpc2Request.cs" company="Scratch Foundation">
// Copyright (c) Scratch Foundation. All rights reserved.
// </copyright>
namespace ScratchLink.JsonRpc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
/// <summary>
/// Data class representing a JSON-RPC 2.0 Request object.
/// If the "id" property is null, this is a Notification object.
/// </summary>
internal class Request
internal class JsonRpc2Request
{
/// <summary>
/// Gets the JSON RPC version string (always "2.0").
/// </summary>
[JsonPropertyName("jsonrpc")]
public string JsonRPC { get; private set; }
[JsonPropertyOrder(-100)]
public string JsonRPC { get; } = "2.0";
/// <summary>
/// Gets or sets the name of the method being called.
@ -35,7 +31,7 @@ internal class Request
/// </summary>
[JsonPropertyName("params")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public JsonElement? Params { get; set; }
public object Params { get; set; }
/// <summary>
/// Gets or sets the request ID. May be a string, integer, or absent.
@ -43,5 +39,5 @@ internal class Request
/// </summary>
[JsonPropertyName("id")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public JsonElement? Id { get; set; }
public object Id { get; set; }
}

View file

@ -0,0 +1,44 @@
// <copyright file="JsonRpc2Response.cs" company="Scratch Foundation">
// Copyright (c) Scratch Foundation. All rights reserved.
// </copyright>
namespace ScratchLink.JsonRpc;
using System.Text.Json;
using System.Text.Json.Serialization;
/// <summary>
/// Data class representing a JSON-RPC 2.0 Response object.
/// Either "result" or "error" should be filled, not both.
/// </summary>
internal class JsonRpc2Response
{
/// <summary>
/// Gets the JSON RPC version string (always "2.0").
/// </summary>
[JsonPropertyName("jsonrpc")]
[JsonPropertyOrder(-100)]
public string JsonRPC { get; } = "2.0";
/// <summary>
/// Gets or sets the successful result of the corresponding Request.
/// This is REQUIRED on success and MUST NOT exist if there was an error.
/// </summary>
[JsonPropertyName("result")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public object Result { get; set; }
/// <summary>
/// Gets or sets an object describing an error triggered by the corresponding Request.
/// This is REQUIRED on error and MUST NOT exist if there was no error.
/// </summary>
[JsonPropertyName("error")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public JsonRpc2Error Error { get; set; }
/// <summary>
/// Gets or sets the response ID, which must match the ID of the corresponding Request. May be a string or integer.
/// </summary>
[JsonPropertyName("id")]
public JsonElement Id { get; set; }
}

View file

@ -4,14 +4,14 @@
namespace ScratchLink;
using ScratchLink.JsonRpc;
using System.Net.WebSockets;
using System.Text.Json;
using System.Text.Json.Nodes;
using JsonRpcMethodHandler = Func<
string, // method name
System.Text.Json.JsonElement?, // method params / args
Task<System.Text.Json.Nodes.JsonValue> // return value
object, // params / args
Task<object> // return value - must be JSON-serializable
>;
/// <summary>
@ -19,6 +19,12 @@ using JsonRpcMethodHandler = Func<
/// </summary>
internal class Session
{
/// <summary>
/// Specifies the Scratch Link network protocol version. Note that this is not the application version.
/// Keep this in sync with the version number in `NetworkProtocol.md`.
/// </summary>
protected const string NetworkProtocolVersion = "1.2";
/// <summary>
/// Stores the mapping from method names to handlers.
/// </summary>
@ -73,9 +79,63 @@ internal class Session
this.cancellationTokenSource.Cancel();
}
protected Task<JsonValue> HandleGetVersion(string methodName, JsonElement? args)
/// <summary>
/// Handle a "getVersion" request.
/// </summary>
/// <param name="methodName">The name of the method called (expected: "getVersion").</param>
/// <param name="args">Any arguments passed to the method by the caller (expected: none).</param>
/// <returns>A string representing the protocol version.</returns>
protected Task<object> HandleGetVersion(string methodName, object args)
{
return Task.FromResult(JsonValue.Create(0));
return Task.FromResult<object>(new Dictionary<string, string>
{
{ "protocol", NetworkProtocolVersion },
});
}
private Task<object> HandleUnrecognizedMethod(string methodName, object args)
{
throw new JsonRpc2Exception(JsonRpc2Error.MethodNotFound(methodName));
}
private async Task HandleRequest(JsonRpc.JsonRpc2Request request, CancellationToken cancellationToken)
{
var handler = this.Handlers.GetValueOrDefault(request.Method, this.HandleUnrecognizedMethod);
object result = null;
JsonRpc2Error error = null;
try
{
result = await handler(request.Method, request.Params);
}
catch (JsonRpc2Exception e)
{
error = e.Error;
}
catch (Exception e)
{
error = JsonRpc2Error.ApplicationError($"Unhandled error encountered during call: {e}");
}
if (request.Id is JsonElement requestId)
{
await this.SendResponse(requestId, result, error, cancellationToken);
}
}
private async Task SendResponse(JsonElement id, object result, JsonRpc2Error error, CancellationToken cancellationToken)
{
var response = new JsonRpc2Response
{
Id = id,
Result = (error == null) ? result : null,
Error = error,
};
var responseBytes = JsonSerializer.SerializeToUtf8Bytes(response);
var webSocket = this.context.WebSocket;
await webSocket.SendAsync(responseBytes, WebSocketMessageType.Text, true, cancellationToken);
}
private async void CommLoop()
@ -89,16 +149,15 @@ internal class Session
{
messageBuffer.SetLength(0);
var result = await webSocket.ReceiveMessageToStream(messageBuffer, MessageSizeLimit, cancellationToken);
if (result.CloseStatus.HasValue)
{
break;
}
messageBuffer.Position = 0;
var request = JsonSerializer.Deserialize<JsonRpc.Request>(messageBuffer);
if (request != null)
if (messageBuffer.Length > 0)
{
await this.HandleRequest(request, cancellationToken);
messageBuffer.Position = 0;
var request = JsonSerializer.Deserialize<JsonRpc.JsonRpc2Request>(messageBuffer);
if (request != null)
{
await this.HandleRequest(request, cancellationToken);
}
}
}
}
@ -112,26 +171,4 @@ internal class Session
webSocket.Dispose();
}
}
private async Task HandleRequest(JsonRpc.Request request, CancellationToken cancellationToken)
{
var handler = this.Handlers[request.Method];
/*
try
{
var = (handler != null)
if (handler != null)
{
var result =
}
}
catch (Exception)
{
}
var result = handler != null ?
await handler(request.Method, request.Params) :
await HandleUnrecognizedMethod(request.Method, request.Params);
*/
}
}