using System;
using System.Collections.Specialized;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
// using Codice.Utils; // MIRROR CHANGE
using Edgegap.Codice.Utils; // MIRROR CHANGE
using UnityEngine;
namespace Edgegap.Editor.Api
{
///
/// Handles base URL and common methods for all Edgegap APIs.
///
public abstract class EdgegapApiBase
{
#region Vars
private readonly HttpClient _httpClient = new HttpClient(); // Base address set // MIRROR CHANGE: Unity 2020 support
protected ApiEnvironment SelectedApiEnvironment { get; }
protected EdgegapWindowMetadata.LogLevel LogLevel { get; set; }
protected bool IsLogLevelDebug => LogLevel == EdgegapWindowMetadata.LogLevel.Debug;
/// Based on SelectedApiEnvironment.
///
private string GetBaseUrl() =>
SelectedApiEnvironment == ApiEnvironment.Staging
? ApiEnvironment.Staging.GetApiUrl()
: ApiEnvironment.Console.GetApiUrl();
#endregion // Vars
/// "console" || "staging-console"?
/// Without the "token " prefix, although we'll clear this if present
/// You may want more-verbose logs other than errs
protected EdgegapApiBase(
ApiEnvironment apiEnvironment,
string apiToken,
EdgegapWindowMetadata.LogLevel logLevel = EdgegapWindowMetadata.LogLevel.Error)
{
this.SelectedApiEnvironment = apiEnvironment;
this._httpClient.BaseAddress = new Uri($"{GetBaseUrl()}/");
this._httpClient.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json"));
string cleanedApiToken = apiToken.Replace("token ", ""); // We already prefixed token below
this._httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("token", cleanedApiToken);
this.LogLevel = logLevel;
}
#region HTTP Requests
///
/// POST | We already added "https://api.edgegap.com/" (or similar) BaseAddress via constructor.
///
///
/// Serialize to your model via Newtonsoft
///
/// - Success => returns HttpResponseMessage result
/// - Error => Catches errs => returns null (no rethrow)
///
protected async Task PostAsync(string relativePath = "", string json = "{}")
{
StringContent stringContent = CreateStringContent(json);
Uri uri = new Uri(_httpClient.BaseAddress, relativePath); // Normalize POST uri: Can't end with `/`.
if (IsLogLevelDebug)
Debug.Log($"PostAsync to: `{uri}` with json: `{json}`");
try
{
return await ExecuteRequestAsync(() => _httpClient.PostAsync(uri, stringContent));
}
catch (Exception e)
{
Debug.LogError($"Error: {e}");
throw;
}
}
///
/// PATCH | We already added "https://api.edgegap.com/" (or similar) BaseAddress via constructor.
///
///
/// Serialize to your model via Newtonsoft
///
/// - Success => returns HttpResponseMessage result
/// - Error => Catches errs => returns null (no rethrow)
///
protected async Task PatchAsync(string relativePath = "", string json = "{}")
{
StringContent stringContent = CreateStringContent(json);
Uri uri = new Uri(_httpClient.BaseAddress, relativePath); // Normalize PATCH uri: Can't end with `/`.
if (IsLogLevelDebug)
Debug.Log($"PatchAsync to: `{uri}` with json: `{json}`");
// (!) As of 11/15/2023, .PatchAsync() is "unsupported by Unity" -- so we manually set the verb and SendAsync()
// Create the request manually
HttpRequestMessage patchRequest = new HttpRequestMessage(new HttpMethod("PATCH"), uri)
{
Content = stringContent,
};
try
{
return await ExecuteRequestAsync(() => _httpClient.SendAsync(patchRequest));
}
catch (Exception e)
{
Debug.LogError($"Error: {e}");
throw;
}
}
///
/// GET | We already added "https://api.edgegap.com/" (or similar) BaseAddress via constructor.
///
///
///
/// To append to the URL; eg: "foo=0&bar=1"
/// (!) First query key should prefix nothing, as shown
///
/// - Success => returns HttpResponseMessage result
/// - Error => Catches errs => returns null (no rethrow)
///
protected async Task GetAsync(string relativePath = "", string customQuery = "")
{
string completeRelativeUri = prepareEdgegapUriWithQuery(
relativePath,
customQuery);
if (IsLogLevelDebug)
Debug.Log($"GetAsync to: `{completeRelativeUri} with customQuery: `{customQuery}`");
try
{
return await ExecuteRequestAsync(() => _httpClient.GetAsync(completeRelativeUri));
}
catch (Exception e)
{
Debug.LogError($"Error: {e}");
throw;
}
}
///
/// DELETE | We already added "https://api.edgegap.com/" (or similar) BaseAddress via constructor.
///
///
///
/// To append to the URL; eg: "foo=0&bar=1"
/// (!) First query key should prefix nothing, as shown
///
/// - Success => returns HttpResponseMessage result
/// - Error => Catches errs => returns null (no rethrow)
///
protected async Task DeleteAsync(string relativePath = "", string customQuery = "")
{
string completeRelativeUri = prepareEdgegapUriWithQuery(
relativePath,
customQuery);
if (IsLogLevelDebug)
Debug.Log($"DeleteAsync to: `{completeRelativeUri} with customQuery: `{customQuery}`");
try
{
return await ExecuteRequestAsync(() => _httpClient.DeleteAsync(completeRelativeUri));
}
catch (Exception e)
{
Debug.LogError($"Error: {e}");
throw;
}
}
/// POST || GET
///
///
///
private static async Task ExecuteRequestAsync(
Func> requestFunc,
CancellationToken cancellationToken = default)
{
HttpResponseMessage response = null;
try
{
response = await requestFunc();
}
catch (HttpRequestException e)
{
Debug.LogError($"HttpRequestException: {e.Message}");
return null;
}
catch (TaskCanceledException e)
{
if (cancellationToken.IsCancellationRequested)
Debug.LogError("Task was cancelled by caller.");
else
Debug.LogError($"TaskCanceledException: Timeout - {e.Message}");
return null;
}
catch (Exception e) // Generic exception handler
{
Debug.LogError($"Unexpected error occurred: {e.Message}");
return null;
}
// Check for a successful status code
if (response == null)
{
Debug.Log("!Success (null response) - returning 500");
return CreateUnknown500Err();
}
if (!response.IsSuccessStatusCode)
{
HttpMethod httpMethod = response.RequestMessage.Method;
Debug.Log($"!Success: {(short)response.StatusCode} {response.ReasonPhrase} - " +
$"{httpMethod} | {response.RequestMessage.RequestUri}` - " +
$"{response.Content?.ReadAsStringAsync().Result}");
}
return response;
}
#endregion // HTTP Requests
#region Utils
/// Creates a UTF-8 encoded application/json + json obj
/// Arbitrary json obj
///
private StringContent CreateStringContent(string json = "{}") =>
new StringContent(json, Encoding.UTF8, "application/json"); // MIRROR CHANGE: 'new()' not supported in Unity 2020
private static HttpResponseMessage CreateUnknown500Err() =>
new HttpResponseMessage(HttpStatusCode.InternalServerError); // 500 - Unknown // MIRROR CHANGE: 'new()' not supported in Unity 2020
///
/// Merges Edgegap-required query params (source) -> merges with custom query -> normalizes.
///
///
///
///
private string prepareEdgegapUriWithQuery(string relativePath, string customQuery)
{
// Create UriBuilder using the BaseAddress
UriBuilder uriBuilder = new UriBuilder(_httpClient.BaseAddress);
// Add the relative path to the UriBuilder's path
uriBuilder.Path += relativePath;
// Parse the existing query from the UriBuilder
NameValueCollection query = HttpUtility.ParseQueryString(uriBuilder.Query);
// Add default "source=unity" param
query["source"] = "unity";
// Parse and merge the custom query parameters
NameValueCollection customParams = HttpUtility.ParseQueryString(customQuery);
foreach (string key in customParams)
{
query[key] = customParams[key];
}
// Set the merged query back to the UriBuilder
uriBuilder.Query = query.ToString();
// Extract the complete relative URI and return it
return uriBuilder.Uri.PathAndQuery;
}
#endregion // Utils
}
}