// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using EpicGames.Horde.Jobs;
#pragma warning disable CA2234 // Use Uri instead of string
namespace EpicGames.Horde
{
///
/// Static helper methods for implementing Horde HTTP requests with standard semantics
///
public static class HordeHttpRequest
{
static readonly JsonSerializerOptions s_jsonSerializerOptions = CreateJsonSerializerOptions();
internal static JsonSerializerOptions JsonSerializerOptions => s_jsonSerializerOptions;
///
/// Create the shared instance of JSON options for HordeHttpClient instances
///
static JsonSerializerOptions CreateJsonSerializerOptions()
{
JsonSerializerOptions options = new JsonSerializerOptions();
ConfigureJsonSerializer(options);
return options;
}
///
/// Configures a JSON serializer to read Horde responses
///
/// options for the serializer
public static void ConfigureJsonSerializer(JsonSerializerOptions options)
{
options.AllowTrailingCommas = true;
options.ReadCommentHandling = JsonCommentHandling.Skip;
options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
options.PropertyNameCaseInsensitive = true;
options.Converters.Add(new JsonStringEnumConverter());
options.Converters.Add(new StringIdJsonConverterFactory());
options.Converters.Add(new BinaryIdJsonConverterFactory());
options.Converters.Add(new SubResourceIdJsonConverterFactory());
}
///
/// Deletes a resource from an HTTP endpoint
///
/// Http client instance
/// The url to retrieve
/// Cancels the request
public static async Task DeleteAsync(HttpClient httpClient, string relativePath, CancellationToken cancellationToken = default)
{
using HttpResponseMessage response = await httpClient.DeleteAsync(relativePath, cancellationToken);
response.EnsureSuccessStatusCode();
}
///
/// Gets a resource from an HTTP endpoint and parses it as a JSON object
///
/// The object type to return
/// Http client instance
/// The url to retrieve
/// Cancels the request
/// New instance of the object
public static async Task GetAsync(HttpClient httpClient, string relativePath, CancellationToken cancellationToken = default)
{
TResponse? response = await httpClient.GetFromJsonAsync(relativePath, s_jsonSerializerOptions, cancellationToken);
return response ?? throw new InvalidCastException($"Expected non-null response from GET to {relativePath}");
}
///
/// Posts an object to an HTTP endpoint as a JSON object, and parses the response object
///
/// The object type to post
/// Http client instance
/// The url to retrieve
/// The object to post
/// Cancels the request
/// The response parsed into the requested type
internal static async Task PostAsync(HttpClient httpClient, string relativePath, TRequest request, CancellationToken cancellationToken = default)
{
return await httpClient.PostAsJsonAsync(relativePath, request, s_jsonSerializerOptions, cancellationToken);
}
///
/// Posts an object to an HTTP endpoint as a JSON object, and parses the response object
///
/// The object type to return
/// The object type to post
/// Http client instance
/// The url to retrieve
/// The object to post
/// Cancels the request
/// The response parsed into the requested type
public static async Task PostAsync(HttpClient httpClient, string relativePath, TRequest request, CancellationToken cancellationToken = default)
{
using (HttpResponseMessage response = await PostAsync(httpClient, relativePath, request, cancellationToken))
{
if (!response.IsSuccessStatusCode)
{
string body = await response.Content.ReadAsStringAsync(cancellationToken);
throw new HttpRequestException($"{(int)response.StatusCode} ({response.StatusCode}) posting to {new Uri(httpClient.BaseAddress!, relativePath)}: {body}", null, response.StatusCode);
}
TResponse? responseValue = await response.Content.ReadFromJsonAsync(s_jsonSerializerOptions, cancellationToken);
return responseValue ?? throw new InvalidCastException($"Expected non-null response from POST to {relativePath}");
}
}
///
/// Puts an object to an HTTP endpoint as a JSON object
///
/// The object type to post
/// Http client instance
/// The url to write to
/// The object to post
/// Cancels the request
/// Response message
public static async Task PutAsync(HttpClient httpClient, string relativePath, TRequest request, CancellationToken cancellationToken)
{
return await httpClient.PutAsJsonAsync(relativePath, request, s_jsonSerializerOptions, cancellationToken);
}
///
/// Puts an object to an HTTP endpoint as a JSON object
///
/// The object type to return
/// The object type to post
/// Http client instance
/// The url to write to
/// The object to post
/// Cancels the request
/// Response message
public static async Task PutAsync(HttpClient httpClient, string relativePath, TRequest request, CancellationToken cancellationToken)
{
using (HttpResponseMessage response = await httpClient.PutAsJsonAsync(relativePath, request, s_jsonSerializerOptions, cancellationToken))
{
if (!response.IsSuccessStatusCode)
{
string body = await response.Content.ReadAsStringAsync(cancellationToken);
throw new HttpRequestException($"{response.StatusCode} put to {new Uri(httpClient.BaseAddress!, relativePath)}: {body}", null, response.StatusCode);
}
TResponse? responseValue = await response.Content.ReadFromJsonAsync(s_jsonSerializerOptions, cancellationToken);
return responseValue ?? throw new InvalidCastException($"Expected non-null response from PUT to {relativePath}");
}
}
}
}