EdgegapApiBase.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. using System;
  2. using System.Collections.Specialized;
  3. using System.Net;
  4. using System.Net.Http;
  5. using System.Net.Http.Headers;
  6. using System.Text;
  7. using System.Threading;
  8. using System.Threading.Tasks;
  9. // using Codice.Utils; // MIRROR CHANGE
  10. using Edgegap.Codice.Utils; // MIRROR CHANGE
  11. using UnityEngine;
  12. namespace Edgegap.Editor.Api
  13. {
  14. /// <summary>
  15. /// Handles base URL and common methods for all Edgegap APIs.
  16. /// </summary>
  17. public abstract class EdgegapApiBase
  18. {
  19. #region Vars
  20. private readonly HttpClient _httpClient = new HttpClient(); // Base address set // MIRROR CHANGE: Unity 2020 support
  21. protected ApiEnvironment SelectedApiEnvironment { get; }
  22. protected EdgegapWindowMetadata.LogLevel LogLevel { get; set; }
  23. protected bool IsLogLevelDebug => LogLevel == EdgegapWindowMetadata.LogLevel.Debug;
  24. /// <summary>Based on SelectedApiEnvironment.</summary>
  25. /// <returns></returns>
  26. private string GetBaseUrl() =>
  27. SelectedApiEnvironment == ApiEnvironment.Staging
  28. ? ApiEnvironment.Staging.GetApiUrl()
  29. : ApiEnvironment.Console.GetApiUrl();
  30. #endregion // Vars
  31. /// <param name="apiEnvironment">"console" || "staging-console"?</param>
  32. /// <param name="apiToken">Without the "token " prefix, although we'll clear this if present</param>
  33. /// <param name="logLevel">You may want more-verbose logs other than errs</param>
  34. protected EdgegapApiBase(
  35. ApiEnvironment apiEnvironment,
  36. string apiToken,
  37. EdgegapWindowMetadata.LogLevel logLevel = EdgegapWindowMetadata.LogLevel.Error)
  38. {
  39. this.SelectedApiEnvironment = apiEnvironment;
  40. this._httpClient.BaseAddress = new Uri($"{GetBaseUrl()}/");
  41. this._httpClient.DefaultRequestHeaders.Accept.Add(
  42. new MediaTypeWithQualityHeaderValue("application/json"));
  43. string cleanedApiToken = apiToken.Replace("token ", ""); // We already prefixed token below
  44. this._httpClient.DefaultRequestHeaders.Authorization =
  45. new AuthenticationHeaderValue("token", cleanedApiToken);
  46. this.LogLevel = logLevel;
  47. }
  48. #region HTTP Requests
  49. /// <summary>
  50. /// POST | We already added "https://api.edgegap.com/" (or similar) BaseAddress via constructor.
  51. /// </summary>
  52. /// <param name="relativePath"></param>
  53. /// <param name="json">Serialize to your model via Newtonsoft</param>
  54. /// <returns>
  55. /// - Success => returns HttpResponseMessage result
  56. /// - Error => Catches errs => returns null (no rethrow)
  57. /// </returns>
  58. protected async Task<HttpResponseMessage> PostAsync(string relativePath = "", string json = "{}")
  59. {
  60. StringContent stringContent = CreateStringContent(json);
  61. Uri uri = new Uri(_httpClient.BaseAddress, relativePath); // Normalize POST uri: Can't end with `/`.
  62. if (IsLogLevelDebug)
  63. Debug.Log($"PostAsync to: `{uri}` with json: `{json}`");
  64. try
  65. {
  66. return await ExecuteRequestAsync(() => _httpClient.PostAsync(uri, stringContent));
  67. }
  68. catch (Exception e)
  69. {
  70. Debug.LogError($"Error: {e}");
  71. throw;
  72. }
  73. }
  74. /// <summary>
  75. /// PATCH | We already added "https://api.edgegap.com/" (or similar) BaseAddress via constructor.
  76. /// </summary>
  77. /// <param name="relativePath"></param>
  78. /// <param name="json">Serialize to your model via Newtonsoft</param>
  79. /// <returns>
  80. /// - Success => returns HttpResponseMessage result
  81. /// - Error => Catches errs => returns null (no rethrow)
  82. /// </returns>
  83. protected async Task<HttpResponseMessage> PatchAsync(string relativePath = "", string json = "{}")
  84. {
  85. StringContent stringContent = CreateStringContent(json);
  86. Uri uri = new Uri(_httpClient.BaseAddress, relativePath); // Normalize PATCH uri: Can't end with `/`.
  87. if (IsLogLevelDebug)
  88. Debug.Log($"PatchAsync to: `{uri}` with json: `{json}`");
  89. // (!) As of 11/15/2023, .PatchAsync() is "unsupported by Unity" -- so we manually set the verb and SendAsync()
  90. // Create the request manually
  91. HttpRequestMessage patchRequest = new HttpRequestMessage(new HttpMethod("PATCH"), uri)
  92. {
  93. Content = stringContent,
  94. };
  95. try
  96. {
  97. return await ExecuteRequestAsync(() => _httpClient.SendAsync(patchRequest));
  98. }
  99. catch (Exception e)
  100. {
  101. Debug.LogError($"Error: {e}");
  102. throw;
  103. }
  104. }
  105. /// <summary>
  106. /// GET | We already added "https://api.edgegap.com/" (or similar) BaseAddress via constructor.
  107. /// </summary>
  108. /// <param name="relativePath"></param>
  109. /// <param name="customQuery">
  110. /// To append to the URL; eg: "foo=0&bar=1"
  111. /// (!) First query key should prefix nothing, as shown</param>
  112. /// <returns>
  113. /// - Success => returns HttpResponseMessage result
  114. /// - Error => Catches errs => returns null (no rethrow)
  115. /// </returns>
  116. protected async Task<HttpResponseMessage> GetAsync(string relativePath = "", string customQuery = "")
  117. {
  118. string completeRelativeUri = prepareEdgegapUriWithQuery(
  119. relativePath,
  120. customQuery);
  121. if (IsLogLevelDebug)
  122. Debug.Log($"GetAsync to: `{completeRelativeUri} with customQuery: `{customQuery}`");
  123. try
  124. {
  125. return await ExecuteRequestAsync(() => _httpClient.GetAsync(completeRelativeUri));
  126. }
  127. catch (Exception e)
  128. {
  129. Debug.LogError($"Error: {e}");
  130. throw;
  131. }
  132. }
  133. /// <summary>
  134. /// DELETE | We already added "https://api.edgegap.com/" (or similar) BaseAddress via constructor.
  135. /// </summary>
  136. /// <param name="relativePath"></param>
  137. /// <param name="customQuery">
  138. /// To append to the URL; eg: "foo=0&bar=1"
  139. /// (!) First query key should prefix nothing, as shown</param>
  140. /// <returns>
  141. /// - Success => returns HttpResponseMessage result
  142. /// - Error => Catches errs => returns null (no rethrow)
  143. /// </returns>
  144. protected async Task<HttpResponseMessage> DeleteAsync(string relativePath = "", string customQuery = "")
  145. {
  146. string completeRelativeUri = prepareEdgegapUriWithQuery(
  147. relativePath,
  148. customQuery);
  149. if (IsLogLevelDebug)
  150. Debug.Log($"DeleteAsync to: `{completeRelativeUri} with customQuery: `{customQuery}`");
  151. try
  152. {
  153. return await ExecuteRequestAsync(() => _httpClient.DeleteAsync(completeRelativeUri));
  154. }
  155. catch (Exception e)
  156. {
  157. Debug.LogError($"Error: {e}");
  158. throw;
  159. }
  160. }
  161. /// <summary>POST || GET</summary>
  162. /// <param name="requestFunc"></param>
  163. /// <param name="cancellationToken"></param>
  164. /// <returns></returns>
  165. private static async Task<HttpResponseMessage> ExecuteRequestAsync(
  166. Func<Task<HttpResponseMessage>> requestFunc,
  167. CancellationToken cancellationToken = default)
  168. {
  169. HttpResponseMessage response = null;
  170. try
  171. {
  172. response = await requestFunc();
  173. }
  174. catch (HttpRequestException e)
  175. {
  176. Debug.LogError($"HttpRequestException: {e.Message}");
  177. return null;
  178. }
  179. catch (TaskCanceledException e)
  180. {
  181. if (cancellationToken.IsCancellationRequested)
  182. Debug.LogError("Task was cancelled by caller.");
  183. else
  184. Debug.LogError($"TaskCanceledException: Timeout - {e.Message}");
  185. return null;
  186. }
  187. catch (Exception e) // Generic exception handler
  188. {
  189. Debug.LogError($"Unexpected error occurred: {e.Message}");
  190. return null;
  191. }
  192. // Check for a successful status code
  193. if (response == null)
  194. {
  195. Debug.Log("!Success (null response) - returning 500");
  196. return CreateUnknown500Err();
  197. }
  198. if (!response.IsSuccessStatusCode)
  199. {
  200. HttpMethod httpMethod = response.RequestMessage.Method;
  201. Debug.Log($"!Success: {(short)response.StatusCode} {response.ReasonPhrase} - " +
  202. $"{httpMethod} | {response.RequestMessage.RequestUri}` - " +
  203. $"{response.Content?.ReadAsStringAsync().Result}");
  204. }
  205. return response;
  206. }
  207. #endregion // HTTP Requests
  208. #region Utils
  209. /// <summary>Creates a UTF-8 encoded application/json + json obj</summary>
  210. /// <param name="json">Arbitrary json obj</param>
  211. /// <returns></returns>
  212. private StringContent CreateStringContent(string json = "{}") =>
  213. new StringContent(json, Encoding.UTF8, "application/json"); // MIRROR CHANGE: 'new()' not supported in Unity 2020
  214. private static HttpResponseMessage CreateUnknown500Err() =>
  215. new HttpResponseMessage(HttpStatusCode.InternalServerError); // 500 - Unknown // MIRROR CHANGE: 'new()' not supported in Unity 2020
  216. /// <summary>
  217. /// Merges Edgegap-required query params (source) -> merges with custom query -> normalizes.
  218. /// </summary>
  219. /// <param name="relativePath"></param>
  220. /// <param name="customQuery"></param>
  221. /// <returns></returns>
  222. private string prepareEdgegapUriWithQuery(string relativePath, string customQuery)
  223. {
  224. // Create UriBuilder using the BaseAddress
  225. UriBuilder uriBuilder = new UriBuilder(_httpClient.BaseAddress);
  226. // Add the relative path to the UriBuilder's path
  227. uriBuilder.Path += relativePath;
  228. // Parse the existing query from the UriBuilder
  229. NameValueCollection query = HttpUtility.ParseQueryString(uriBuilder.Query);
  230. // Add default "source=unity" param
  231. query["source"] = "unity";
  232. // Parse and merge the custom query parameters
  233. NameValueCollection customParams = HttpUtility.ParseQueryString(customQuery);
  234. foreach (string key in customParams)
  235. {
  236. query[key] = customParams[key];
  237. }
  238. // Set the merged query back to the UriBuilder
  239. uriBuilder.Query = query.ToString();
  240. // Extract the complete relative URI and return it
  241. return uriBuilder.Uri.PathAndQuery;
  242. }
  243. #endregion // Utils
  244. }
  245. }