Skip to content

Commit

Permalink
com.utilities.rest 3.0.0 (#77)
Browse files Browse the repository at this point in the history
- overhauled Server Sent Event callbacks
  - properly handles all event types and fields
  - breaking change: data payloads now come nested in json object
- Rest.Response.ctr has new overload which takes RestParameters
- updated com.utilities.async -> 2.1.6
  • Loading branch information
StephenHodgson committed Jun 6, 2024
1 parent 650c92a commit 79166f2
Show file tree
Hide file tree
Showing 6 changed files with 103 additions and 59 deletions.
15 changes: 15 additions & 0 deletions Documentation~/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,21 @@ response.Validate(debug: true);

#### Server Sent Events

> [!WARNING]
> Breaking change. `eventData` payloads are now json objects where the type is the key and field data is value.
> For existing data callbacks, they are now nested: `{"data":"{<payload data>}"}`
Handles [server sent event](https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events) messages.

`eventData` json Schema:

```json
{
"type":"value",
"data":"{<payload data>}" // nullable
}
```

```csharp
var jsonData = "{\"data\":\"content\"}";
var response = await Rest.PostAsync("www.your.api/endpoint", jsonData, eventData => {
Expand Down
4 changes: 2 additions & 2 deletions Runtime/DownloadHandlerCallback.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ protected override bool ReceiveData(byte[] unprocessedData, int dataLength)
var buffer = new byte[bytesToRead];
var bytesRead = stream.Read(buffer, 0, (int)bytesToRead);
streamPosition += bytesRead;
OnDataReceived?.Invoke(new Response(webRequest.url, webRequest.method, null, true, null, buffer, webRequest.responseCode, webRequest.GetResponseHeaders()));
OnDataReceived?.Invoke(new Response(webRequest.url, webRequest.method, null, true, null, buffer, webRequest.responseCode, webRequest.GetResponseHeaders(), null, null));
}
}
catch (Exception e)
Expand All @@ -69,7 +69,7 @@ protected override void CompleteContent()
var buffer = new byte[StreamOffset];
var bytesRead = stream.Read(buffer);
streamPosition += bytesRead;
OnDataReceived?.Invoke(new Response(webRequest.url, webRequest.method, null, true, null, buffer, webRequest.responseCode, webRequest.GetResponseHeaders()));
OnDataReceived?.Invoke(new Response(webRequest.url, webRequest.method, null, true, null, buffer, webRequest.responseCode, webRequest.GetResponseHeaders(), null, null));
}
}
catch (Exception e)
Expand Down
53 changes: 28 additions & 25 deletions Runtime/Response.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ public class Response
/// </summary>
public string Error { get; }

/// <summary>
/// Request parameters.
/// </summary>
public RestParameters Parameters { get; }

/// <summary>
/// Constructor.
/// </summary>
Expand All @@ -69,8 +74,9 @@ public class Response
/// <param name="data">Response data from the resource.</param>
/// <param name="responseCode">Response code from the resource.</param>
/// <param name="headers">Response headers from the resource.</param>
/// <param name="parameters">The parameters of the request.</param>
/// <param name="error">Optional, error message from the resource.</param>
public Response(string request, string method, string requestBody, bool successful, string body, byte[] data, long responseCode, IReadOnlyDictionary<string, string> headers, string error = null)
public Response(string request, string method, string requestBody, bool successful, string body, byte[] data, long responseCode, IReadOnlyDictionary<string, string> headers, RestParameters parameters, string error = null)
{
Request = request;
RequestBody = requestBody;
Expand All @@ -81,23 +87,14 @@ public Response(string request, string method, string requestBody, bool successf
Code = responseCode;
Headers = headers;
Error = error;
Parameters = parameters;
}

/// <summary>
/// Constructor.
/// </summary>
/// <param name="request">The request that prompted the response.</param>
/// <param name="method">The request method that prompted the response.</param>
/// <param name="successful">Was the REST call successful?</param>
/// <param name="body">Response body from the resource.</param>
/// <param name="data">Response data from the resource.</param>
/// <param name="responseCode">Response code from the resource.</param>
/// <param name="headers">Response headers from the resource.</param>
/// <param name="error">Optional, error message from the resource.</param>
[Obsolete("Use new .ctr with requestBody")]
public Response(string request, string method, bool successful, string body, byte[] data, long responseCode, IReadOnlyDictionary<string, string> headers, string error = null)
[Obsolete("Use new .ctr with parameters")]
public Response(string request, string method, string requestBody, bool successful, string body, byte[] data, long responseCode, IReadOnlyDictionary<string, string> headers, string error = null)
{
Request = request;
RequestBody = requestBody;
Method = method;
Successful = successful;
Body = body;
Expand Down Expand Up @@ -159,24 +156,30 @@ public string ToString(string methodName)

if (!string.IsNullOrWhiteSpace(Body))
{
var parts = Body.Split("data: ");

if (parts.Length > 0)
if (Parameters.ServerSentEvents.Count > 0)
{
debugMessageObject["response"]["body"] = new JArray();

foreach (var part in parts)
foreach (var (type, value, data) in Parameters.ServerSentEvents)
{
if (string.IsNullOrWhiteSpace(part) || part.Contains("[DONE]\n\n")) { continue; }

try
var eventObject = new JObject
{
((JArray)debugMessageObject["response"]["body"]).Add(JToken.Parse(part));
}
catch
[type] = value
};

if (!string.IsNullOrWhiteSpace(data))
{
((JArray)debugMessageObject["response"]["body"]).Add(new JValue(part));
try
{
eventObject[nameof(data)] = JToken.Parse(data);
}
catch
{
eventObject[nameof(data)] = data;
}
}

((JArray)debugMessageObject["response"]["body"]).Add(eventObject);
}
}
else
Expand Down
84 changes: 54 additions & 30 deletions Runtime/Rest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
Expand All @@ -25,12 +25,13 @@ public static class Rest
{
internal const string FileUriPrefix = "file:https://";
private const string kHttpVerbPATCH = "PATCH";
private const string eventDelimiter = "data: ";
private const string stopEventDelimiter = "[DONE]";
private const string content_type = "Content-Type";
private const string content_length = "Content-Length";
private const string application_json = "application/json";
private const string application_octet_stream = "application/octet-stream";
private const string ssePattern = @"(?:(?:(?<type>[^:\n]*):)(?<value>(?:(?!\n\n|\ndata:).)*)(?:\ndata:(?<data>(?:(?!\n\n).)*))?\n\n)";

private static readonly Regex sseRegex = new(ssePattern);

#region Authentication

Expand Down Expand Up @@ -1203,7 +1204,7 @@ async void CallbackThread()
}
catch (Exception e)
{
return new Response(webRequest.url, webRequest.method, requestBody, false, $"{nameof(Rest)}.{nameof(SendAsync)}::{nameof(UnityWebRequest.SendWebRequest)} Failed!", null, -1, null, e.ToString());
return new Response(webRequest.url, webRequest.method, requestBody, false, $"{nameof(Rest)}.{nameof(SendAsync)}::{nameof(UnityWebRequest.SendWebRequest)} Failed!", null, -1, null, parameters, e.ToString());
}

parameters?.Progress?.Report(new Progress(webRequest.downloadedBytes, webRequest.downloadedBytes, 100f, 0, Progress.DataUnit.b));
Expand All @@ -1216,13 +1217,13 @@ UnityWebRequest.Result.ConnectionError or
{
return webRequest.downloadHandler switch
{
DownloadHandlerFile => new Response(webRequest.url, webRequest.method, requestBody, false, null, null, webRequest.responseCode, responseHeaders, $"{webRequest.error}\n{webRequest.downloadHandler?.error}"),
DownloadHandlerTexture => new Response(webRequest.url, webRequest.method, requestBody, false, null, null, webRequest.responseCode, responseHeaders, $"{webRequest.error}\n{webRequest.downloadHandler?.error}"),
DownloadHandlerAudioClip => new Response(webRequest.url, webRequest.method, requestBody, false, null, null, webRequest.responseCode, responseHeaders, $"{webRequest.error}\n{webRequest.downloadHandler?.error}"),
DownloadHandlerAssetBundle => new Response(webRequest.url, webRequest.method, requestBody, false, null, null, webRequest.responseCode, responseHeaders, $"{webRequest.error}\n{webRequest.downloadHandler?.error}"),
DownloadHandlerBuffer bufferDownloadHandler => new Response(webRequest.url, webRequest.method, requestBody, false, bufferDownloadHandler.text, bufferDownloadHandler.data, webRequest.responseCode, responseHeaders, $"{webRequest.error}\n{webRequest.downloadHandler?.error}"),
DownloadHandlerScript scriptDownloadHandler => new Response(webRequest.url, webRequest.method, requestBody, false, scriptDownloadHandler.text, scriptDownloadHandler.data, webRequest.responseCode, responseHeaders, $"{webRequest.error}\n{webRequest.downloadHandler?.error}"),
_ => new Response(webRequest.url, webRequest.method, requestBody, false, webRequest.responseCode == 401 ? "Invalid Credentials" : webRequest.downloadHandler?.text, webRequest.downloadHandler?.data, webRequest.responseCode, responseHeaders, $"{webRequest.error}\n{webRequest.downloadHandler?.error}")
DownloadHandlerFile => new Response(webRequest.url, webRequest.method, requestBody, false, null, null, webRequest.responseCode, responseHeaders, parameters, $"{webRequest.error}\n{webRequest.downloadHandler?.error}"),
DownloadHandlerTexture => new Response(webRequest.url, webRequest.method, requestBody, false, null, null, webRequest.responseCode, responseHeaders, parameters, $"{webRequest.error}\n{webRequest.downloadHandler?.error}"),
DownloadHandlerAudioClip => new Response(webRequest.url, webRequest.method, requestBody, false, null, null, webRequest.responseCode, responseHeaders, parameters, $"{webRequest.error}\n{webRequest.downloadHandler?.error}"),
DownloadHandlerAssetBundle => new Response(webRequest.url, webRequest.method, requestBody, false, null, null, webRequest.responseCode, responseHeaders, parameters, $"{webRequest.error}\n{webRequest.downloadHandler?.error}"),
DownloadHandlerBuffer bufferDownloadHandler => new Response(webRequest.url, webRequest.method, requestBody, false, bufferDownloadHandler.text, bufferDownloadHandler.data, webRequest.responseCode, responseHeaders, parameters, $"{webRequest.error}\n{webRequest.downloadHandler?.error}"),
DownloadHandlerScript scriptDownloadHandler => new Response(webRequest.url, webRequest.method, requestBody, false, scriptDownloadHandler.text, scriptDownloadHandler.data, webRequest.responseCode, responseHeaders, parameters, $"{webRequest.error}\n{webRequest.downloadHandler?.error}"),
_ => new Response(webRequest.url, webRequest.method, requestBody, false, webRequest.responseCode == 401 ? "Invalid Credentials" : webRequest.downloadHandler?.text, webRequest.downloadHandler?.data, webRequest.responseCode, responseHeaders, parameters, $"{webRequest.error}\n{webRequest.downloadHandler?.error}")
};
}

Expand All @@ -1233,35 +1234,58 @@ UnityWebRequest.Result.ConnectionError or

return webRequest.downloadHandler switch
{
DownloadHandlerFile => new Response(webRequest.url, webRequest.method, requestBody, true, null, null, webRequest.responseCode, responseHeaders),
DownloadHandlerTexture => new Response(webRequest.url, webRequest.method, requestBody, true, null, null, webRequest.responseCode, responseHeaders),
DownloadHandlerAudioClip => new Response(webRequest.url, webRequest.method, requestBody, true, null, null, webRequest.responseCode, responseHeaders),
DownloadHandlerAssetBundle => new Response(webRequest.url, webRequest.method, requestBody, true, null, null, webRequest.responseCode, responseHeaders),
DownloadHandlerBuffer bufferDownloadHandler => new Response(webRequest.url, webRequest.method, requestBody, true, bufferDownloadHandler.text, bufferDownloadHandler.data, webRequest.responseCode, responseHeaders),
DownloadHandlerScript scriptDownloadHandler => new Response(webRequest.url, webRequest.method, requestBody, true, scriptDownloadHandler.text, scriptDownloadHandler.data, webRequest.responseCode, responseHeaders),
_ => new Response(webRequest.url, webRequest.method, requestBody, true, webRequest.downloadHandler?.text, webRequest.downloadHandler?.data, webRequest.responseCode, responseHeaders)
DownloadHandlerFile => new Response(webRequest.url, webRequest.method, requestBody, true, null, null, webRequest.responseCode, responseHeaders, parameters),
DownloadHandlerTexture => new Response(webRequest.url, webRequest.method, requestBody, true, null, null, webRequest.responseCode, responseHeaders, parameters),
DownloadHandlerAudioClip => new Response(webRequest.url, webRequest.method, requestBody, true, null, null, webRequest.responseCode, responseHeaders, parameters),
DownloadHandlerAssetBundle => new Response(webRequest.url, webRequest.method, requestBody, true, null, null, webRequest.responseCode, responseHeaders, parameters),
DownloadHandlerBuffer bufferDownloadHandler => new Response(webRequest.url, webRequest.method, requestBody, true, bufferDownloadHandler.text, bufferDownloadHandler.data, webRequest.responseCode, responseHeaders, parameters),
DownloadHandlerScript scriptDownloadHandler => new Response(webRequest.url, webRequest.method, requestBody, true, scriptDownloadHandler.text, scriptDownloadHandler.data, webRequest.responseCode, responseHeaders, parameters),
_ => new Response(webRequest.url, webRequest.method, requestBody, true, webRequest.downloadHandler?.text, webRequest.downloadHandler?.data, webRequest.responseCode, responseHeaders, parameters)
};

void SendServerEventCallback(bool isEnd)
{
var allEventMessages = webRequest.downloadHandler?.text;
if (string.IsNullOrWhiteSpace(allEventMessages)) { return; }

var matches = sseRegex.Matches(allEventMessages!);
var stride = isEnd ? 0 : 1;
parameters ??= new RestParameters();
var lines = webRequest.downloadHandler?.text
.Split(eventDelimiter, StringSplitOptions.RemoveEmptyEntries)
.ToArray();

if (lines is { Length: > 1 })
for (var i = parameters.ServerSentEventCount; i < matches.Count - stride; i++)
{
var stride = isEnd ? 0 : 1;
for (var i = parameters.ServerSentEventCount; i < lines.Length - stride; i++)
string type;
string value;
string data;

var match = matches[i];

const string comment = nameof(comment);
type = match.Groups[nameof(type)].Value.Trim();
// If the field type is not provided, treat it as a comment
type = string.IsNullOrEmpty(type) ? comment : type;
value = match.Groups[nameof(value)].Value.Trim();
data = match.Groups[nameof(data)].Value.Trim();

if ((type.Equals("event") && value.Equals("done") && data.Equals("[DONE]")) ||
(type.Equals("data") && value.Equals("[DONE]")))
{
var line = lines[i];
return;
}

if (!line.Contains(stopEventDelimiter))
{
parameters.ServerSentEventCount++;
serverSentEventCallback.Invoke(line);
}
var eventStringBuilder = new StringBuilder();
eventStringBuilder.Append("{");
eventStringBuilder.Append($"\"{type}\":\"{value}\"");

if (!string.IsNullOrWhiteSpace(data))
{
eventStringBuilder.Append($",\"{nameof(data)}\":\"{data}\"");
}

eventStringBuilder.Append("}");
serverSentEventCallback.Invoke(eventStringBuilder.ToString());
parameters.ServerSentEventCount++;
parameters.ServerSentEvents.Add(new Tuple<string, string, string>(type, value, data));
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions Runtime/RestParameters.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ public RestParameters(

internal int ServerSentEventCount { get; set; }

internal readonly List<Tuple<string, string, string>> ServerSentEvents = new();

/// <summary>
/// Cache downloaded content.<br/>
/// Default is true.
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"displayName": "Utilities.Rest",
"description": "This package contains useful RESTful utilities for the Unity Game Engine.",
"keywords": [],
"version": "2.5.7",
"version": "3.0.0",
"unity": "2021.3",
"documentationUrl": "https://github.com/RageAgainstThePixel/com.utilities.rest#documentation",
"changelogUrl": "https://github.com/RageAgainstThePixel/com.utilities.rest/releases",
Expand All @@ -15,7 +15,7 @@
"author": "Stephen Hodgson",
"url": "https://github.com/StephenHodgson",
"dependencies": {
"com.utilities.async": "2.1.5",
"com.utilities.async": "2.1.6",
"com.utilities.extensions": "1.1.15",
"com.unity.modules.unitywebrequest": "1.0.0",
"com.unity.modules.unitywebrequestassetbundle": "1.0.0",
Expand Down

0 comments on commit 79166f2

Please sign in to comment.