├── http ├── Method.cs ├── IRestResponse.cs ├── QueryParams.cs ├── RestResponse.cs └── RestRequest.cs ├── model ├── INestedResults.cs └── NestedResults.cs ├── .gitignore ├── LICENSE ├── helpers ├── ReflectionHelper.cs ├── SignatureHelper.cs ├── XmlHelper.cs ├── UrlHelper.cs └── JsonHelper.cs ├── RestClient.cs └── README.md /http/Method.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using UnityEngine; 5 | using System.Collections; 6 | 7 | namespace RESTClient { 8 | public enum Method { 9 | GET, 10 | POST, 11 | PATCH, 12 | DELETE, 13 | PUT 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /http/IRestResponse.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System; 5 | using System.Net; 6 | 7 | namespace RESTClient { 8 | public interface IRestResponse { 9 | bool IsError { get; } 10 | 11 | string ErrorMessage { get; } 12 | 13 | string Url { get; } 14 | 15 | HttpStatusCode StatusCode { get; } 16 | 17 | string Content { get; } 18 | 19 | T Data { get; } 20 | } 21 | } 22 | 23 | -------------------------------------------------------------------------------- /model/INestedResults.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | namespace RESTClient { 5 | /// 6 | /// Interface to support table Query with `$inlinecount=allpages` 7 | /// 8 | public interface INestedResults { 9 | } 10 | 11 | public interface INestedResults { 12 | // work-around for WSA 13 | string GetArrayField(); 14 | string GetCountField(); 15 | 16 | void SetArray(T[] array); 17 | void SetCount(uint count); 18 | 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /[Ll]ibrary/ 2 | /[Tt]emp/ 3 | /[Oo]bj/ 4 | /[Bb]uild/ 5 | /[Bb]uilds/ 6 | /Assets/AssetStoreTools* 7 | 8 | # Visual Studio 2015 cache directory 9 | /.vs/ 10 | 11 | # Autogenerated VS/MD/Consulo solution and project files 12 | ExportedObj/ 13 | .consulo/ 14 | *.csproj 15 | *.unityproj 16 | *.sln 17 | *.suo 18 | *.tmp 19 | *.user 20 | *.userprefs 21 | *.pidb 22 | *.booproj 23 | *.svd 24 | *.pdb 25 | 26 | # Unity3D generated meta files 27 | *.pidb.meta 28 | *.meta 29 | 30 | # Unity3D Generated File On Crash Reports 31 | sysinfo.txt 32 | 33 | # Builds 34 | *.apk 35 | *.unitypackage 36 | 37 | # IDEs 38 | /.vscode 39 | omnisharp.json 40 | .editorconfig 41 | -------------------------------------------------------------------------------- /model/NestedResults.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System; 5 | 6 | namespace RESTClient { 7 | /// 8 | /// Wrap your data model with this object to call the table Query with `$inlinecount=allpages` param. 9 | /// 10 | [Serializable] 11 | public sealed class NestedResults : INestedResults { 12 | public uint count; 13 | public T[] results; 14 | 15 | // WSA work-around 16 | public void SetArray(T[] array) { 17 | this.results = array; 18 | } 19 | 20 | public void SetCount(uint count) { 21 | this.count = count; 22 | } 23 | 24 | public string GetArrayField() { 25 | return "results"; 26 | } 27 | 28 | public string GetCountField() { 29 | return "count"; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Unity3dAzure 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /http/QueryParams.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System; 5 | using System.Collections; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Text; 9 | using UnityEngine; 10 | 11 | namespace RESTClient { 12 | public class QueryParams { 13 | private Dictionary parameters; 14 | 15 | public QueryParams() { 16 | parameters = new Dictionary(); 17 | } 18 | 19 | public void AddParam(string key, string value) { 20 | parameters.Add(key, value); 21 | } 22 | 23 | public override string ToString() { 24 | if (parameters.Count == 0) { 25 | return ""; 26 | } 27 | StringBuilder sb = new StringBuilder("?"); 28 | foreach (KeyValuePair param in parameters) { 29 | string key = WWW.EscapeURL(param.Key); 30 | string value = WWW.EscapeURL(param.Value); 31 | sb.Append(key + "=" + value + "&"); 32 | } 33 | sb.Remove(sb.Length - 1, 1); 34 | return sb.ToString(); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /helpers/ReflectionHelper.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System.Reflection; 5 | 6 | namespace RESTClient { 7 | /// 8 | /// Helper methods to check and get object properties 9 | /// 10 | public static class ReflectionHelper { 11 | public static bool HasProperty(object obj, string propertyName) { 12 | return GetProperty(obj, propertyName) != null; 13 | } 14 | 15 | public static PropertyInfo GetProperty(object obj, string propertyName) { 16 | #if NETFX_CORE 17 | return obj.GetType().GetTypeInfo().GetDeclaredProperty(propertyName); // GetProperty for UWP 18 | #else 19 | return obj.GetType().GetProperty(propertyName); 20 | #endif 21 | } 22 | 23 | public static bool HasField(object obj, string fieldName) { 24 | return GetField(obj, fieldName) != null; 25 | } 26 | 27 | public static FieldInfo GetField(object obj, string fieldName) { 28 | #if NETFX_CORE 29 | return obj.GetType().GetTypeInfo().GetDeclaredField(fieldName); // GetField for UWP 30 | #else 31 | return obj.GetType().GetField(fieldName); 32 | #endif 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /helpers/SignatureHelper.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Text; 8 | using System.Security.Cryptography; 9 | 10 | #if NETFX_CORE 11 | using Windows.Security.Cryptography.Core; 12 | using Windows.Security.Cryptography; 13 | using System.Runtime.InteropServices.WindowsRuntime; 14 | #endif 15 | 16 | namespace RESTClient { 17 | public static class SignatureHelper { 18 | public static string Sign(byte[] key, string stringToSign) { 19 | #if NETFX_CORE 20 | MacAlgorithmProvider provider = MacAlgorithmProvider.OpenAlgorithm(MacAlgorithmNames.HmacSha256); 21 | CryptographicHash hash = provider.CreateHash(key.AsBuffer()); 22 | hash.Append(CryptographicBuffer.ConvertStringToBinary(stringToSign, BinaryStringEncoding.Utf8)); 23 | return CryptographicBuffer.EncodeToBase64String( hash.GetValueAndReset() ); 24 | #else 25 | var hmac = new HMACSHA256(); 26 | hmac.Key = key; 27 | byte[] sig = hmac.ComputeHash(Encoding.UTF8.GetBytes(stringToSign)); 28 | return Convert.ToBase64String(sig); 29 | #endif 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /helpers/XmlHelper.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System; 5 | using UnityEngine; 6 | using System.Xml; 7 | using System.Xml.Serialization; 8 | using System.IO; 9 | using System.Text; 10 | 11 | namespace RESTClient { 12 | public static class XmlHelper { 13 | public static XmlDocument LoadResourceDocument(string filename) { 14 | string xml = LoadResourceText(filename); 15 | XmlDocument doc = new XmlDocument(); 16 | doc.LoadXml(xml); 17 | return doc; 18 | } 19 | 20 | public static string LoadResourceText(string filename) { 21 | TextAsset contents = (TextAsset)Resources.Load(filename); 22 | return contents.text; 23 | } 24 | 25 | public static string LoadAsset(string filepath, string extension = ".xml") { 26 | string path = Path.Combine(Application.dataPath, filepath + extension); 27 | return File.ReadAllText(path); 28 | } 29 | 30 | public static T FromXml(string xml) { 31 | XmlSerializer serializer = new XmlSerializer(typeof(T)); 32 | using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(xml))) { 33 | return (T)serializer.Deserialize(stream); 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /helpers/UrlHelper.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System; 5 | using System.Text; 6 | using System.Collections.Generic; 7 | 8 | namespace RESTClient { 9 | public static class UrlHelper { 10 | 11 | /// 12 | /// The returned url format will be: baseUrl + path(s) + query string 13 | /// 14 | /// The URL. 15 | /// Base URL. 16 | /// Query parameters. 17 | /// Paths. 18 | public static string BuildQuery(string baseUrl, Dictionary queryParams = null, params string[] paths) { 19 | StringBuilder q = new StringBuilder(); 20 | if (queryParams == null) { 21 | return BuildQuery(baseUrl, "", paths); 22 | } 23 | 24 | foreach (KeyValuePair param in queryParams) { 25 | if (q.Length == 0) { 26 | q.Append("?"); 27 | } else { 28 | q.Append("&"); 29 | } 30 | q.Append(param.Key + "=" + param.Value); 31 | } 32 | 33 | return BuildQuery(baseUrl, q.ToString(), paths); 34 | } 35 | 36 | public static string BuildQuery(string baseUrl, string queryString, params string[] paths) { 37 | StringBuilder sb = new StringBuilder(); 38 | sb.Append(baseUrl); 39 | if (!baseUrl.EndsWith("/")) { 40 | sb.Append("/"); 41 | } 42 | 43 | foreach (string path in paths) { 44 | if (!path.EndsWith("/")) { 45 | sb.Append(path); 46 | } else { 47 | sb.Append(path + "/"); 48 | } 49 | } 50 | 51 | sb.Append(queryString); 52 | return sb.ToString(); 53 | } 54 | } 55 | } 56 | 57 | -------------------------------------------------------------------------------- /RestClient.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using UnityEngine; 5 | using System.Collections; 6 | using System.Collections.Generic; 7 | using System; 8 | using System.Text.RegularExpressions; 9 | 10 | 11 | #if !NETFX_CORE || UNITY_ANDROID 12 | using System.Net; 13 | using System.Security.Cryptography.X509Certificates; 14 | using System.Net.Security; 15 | #endif 16 | 17 | namespace RESTClient { 18 | public class RestClient { 19 | public string Url { get; private set; } 20 | 21 | /// 22 | /// Creates a new REST Client 23 | /// 24 | public RestClient(string url, bool forceHttps = false) { 25 | Url = forceHttps ? HttpsUri(url) : url; 26 | // required for running in Windows and Android 27 | #if !NETFX_CORE || UNITY_ANDROID 28 | ServicePointManager.ServerCertificateValidationCallback = RemoteCertificateValidationCallback; 29 | #endif 30 | } 31 | 32 | public override string ToString() { 33 | return this.Url; 34 | } 35 | 36 | /// 37 | /// Changes 'http' to be 'https' instead 38 | /// 39 | private static string HttpsUri(string appUrl) { 40 | return Regex.Replace(appUrl, "(?si)^http://", "https://").TrimEnd('/'); 41 | } 42 | 43 | private static string DomainName(string url) { 44 | var match = Regex.Match(url, @"^(https:\/\/|http:\/\/)(www\.)?([a-z0-9-_]+\.[a-z]+)", RegexOptions.IgnoreCase); 45 | if (match.Groups.Count == 4 && match.Groups[3].Value.Length > 0) { 46 | return match.Groups[3].Value; 47 | } 48 | return url; 49 | } 50 | 51 | #if !NETFX_CORE || UNITY_ANDROID 52 | private bool RemoteCertificateValidationCallback(System.Object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) { 53 | // Check the certificate to see if it was issued from host 54 | if (certificate.Subject.Contains(DomainName(Url))) { 55 | return true; 56 | } else { 57 | return false; 58 | } 59 | } 60 | #endif 61 | 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /http/RestResponse.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System.Net; 5 | 6 | namespace RESTClient { 7 | public abstract class Response { 8 | public bool IsError { get; set; } 9 | 10 | public string ErrorMessage { get; set; } 11 | 12 | public string Url { get; set; } 13 | 14 | public HttpStatusCode StatusCode { get; set; } 15 | 16 | public string Content { get; set; } 17 | 18 | protected Response(HttpStatusCode statusCode) { 19 | this.StatusCode = statusCode; 20 | this.IsError = !((int)statusCode >= 200 && (int)statusCode < 300); 21 | } 22 | 23 | // success 24 | protected Response(HttpStatusCode statusCode, string url, string text) { 25 | this.IsError = false; 26 | this.Url = url; 27 | this.ErrorMessage = null; 28 | this.StatusCode = statusCode; 29 | this.Content = text; 30 | } 31 | 32 | // failure 33 | protected Response(string error, HttpStatusCode statusCode, string url, string text) { 34 | this.IsError = true; 35 | this.Url = url; 36 | this.ErrorMessage = error; 37 | this.StatusCode = statusCode; 38 | this.Content = text; 39 | } 40 | } 41 | 42 | public sealed class RestResponse : Response { 43 | // success 44 | public RestResponse(HttpStatusCode statusCode, string url, string text) : base(statusCode, url, text) { 45 | } 46 | 47 | // failure 48 | public RestResponse(string error, HttpStatusCode statusCode, string url, string text) : base(error, statusCode, url, text) { 49 | } 50 | } 51 | 52 | public sealed class RestResponse : Response, IRestResponse { 53 | public T Data { get; set; } 54 | 55 | // success 56 | public RestResponse(HttpStatusCode statusCode, string url, string text, T data) : base(statusCode, url, text) { 57 | this.Data = data; 58 | } 59 | public RestResponse(HttpStatusCode statusCode, string url, string text) : base(statusCode, url, text) { 60 | } 61 | 62 | // failure 63 | public RestResponse(string error, HttpStatusCode statusCode, string url, string text) : base(error, statusCode, url, text) { 64 | } 65 | } 66 | 67 | /// 68 | /// Parsed JSON result could either be an object or an array of objects 69 | /// 70 | internal sealed class RestResult : Response { 71 | public T AnObject { get; set; } 72 | 73 | public T[] AnArrayOfObjects { get; set; } 74 | 75 | public RestResult(HttpStatusCode statusCode) : base(statusCode) { 76 | } 77 | } 78 | 79 | internal sealed class RestResult : Response { 80 | public RestResult(HttpStatusCode statusCode) : base(statusCode) { 81 | } 82 | } 83 | 84 | } 85 | 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # REST Client for Unity 2 | For Unity developers looking to use REST Services in their Unity game / app. 3 | 4 | **RESTClient** for Unity is built on top of [UnityWebRequest](https://docs.unity3d.com/Manual/UnityWebRequest.html) and Unity's [JsonUtility](https://docs.unity3d.com/ScriptReference/JsonUtility.html) to make it easier to compose REST requests and return the results serialized as native C# data model objects. 5 | 6 | ## Features 7 | - Methods to add request body, headers and query strings to REST requests. 8 | - Ability to return result as native object or as plain text. 9 | - Work around for nested arrays. 10 | - Work around for parsing abstract types on UWP. 11 | 12 | ## How do I use this with cloud services? 13 | Checkout the following projects for Unity which were built using this REST Client library as examples. 14 | - [Azure App Services](https://github.com/Unity3dAzure/AppServices) 15 | - [Azure Blob Storage](https://github.com/Unity3dAzure/StorageServices) 16 | - [Azure Functions](https://github.com/Unity3dAzure/AzureFunctions) 17 | - [Nether (serverless)](https://github.com/MicrosoftDX/nether/tree/serverless/src/Client/Unity) 18 | 19 | ## Example Usage 20 | This snippet shows how to **POST** a REST request to a new [Azure Function](https://azure.microsoft.com/en-gb/services/functions/) HTTP Trigger "hello" sample function: 21 | 22 | ``` 23 | using RESTClient; 24 | using System; 25 | ``` 26 | 27 | ``` 28 | public class RESTClientExample : MonoBehaviour { 29 | 30 | private string url = "https://***.azurewebsites.net/api/hello"; // Azure Function API endpoint 31 | private string code = "***"; // Azure Function code 32 | 33 | void Start () { 34 | StartCoroutine( SayHello(SayHelloCompleted) ); 35 | } 36 | 37 | private IEnumerator SayHello(Action> callback = null) { 38 | RestRequest request = new RestRequest(url, Method.POST); 39 | request.AddHeader("Content-Type", "application/json"); 40 | request.AddQueryParam("code", code); 41 | request.AddBody("{\"name\": \"unity\"}"); 42 | yield return request.Request.Send(); 43 | request.GetText(callback); 44 | } 45 | 46 | private void SayHelloCompleted(IRestResponse response) { 47 | if (response.IsError) { 48 | Debug.LogError("Request error: " + response.StatusCode); 49 | return; 50 | } 51 | Debug.Log("Completed: " + response.Content); 52 | } 53 | 54 | } 55 | ``` 56 | 57 | ## Requirements 58 | Requires Unity v5.3 or greater as [UnityWebRequest](https://docs.unity3d.com/Manual/UnityWebRequest.html) and [JsonUtility](https://docs.unity3d.com/ScriptReference/JsonUtility.html) features are used. Unity will be extending platform support for UnityWebRequest so keep Unity up to date if you need to support these additional platforms. 59 | 60 | ## Supported platforms 61 | Intended to work on all the platforms [UnityWebRequest](https://docs.unity3d.com/Manual/UnityWebRequest.html) supports including: 62 | * Unity Editor (Mac/PC) and Standalone players 63 | * iOS 64 | * Android 65 | * Windows 10 (UWP) 66 | 67 | ## Troubleshooting 68 | - Remember to wrap async calls with [`StartCoroutine()`](https://docs.unity3d.com/ScriptReference/MonoBehaviour.StartCoroutine.html) 69 | - Before building for a target platform remember to check the **Internet Client capability** is enabled in the Unity player settings, otherwise all REST calls will fail with a '-1' status error. 70 | 71 | Questions or tweet [@deadlyfingers](https://twitter.com/deadlyfingers) 72 | -------------------------------------------------------------------------------- /helpers/JsonHelper.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using UnityEngine; 5 | using System; 6 | using System.Text.RegularExpressions; 7 | #if NETFX_CORE 8 | using Windows.Data.Json; 9 | using System.Collections.Generic; 10 | using System.Linq; 11 | #endif 12 | 13 | namespace RESTClient { 14 | /// 15 | /// Wrapper work-around for json array 16 | /// Issue reference: https://forum.unity3d.com/threads/how-to-load-an-array-with-jsonutility.375735/ 17 | /// 18 | #pragma warning disable 0649 // suppresses warning: array "is never assigned to, and will always have its default value 'null'" 19 | [Serializable] 20 | internal class Wrapper { 21 | 22 | public T[] array; 23 | } 24 | 25 | public static class JsonHelper { 26 | /// 27 | /// Work-around to parse json array 28 | /// 29 | public static T[] FromJsonArray(string json) { 30 | // Work-around for JsonUtility array serialization issues in Windows Store Apps. 31 | #if NETFX_CORE 32 | JsonArray jsonArray = new JsonArray(); 33 | if (JsonArray.TryParse(json, out jsonArray)) { 34 | return GetArray(jsonArray); 35 | } 36 | Debug.LogWarning("Failed to parse json array of type:" + typeof(T).ToString() ); 37 | return default(T[]); 38 | #endif 39 | string newJson = "{\"array\":" + json + "}"; 40 | Wrapper wrapper = new Wrapper(); 41 | try { 42 | wrapper = JsonUtility.FromJson>(newJson); 43 | } catch (Exception e) { 44 | Debug.LogWarning("Failed to parse json array of type:" + typeof(T).ToString() + " Exception message: " + e.Message); 45 | return default(T[]); 46 | } 47 | return wrapper.array; 48 | } 49 | 50 | public static N FromJsonNestedArray(string json, string namedArray) where N : INestedResults, new() { 51 | #if NETFX_CORE 52 | JsonObject jsonObject = new JsonObject(); 53 | if (JsonObject.TryParse(json, out jsonObject)) { 54 | JsonArray jsonArray = jsonObject.GetNamedArray(namedArray); 55 | T[] array = GetArray(jsonArray); 56 | N nestedResults = new N(); 57 | nestedResults.SetArray(array); 58 | 59 | string namedCount = nestedResults.GetCountField(); 60 | uint count = Convert.ToUInt32( jsonObject.GetNamedNumber(namedCount) ); 61 | nestedResults.SetCount(count); 62 | 63 | return nestedResults; 64 | } else { 65 | Debug.LogWarning("Failed to parse json nested array of type:" + typeof(T).ToString()); 66 | return default(N); 67 | } 68 | #endif 69 | N results = JsonUtility.FromJson(json); 70 | return results; 71 | } 72 | 73 | #if NETFX_CORE 74 | private static T[] GetArray(JsonArray array) 75 | { 76 | List list = new List(); 77 | foreach (var x in array) { 78 | try { 79 | T item = JsonUtility.FromJson(x.ToString()); 80 | list.Add(item); 81 | } catch (Exception e) { 82 | Debug.LogWarning("Failed to parse json of type:" + typeof(T).ToString() + " Exception message: " + e.Message + " json:'" + x.ToString() + "'"); 83 | } 84 | } 85 | return list.ToArray(); 86 | } 87 | #endif 88 | 89 | /// 90 | /// Workaround to only exclude Data Model's read only system properties being returned as json object. Unfortunately there is no JsonUtil attribute to do this as [NonSerialized] will just ignore the properties completely (both in and out). 91 | /// 92 | public static string ToJsonExcludingSystemProperties(object obj) { 93 | string jsonString = JsonUtility.ToJson(obj); 94 | return Regex.Replace(jsonString, "(?i)(\\\"id\\\":\\\"\\\",)?(\\\"createdAt\\\":\\\"[0-9TZ:.-]*\\\",)?(\\\"updatedAt\\\":\\\"[0-9TZ:.-]*\\\",)?(\\\"version\\\":\\\"[A-Z0-9=]*\\\",)?(\\\"deleted\\\":(true|false),)?(\\\"ROW_NUMBER\\\":\\\"[0-9]*\\\",)?", ""); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /http/RestRequest.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using UnityEngine; 5 | using UnityEngine.Networking; 6 | using System; 7 | using System.Net; 8 | using System.Collections.Generic; 9 | using System.Text.RegularExpressions; 10 | using System.Text; 11 | 12 | namespace RESTClient { 13 | public class RestRequest : IDisposable { 14 | public UnityWebRequest Request { get; private set; } 15 | 16 | private QueryParams queryParams; 17 | 18 | public RestRequest(UnityWebRequest request) { 19 | this.Request = request; 20 | } 21 | 22 | public RestRequest(string url, Method method) { 23 | Request = new UnityWebRequest(url, method.ToString()); 24 | Request.downloadHandler = new DownloadHandlerBuffer(); 25 | } 26 | 27 | public void AddHeader(string key, string value) { 28 | Request.SetRequestHeader(key, value); 29 | } 30 | 31 | public void AddHeaders(Dictionary headers) { 32 | foreach (KeyValuePair header in headers) { 33 | AddHeader(header.Key, header.Value); 34 | } 35 | } 36 | 37 | public void AddBody(string text, string contentType = "text/plain; charset=UTF-8") { 38 | byte[] bytes = Encoding.UTF8.GetBytes(text); 39 | this.AddBody(bytes, contentType, false); 40 | } 41 | 42 | public void AddBody(byte[] bytes, string contentType) { 43 | this.AddBody(bytes, contentType, false); 44 | } 45 | 46 | public void AddBody(byte[] bytes, string contentType, bool isChunked) { 47 | if (Request.uploadHandler != null) { 48 | Debug.LogWarning("Request body can only be set once"); 49 | return; 50 | } 51 | Request.chunkedTransfer = isChunked; 52 | Request.uploadHandler = new UploadHandlerRaw(bytes); 53 | Request.uploadHandler.contentType = contentType; 54 | } 55 | 56 | public virtual void AddBody(T data, string contentType = "application/json; charset=utf-8") { 57 | if (typeof(T) == typeof(string)) { 58 | this.AddBody(data.ToString(), contentType); 59 | return; 60 | } 61 | string jsonString = JsonUtility.ToJson(data); 62 | byte[] bytes = Encoding.UTF8.GetBytes(jsonString); 63 | this.AddBody(bytes, contentType, false); 64 | } 65 | 66 | public virtual void AddQueryParam(string key, string value, bool shouldUpdateRequestUrl = false) { 67 | if (queryParams == null) { 68 | queryParams = new QueryParams(); 69 | } 70 | queryParams.AddParam(key, value); 71 | if (shouldUpdateRequestUrl) { 72 | UpdateRequestUrl(); 73 | } 74 | } 75 | 76 | public void SetQueryParams(QueryParams queryParams) { 77 | if (this.queryParams != null) { 78 | Debug.LogWarning("Replacing previous query params"); 79 | } 80 | this.queryParams = queryParams; 81 | } 82 | 83 | public virtual void UpdateRequestUrl() { 84 | if (queryParams == null) { 85 | return; 86 | } 87 | var match = Regex.Match(Request.url, @"^(.+)(\\?)(.+)", RegexOptions.IgnoreCase); 88 | if (match.Groups.Count == 4 && match.Groups[0].Value.Length > 0) { 89 | string url = match.Groups[0].Value + queryParams.ToString(); 90 | Request.url = url; 91 | } 92 | } 93 | 94 | #region Response and object parsing 95 | 96 | private RestResult GetRestResult(bool expectedBodyContent = true) { 97 | HttpStatusCode statusCode = (HttpStatusCode)Enum.Parse(typeof(HttpStatusCode), Request.responseCode.ToString()); 98 | RestResult result = new RestResult(statusCode); 99 | 100 | if (result.IsError) { 101 | result.ErrorMessage = "Response failed with status: " + statusCode.ToString(); 102 | return result; 103 | } 104 | 105 | if (expectedBodyContent && string.IsNullOrEmpty(Request.downloadHandler.text)) { 106 | result.IsError = true; 107 | result.ErrorMessage = "Response has empty body"; 108 | return result; 109 | } 110 | return result; 111 | } 112 | 113 | private RestResult GetRestResult() { 114 | HttpStatusCode statusCode = (HttpStatusCode)Enum.Parse(typeof(HttpStatusCode), Request.responseCode.ToString()); 115 | RestResult result = new RestResult(statusCode); 116 | 117 | if (result.IsError) { 118 | result.ErrorMessage = "Response failed with status: " + statusCode.ToString(); 119 | return result; 120 | } 121 | 122 | if (string.IsNullOrEmpty(Request.downloadHandler.text)) { 123 | result.IsError = true; 124 | result.ErrorMessage = "Response has empty body"; 125 | return result; 126 | } 127 | 128 | return result; 129 | } 130 | 131 | #endregion 132 | 133 | #region JSON object parsing response 134 | 135 | /// 136 | /// Shared method to return response result whether an object or array of objects 137 | /// 138 | private RestResult TryParseJsonArray() { 139 | RestResult result = GetRestResult(); 140 | // try parse an array of objects 141 | try { 142 | result.AnArrayOfObjects = JsonHelper.FromJsonArray(Request.downloadHandler.text); 143 | } catch (Exception e) { 144 | result.IsError = true; 145 | result.ErrorMessage = "Failed to parse an array of objects of type: " + typeof(T).ToString() + " Exception message: " + e.Message; 146 | } 147 | return result; 148 | } 149 | 150 | private RestResult TryParseJson() { 151 | RestResult result = GetRestResult(); 152 | // try parse an object 153 | try { 154 | result.AnObject = JsonUtility.FromJson(Request.downloadHandler.text); 155 | } catch (Exception e) { 156 | result.IsError = true; 157 | result.ErrorMessage = "Failed to parse object of type: " + typeof(T).ToString() + " Exception message: " + e.Message; 158 | } 159 | return result; 160 | } 161 | 162 | /// 163 | /// Parses object with T data = JsonUtil.FromJson, then callback RestResponse 164 | /// 165 | public IRestResponse ParseJson(Action> callback = null) { 166 | RestResult result = TryParseJson(); 167 | RestResponse response; 168 | if (result.IsError) { 169 | Debug.LogWarning("Response error status:" + result.StatusCode + " code:" + Request.responseCode + " error:" + result.ErrorMessage + " Request url:" + Request.url); 170 | response = new RestResponse(result.ErrorMessage, result.StatusCode, Request.url, Request.downloadHandler.text); 171 | } else { 172 | response = new RestResponse(result.StatusCode, Request.url, Request.downloadHandler.text, result.AnObject); 173 | } 174 | if (callback != null) { 175 | callback(response); 176 | } 177 | this.Dispose(); 178 | return response; 179 | } 180 | 181 | /// 182 | /// Parses array of objects with T[] data = JsonHelper.GetJsonArray, then callback RestResponse 183 | /// 184 | public IRestResponse ParseJsonArray(Action> callback = null) { 185 | RestResult result = TryParseJsonArray(); 186 | RestResponse response; 187 | if (result.IsError) { 188 | Debug.LogWarning("Response error status:" + result.StatusCode + " code:" + Request.responseCode + " error:" + result.ErrorMessage + " Request url:" + Request.url); 189 | response = new RestResponse(result.ErrorMessage, result.StatusCode, Request.url, Request.downloadHandler.text); 190 | } else { 191 | response = new RestResponse(result.StatusCode, Request.url, Request.downloadHandler.text, result.AnArrayOfObjects); 192 | } 193 | if (callback != null) { 194 | callback(response); 195 | } 196 | this.Dispose(); 197 | return response; 198 | } 199 | 200 | private RestResult TryParseJsonNestedArray(string namedArray) where N : INestedResults, new() { 201 | RestResult result = GetRestResult(); 202 | // try parse an object 203 | try { 204 | result.AnObject = JsonHelper.FromJsonNestedArray(Request.downloadHandler.text, namedArray); //JsonUtility.FromJson(request.downloadHandler.text); 205 | } catch (Exception e) { 206 | result.IsError = true; 207 | result.ErrorMessage = "Failed to parse object of type: " + typeof(N).ToString() + " Exception message: " + e.Message; 208 | } 209 | return result; 210 | } 211 | 212 | /// Work-around for nested array 213 | public RestResponse ParseJsonNestedArray(string namedArray, Action> callback = null) where N : INestedResults, new() { 214 | RestResult result = TryParseJsonNestedArray(namedArray); 215 | RestResponse response; 216 | if (result.IsError) { 217 | Debug.LogWarning("Response error status:" + result.StatusCode + " code:" + Request.responseCode + " error:" + result.ErrorMessage + " request url:" + Request.url); 218 | response = new RestResponse(result.ErrorMessage, result.StatusCode, Request.url, Request.downloadHandler.text); 219 | } else { 220 | response = new RestResponse(result.StatusCode, Request.url, Request.downloadHandler.text, result.AnObject); 221 | } 222 | if (callback != null) { 223 | callback(response); 224 | } 225 | this.Dispose(); 226 | return response; 227 | } 228 | 229 | #endregion 230 | 231 | #region XML object parsing response 232 | 233 | private RestResult TrySerializeXml() { 234 | RestResult result = GetRestResult(); 235 | // return early if there was a status / data error other than Forbidden 236 | if (result.IsError && result.StatusCode == HttpStatusCode.Forbidden) { 237 | Debug.LogWarning("Authentication Failed: " + Request.downloadHandler.text); 238 | return result; 239 | } else if (result.IsError) { 240 | return result; 241 | } 242 | // otherwise try and serialize XML response text to an object 243 | try { 244 | result.AnObject = XmlHelper.FromXml(Request.downloadHandler.text); 245 | } catch (Exception e) { 246 | result.IsError = true; 247 | result.ErrorMessage = "Failed to parse object of type: " + typeof(T).ToString() + " Exception message: " + e.Message; 248 | } 249 | return result; 250 | } 251 | 252 | public RestResponse ParseXML(Action> callback = null) { 253 | RestResult result = TrySerializeXml(); 254 | RestResponse response; 255 | if (result.IsError) { 256 | Debug.LogWarning("Response error status:" + result.StatusCode + " code:" + Request.responseCode + " error:" + result.ErrorMessage + " request url:" + Request.url); 257 | response = new RestResponse(result.ErrorMessage, result.StatusCode, Request.url, Request.downloadHandler.text); 258 | } else { 259 | response = new RestResponse(result.StatusCode, Request.url, Request.downloadHandler.text, result.AnObject); 260 | } 261 | if (callback != null) { 262 | callback(response); 263 | } 264 | this.Dispose(); 265 | return response; 266 | } 267 | 268 | /// 269 | /// To be used with a callback which passes the response with result including status success or error code, request url and any body text. 270 | /// 271 | /// Callback. 272 | public RestResponse Result(Action callback = null) { 273 | return GetText(callback, false); 274 | } 275 | 276 | public RestResponse GetText(Action callback = null, bool expectedBodyContent = true) { 277 | RestResult result = GetRestResult(expectedBodyContent); 278 | RestResponse response; 279 | if (result.IsError) { 280 | Debug.LogWarning("Response error status:" + result.StatusCode + " code:" + Request.responseCode + " error:" + result.ErrorMessage + " request url:" + Request.url); 281 | response = new RestResponse(result.ErrorMessage, result.StatusCode, Request.url, Request.downloadHandler.text); 282 | } else { 283 | response = new RestResponse(result.StatusCode, Request.url, Request.downloadHandler.text); 284 | } 285 | if (callback != null) { 286 | callback(response); 287 | } 288 | this.Dispose(); 289 | return response; 290 | } 291 | 292 | #endregion 293 | 294 | /// 295 | /// Return response body as plain text. 296 | /// 297 | /// Callback with response type: IRestResponse 298 | public IRestResponse GetText(Action> callback = null) { 299 | RestResult result = GetRestResult(); 300 | RestResponse response; 301 | if (result.IsError) { 302 | response = new RestResponse(result.ErrorMessage, result.StatusCode, Request.url, Request.downloadHandler.text); 303 | } else { 304 | response = new RestResponse(result.StatusCode, Request.url, Request.downloadHandler.text); 305 | } 306 | if (callback != null) { 307 | callback(response); 308 | } 309 | this.Dispose(); 310 | return response; 311 | } 312 | 313 | /// 314 | /// Return response body as bytes. 315 | /// 316 | /// Callback with response type: IRestResponse 317 | public IRestResponse GetBytes(Action> callback = null) { 318 | RestResult result = GetRestResult(false); 319 | RestResponse response; 320 | if (result.IsError) { 321 | response = new RestResponse(result.ErrorMessage, result.StatusCode, Request.url, Request.downloadHandler.text); 322 | } else { 323 | response = new RestResponse(result.StatusCode, Request.url, null, Request.downloadHandler.data); 324 | } 325 | if (callback != null) { 326 | callback(response); 327 | } 328 | this.Dispose(); 329 | return response; 330 | } 331 | 332 | #region Handle native asset UnityWebRequest.Get... requests 333 | 334 | public IRestResponse GetTexture(Action> callback = null) { 335 | RestResult result = GetRestResult(false); 336 | RestResponse response; 337 | if (result.IsError) { 338 | response = new RestResponse(result.ErrorMessage, result.StatusCode, Request.url, Request.downloadHandler.text); 339 | } else { 340 | Texture texture = ((DownloadHandlerTexture)Request.downloadHandler).texture; 341 | response = new RestResponse(result.StatusCode, Request.url, null, texture); 342 | } 343 | if (callback != null) { 344 | callback(response); 345 | } 346 | this.Dispose(); 347 | return response; 348 | } 349 | 350 | public IRestResponse GetAudioClip(Action> callback = null) { 351 | RestResult result = GetRestResult(false); 352 | RestResponse response; 353 | if (result.IsError) { 354 | response = new RestResponse(result.ErrorMessage, result.StatusCode, Request.url, Request.downloadHandler.text); 355 | } else { 356 | AudioClip audioClip = ((DownloadHandlerAudioClip)Request.downloadHandler).audioClip; 357 | response = new RestResponse(result.StatusCode, Request.url, null, audioClip); 358 | } 359 | if (callback != null) { 360 | callback(response); 361 | } 362 | this.Dispose(); 363 | return response; 364 | } 365 | 366 | public IRestResponse GetAssetBundle(Action> callback = null) { 367 | RestResult result = GetRestResult(false); 368 | RestResponse response; 369 | if (result.IsError) { 370 | response = new RestResponse(result.ErrorMessage, result.StatusCode, Request.url, Request.downloadHandler.text); 371 | } else { 372 | AssetBundle assetBundle = ((DownloadHandlerAssetBundle)Request.downloadHandler).assetBundle; 373 | response = new RestResponse(result.StatusCode, Request.url, null, assetBundle); 374 | } 375 | if (callback != null) { 376 | callback(response); 377 | } 378 | this.Dispose(); 379 | return response; 380 | } 381 | 382 | #endregion 383 | 384 | public UnityWebRequestAsyncOperation Send() { 385 | #if UNITY_2017_2_OR_NEWER 386 | return Request.SendWebRequest(); 387 | #else 388 | return Request.Send(); 389 | #endif 390 | } 391 | 392 | public void Dispose() { 393 | Request.Dispose(); // Request completed, clean-up resources 394 | } 395 | 396 | } 397 | } 398 | --------------------------------------------------------------------------------