diff --git a/README.md b/README.md index 727e076..387b2ef 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,17 @@ # Azure App Services for Unity3d -For game developers looking to use Azure App Services (previously Mobile Services) in their Unity project. +For game developers looking to use Azure App Services (previously Mobile Services) in their Unity project. This App Services REST API library for Unity is quite similar in structure to the Azure Mobile Services SDK so if you've used that before you should feel relatively at home. + +## Prerequisites +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. ## How to setup App Services with a new Unity project -1. [Download UnityRestClient](https://github.com/ProjectStratus/UnityRestClient/archive/master.zip) - * Copy JsonFx 'plugins' and the UnityRestClient 'Source' into project `Assets` folder -2. [Download AppServices](https://github.com/Unity3dAzure/AppServices/archive/master.zip) +1. [Download AppServices](https://github.com/Unity3dAzure/AppServices/archive/master.zip) * Copy 'AppServices' into project `Assets` folder. -3. Create an Azure App Service [Mobile App](https://portal.azure.com) +2. Create an Azure App Service [Mobile App](https://portal.azure.com) * Create a Table (using Easy Tables) for app data. ## Azure App Services Demos for Unity 5 -Try the [Azure App Services Demos](https://github.com/Unity3dAzure/AppServicesDemo) project for Unity 5 on Mac / Windows. (The demo project has got everything already bundled in and does not require any additional assets to work. Just wire it up with your [Azure App Service](https://portal.azure.com) and run it right inside the Unity Editor.) +Try the [Azure App Services Demos](https://github.com/Unity3dAzure/AppServicesDemo) project for Unity v5.4.x on Mac / Windows. (The demo project has got everything already bundled in and does not require any additional assets to work. Just wire it up with your [Azure App Service](https://portal.azure.com) and run it right inside the Unity Editor.) For detailed instructions read my developer blog on [how to setup Azure App Services and Unity demo project](http://www.deadlyfingers.net/azure/azure-app-services-for-unity3d/). ## Supported Features @@ -30,18 +31,20 @@ Delete | Delete an item using id property. Query | Get a list of results using a custom query. Lookup | Get an item’s data using id property. -### MobileServiceClient Interface - void Login(MobileServiceAuthenticationProvider provider, string token, Action> callback = null); - void InvokeApi(string apiName, Action> callback = null) where T : new(); +### [MobileServiceClient Interface](https://github.com/Unity3dAzure/AppServices/blob/master/client/IAzureMobileServiceClient.cs) + Login(MobileServiceAuthenticationProvider provider, string token, Action> callback = null); + InvokeApi(string apiName, Action> callback = null) where T : new(); + InvokeApi(string apiName, Method httpMethod, Action> callback = null) where T : new(); + InvokeApi(string apiName, Method httpMethod, T body, Action> callback = null) where T : new(); -### MobileServiceTable Interface - void Insert(T item, Action> callback = null) where T : new(); - void Read(Action>> callback = null) where T : new(); - void Update(T item, Action> callback = null) where T : new(); - void Delete(string id, Action> callback = null) where T : new(); - void Query(CustomQuery query, Action>> callback = null) where T : new(); - void Query(CustomQuery query, Action> callback = null) where T : INestedResults, new(); - void Lookup(string id, Action> callback = null) where T : new(); +### [MobileServiceTable Interface](https://github.com/Unity3dAzure/AppServices/blob/master/table/IAzureMobileServiceTable.cs) + Insert(T item, Action> callback = null) where T : new(); + Read(Action> callback = null) where T : new(); + Update(T item, Action> callback = null) where T : new(); + Delete(string id, Action> callback = null) where T : new(); + Query(CustomQuery query, Action> callback = null) where T : new(); + Query(CustomQuery query, Action> callback = null) where T : INestedResults, new(); + Lookup(string id, Action> callback = null) where T : new(); ## Sample usage ``` @@ -49,8 +52,6 @@ using UnityEngine; using System; using System.Net; using System.Collections.Generic; -using RestSharp; -using Pathfinding.Serialization.JsonFx; using Unity3dAzure.AppServices; ``` @@ -68,25 +69,38 @@ void Start () { ``` ``` private void ReadItems() { - _table.Read(OnReadItemsCompleted); + StartCoroutine( _table.Read(OnReadItemsCompleted) ); } -private void OnReadItemsCompleted(IRestResponse> response) { - if ( response.StatusCode == HttpStatusCode.OK) { - Debug.Log("OnReadItemsCompleted data: " + response.Content); - List items = response.Data; - Debug.Log( "Todo items count: " + items.Count); +private void OnReadItemsCompleted(IRestResponse response) { + if (!response.IsError) { + Debug.Log ("OnReadCompleted: " + response.Url + " data: " + response.Content); + TodoItem[] items = response.Data; + Debug.Log ( "Todo items count: " + items.Count); } else { - ResponseError err = JsonReader.Deserialize(response.Content); - Debug.Log("Error " + err.code.ToString() + " " + err.error + " Uri: " + response.ResponseUri); + Debug.LogWarning ("Read Error Status:" + response.StatusCode + " Url: " + response.Url); } } ``` -## Dependencies -The REST service implements [UnityRestClient](https://github.com/ProjectStratus/UnityRestClient) which uses [JsonFx](https://bitbucket.org/TowerOfBricks/jsonfx-for-unity3d-git/) to parse JSON data. +## Known issues +* There is an issue with [PATCH on Android using UnityWebRequest with Azure App Services](http://answers.unity3d.com/questions/1230067/trying-to-use-patch-on-a-unitywebrequest-on-androi.html). Android won't recognize the "PATCH" http method currently required to update an item in App Services. One workaround is to enable the `X-HTTP-Method-Override` header. Here's the quick fix for App Services running node backend: + 1. Install the "method-override" package. + ``` + npm install method-override --save + ``` + 2. In 'app.js' file insert: + ``` + var methodOverride = require('method-override'); + // after the line "var app = express();" add + app.use(methodOverride('X-HTTP-Method-Override')); + ``` + +This will enable PATCH requests to be sent on Android. -## Supports +## Supported platforms +Will work on all the platforms [UnityWebRequest](https://docs.unity3d.com/Manual/UnityWebRequest.html) supports including: +* Unity Editor and Standalone players * iOS * Android * Windows diff --git a/authentication/MobileServiceAuthenticationProvider.cs b/authentication/MobileServiceAuthenticationProvider.cs index bf7c8ed..ede255a 100644 --- a/authentication/MobileServiceAuthenticationProvider.cs +++ b/authentication/MobileServiceAuthenticationProvider.cs @@ -1,11 +1,11 @@ namespace Unity3dAzure.AppServices { - public enum MobileServiceAuthenticationProvider - { - MicrosoftAccount = 0, - Google = 1, - Twitter = 2, - Facebook = 3, - WindowsAzureActiveDirectory = 4 - } + public enum MobileServiceAuthenticationProvider + { + MicrosoftAccount = 0, + Google = 1, + Twitter = 2, + Facebook = 3, + WindowsAzureActiveDirectory = 4 + } } \ No newline at end of file diff --git a/client/IAzureMobileServiceClient.cs b/client/IAzureMobileServiceClient.cs index bcceb4a..6d0de2b 100644 --- a/client/IAzureMobileServiceClient.cs +++ b/client/IAzureMobileServiceClient.cs @@ -1,39 +1,36 @@ -using System.Collections; -using System.Collections.Generic; -using System; -using RestSharp; +using System; +using System.Collections; namespace Unity3dAzure.AppServices { - [CLSCompliant(false)] - public interface IAzureMobileServiceClient - { - MobileServiceTable GetTable(string tableName) where E : class; - - /// - /// Client-directed login (sdk) for single sign on - /// https://msdn.microsoft.com/en-us/library/azure/jj710106.aspx - /// - void Login(MobileServiceAuthenticationProvider provider, string token, Action> callback = null); - - /// - /// TODO: Service-directed login (webview) - /// - //void Login(MobileServiceAuthenticationProvider provider); - - /// - /// Invokes a custom API using GET HTTP method - /// - void InvokeApi(string apiName, Action> callback = null) where T : new(); + public interface IAzureMobileServiceClient + { + MobileServiceTable GetTable (string tableName) where E : class; + + /// + /// Client-directed login (sdk) for single sign on + /// https://msdn.microsoft.com/en-us/library/azure/jj710106.aspx + /// + IEnumerator Login (MobileServiceAuthenticationProvider provider, string token, Action> callback = null); + + /// + /// TODO: Service-directed login (webview) + /// + //void Login(MobileServiceAuthenticationProvider provider); + + /// + /// Invokes a custom API using GET HTTP method + /// + IEnumerator InvokeApi (string apiName, Action> callback = null) where T : new(); /// /// Invokes a custom API for HTTP Methods: GET, POST, PUT, PATCH, DELETE /// - void InvokeApi(string apiName, Method httpMethod, Action> callback = null) where T : new(); + IEnumerator InvokeApi (string apiName, Method httpMethod, Action> callback = null) where T : new(); /// /// Invokes a custom API with body object /// - void InvokeApi (string apiName, Method httpMethod, T body, Action> callback = null) where T : new(); - } + IEnumerator InvokeApi (string apiName, Method httpMethod, T body, Action> callback = null) where T : new(); + } } \ No newline at end of file diff --git a/client/MobileServiceClient.cs b/client/MobileServiceClient.cs index 8c07046..7bdf401 100644 --- a/client/MobileServiceClient.cs +++ b/client/MobileServiceClient.cs @@ -1,6 +1,5 @@ using UnityEngine; using System.Collections; -using RestSharp; using System.Collections.Generic; using System; using System.Text.RegularExpressions; @@ -14,149 +13,115 @@ namespace Unity3dAzure.AppServices { - [CLSCompliant(false)] - public class MobileServiceClient : RestClient, IAzureMobileServiceClient - { - public string AppUrl { get; private set; } - public string AppKey { get; private set; } + public class MobileServiceClient : IAzureMobileServiceClient + { + public string AppUrl { get; private set; } - public MobileServiceUser User { get; set; } + public MobileServiceUser User { get; set; } - public const string URI_API = "api/"; - - /// - /// Creates a new RestClient using Azure Mobile Service's Application Url and App Key - /// NB: Mobile Services should be migrated to use Azure App Service with constructor below. - /// - public MobileServiceClient(string appUrl, string appKey) : base(appUrl) - { - AppUrl = appUrl; - AppKey = appKey; - - // required for running in Windows and Android - #if !NETFX_CORE || UNITY_ANDROID - Debug.Log("ServerCertificateValidation"); - ServicePointManager.ServerCertificateValidationCallback = RemoteCertificateValidationCallback; - #endif - } - - /// - /// Creates a new RestClient using Azure App Service's Application Url - /// - public MobileServiceClient(string appUrl) : base(appUrl) - { - AppUrl = appUrl; - Debug.Log("AppUrl: " + AppUrl); - - // required for running in Windows and Android - #if !NETFX_CORE || UNITY_ANDROID - Debug.Log("ServerCertificateValidation"); - ServicePointManager.ServerCertificateValidationCallback = RemoteCertificateValidationCallback; - #endif - } + public const string URI_API = "api/"; /// - /// Using factory method forces app url to be changed from 'http' to 'https' url + /// Creates a new RestClient using Azure App Service's Application Url /// - public static MobileServiceClient Create(string appUrl) + public MobileServiceClient (string appUrl) { - return new MobileServiceClient (ForceHttps (appUrl)); - } + AppUrl = HttpsUri (appUrl); + Debug.Log ("App Url: " + AppUrl); + + // required for running in Windows and Android + #if !NETFX_CORE || UNITY_ANDROID + Debug.Log ("ServerCertificateValidation"); + ServicePointManager.ServerCertificateValidationCallback = RemoteCertificateValidationCallback; + #endif + } - public override string ToString() - { - return this.BaseUrl; - } + public override string ToString () + { + return this.AppUrl; + } - public MobileServiceTable GetTable(string tableName) where E : class - { - return new MobileServiceTable(tableName, this); - } + public MobileServiceTable GetTable (string tableName) where E : class + { + return new MobileServiceTable (tableName, this); + } - /// - /// Client-directed single sign on (POST with access token) - /// - public void Login(MobileServiceAuthenticationProvider provider, string token, Action> callback = null) - { - string p = provider.ToString().ToLower(); - string uri = IsAppService() ? ".auth/login/" + p : "login/" + p; - ZumoRequest request = new ZumoRequest(this, uri, Method.POST); - Debug.Log("Login Request Uri: " + uri + " access token: " + token); - request.AddBodyAccessToken(token); - this.ExecuteAsync(request, callback); - } + /// + /// Client-directed single sign on (POST with access token) + /// + public IEnumerator Login (MobileServiceAuthenticationProvider provider, string token, Action> callback = null) + { + string p = provider.ToString ().ToLower (); + string url = string.Format ("{0}/.auth/login/{1}", AppUrl, p); + Debug.Log ("Login Request Url: " + url + " access token: " + token); + ZumoRequest request = new ZumoRequest (this, url, Method.POST); + request.AddBodyAccessToken (token); + yield return request.request.Send (); + request.ParseJson (callback); + } - /// - /// TODO: Service login (using GET via webview) - /// - /* + /// + /// TODO: Service login (using GET via webview) + /// + /* public void Login(MobileServiceAuthenticationProvider provider) { Debug.Log("TODO"); } //*/ - /// - /// GET custom API - /// - public void InvokeApi(string apiName, Action> callback = null) where T : new() - { - InvokeApi (apiName, Method.GET, callback); - } + /// + /// GET custom API + /// + public IEnumerator InvokeApi (string apiName, Action> callback = null) where T : new() + { + return InvokeApi (apiName, Method.GET, callback); + } /// /// Invokes custom API for HTTP Methods: GET, POST, PUT, PATCH, DELETE /// - public void InvokeApi(string apiName, Method httpMethod, Action> callback = null) where T : new() + public IEnumerator InvokeApi (string apiName, Method httpMethod, Action> callback = null) where T : new() { - string uri = URI_API + apiName; - ZumoRequest request = new ZumoRequest(this, uri, httpMethod); - Debug.Log( httpMethod.ToString() + " custom API Request Uri: " + uri ); - this.ExecuteAsync(request, callback); + string url = string.Format ("{0}/{1}{2}", AppUrl, URI_API, apiName); + Debug.Log (httpMethod.ToString () + " custom API Request Url: " + url); + ZumoRequest request = new ZumoRequest (this, url, httpMethod); + yield return request.request.Send (); + request.ParseJson (callback); } /// /// Invokes custom API with body /// - public void InvokeApi(string apiName, Method httpMethod, T body, Action> callback = null) where T : new() + public IEnumerator InvokeApi (string apiName, Method httpMethod, T body, Action> callback = null) where T : new() { - string uri = URI_API + apiName; - ZumoRequest request = new ZumoRequest(this, uri, httpMethod); - request.AddBody (body); - Debug.Log( httpMethod.ToString() + " custom API Request Uri: " + uri ); - this.ExecuteAsync(request, callback); + string url = string.Format ("{0}/{1}{2}", AppUrl, URI_API, apiName); + Debug.Log (httpMethod.ToString () + " custom API Request Url: " + url); + ZumoRequest request = new ZumoRequest (this, url, httpMethod); + request.AddBody (body); + yield return request.request.Send (); + request.ParseJson (callback); } /// - /// When you copy the URL is is 'http' by default, but its preferable to use 'https' + /// When you copy the URL from the Azure Portal it is 'http' by default, but it needs to be 'https' for post /// - private static string ForceHttps(string appUrl) + private static string HttpsUri (string appUrl) { - return Regex.Replace(appUrl, "(?m)http://", "https://"); + return Regex.Replace (appUrl, "(?si)^http://", "https://").TrimEnd ('/'); } - /// - /// Mobile Service uses an AppKey, but App Service does not. - /// - public bool IsAppService() - { - return String.IsNullOrEmpty(AppKey); - } - - #if !NETFX_CORE || UNITY_ANDROID - private bool RemoteCertificateValidationCallback(System.Object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) - { - // Check the certificate to see if it was issued from Azure - if (certificate.Subject.Contains("azurewebsites.net")) - { - return true; - } - else - { - return false; - } - } - #endif + #if !NETFX_CORE || UNITY_ANDROID + private bool RemoteCertificateValidationCallback (System.Object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) + { + // Check the certificate to see if it was issued from Azure + if (certificate.Subject.Contains ("azurewebsites.net")) { + return true; + } else { + return false; + } + } + #endif - } + } } diff --git a/helpers/DataHelper.cs b/helpers/DataHelper.cs new file mode 100644 index 0000000..d546cc6 --- /dev/null +++ b/helpers/DataHelper.cs @@ -0,0 +1,20 @@ +using System; +using System.Text; + +namespace Unity3dAzure.AppServices +{ + public class DataHelper + { + + public static byte[] ToUTF8Bytes (string utf8String) + { + return Encoding.UTF8.GetBytes (utf8String); + } + + public static string ToUTF8String (byte[] bytes) + { + return Encoding.UTF8.GetString (bytes); + } + + } +} \ No newline at end of file diff --git a/helpers/JsonHelper.cs b/helpers/JsonHelper.cs new file mode 100644 index 0000000..4f3d564 --- /dev/null +++ b/helpers/JsonHelper.cs @@ -0,0 +1,107 @@ +#pragma warning disable 0649 // ignores warning: array "is never assigned to, and will always have its default value 'null'" +using UnityEngine; +using System; +using System.Text.RegularExpressions; +#if NETFX_CORE +using Windows.Data.Json; +using System.Collections.Generic; +using System.Linq; +#endif + +namespace Unity3dAzure.AppServices +{ + /// + /// Wrapper work-around for json array described on https://forum.unity3d.com/threads/how-to-load-an-array-with-jsonutility.375735/ + /// + [Serializable] + internal class Wrapper + { + public T[] array; + } + + public class JsonHelper + { + /// + /// Work-around to parse json array + /// + public static T[] FromJsonArray(string json) + { + // Work-around for JsonUtility array serialization issues in Windows Store Apps. +#if NETFX_CORE + JsonArray jsonArray = new JsonArray(); + if (JsonArray.TryParse(json, out jsonArray)) + { + return GetArray(jsonArray); + } + Debug.LogWarning("Failed to parse json array of type:" + typeof(T).ToString() ); + return default(T[]); +#endif + string newJson = "{\"array\":" + json + "}"; + Wrapper wrapper = new Wrapper(); + try + { + wrapper = JsonUtility.FromJson>(newJson); + } + catch (Exception e) + { + Debug.LogWarning("Failed to parse json array of type:" + typeof(T).ToString() + " Exception message: " + e.Message); + return default(T[]); + } + return wrapper.array; + } + + public static N FromJsonNestedArray(string json, string namedArray) where N : INestedResults, new() + { +#if NETFX_CORE + JsonObject jsonObject = new JsonObject(); + if (JsonObject.TryParse(json, out jsonObject)) + { + JsonArray jsonArray = jsonObject.GetNamedArray(namedArray); + T[] array = GetArray(jsonArray); + N nestedResults = new N(); //NestedResults nestedResults = new NestedResults(array); + nestedResults.SetArray(array); + + string namedCount = nestedResults.GetCountField(); + uint count = Convert.ToUInt32( jsonObject.GetNamedNumber(namedCount) ); + nestedResults.SetCount(count); + + return nestedResults; + } else { + Debug.LogWarning("Failed to parse json nested array of type:" + typeof(T).ToString()); + return default(N); + } +#endif + N results = JsonUtility.FromJson(json); // NestedResults nestedResults = JsonUtility.FromJson(json); + return results; + } + +#if NETFX_CORE + private static T[] GetArray(JsonArray array) + { + List list = new List(); + foreach (var x in array) + { + try + { + T item = JsonUtility.FromJson(x.ToString()); + list.Add(item); + } + catch (Exception e) + { + Debug.LogWarning("Failed to parse json of type:" + typeof(T).ToString() + " Exception message: " + e.Message + " json:'" + x.ToString() + "'"); + } + } + return list.ToArray(); + } +#endif + + /// + /// 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). + /// + public static string ToJsonExcludingSystemProperties(object obj) + { + string jsonString = JsonUtility.ToJson(obj); + return Regex.Replace(jsonString, "(?i)(\\\"id\\\":\\\"\\\",)?(\\\"createdAt\\\":\\\"[0-9TZ:.-]*\\\",)?(\\\"updatedAt\\\":\\\"[0-9TZ:.-]*\\\",)?(\\\"version\\\":\\\"[A-Z0-9=]*\\\",)?(\\\"deleted\\\":(true|false),)?(\\\"ROW_NUMBER\\\":\\\"[0-9]*\\\",)?", ""); + } + } +} \ No newline at end of file diff --git a/helpers/Model.cs b/helpers/Model.cs index aa98c62..eef4235 100644 --- a/helpers/Model.cs +++ b/helpers/Model.cs @@ -5,18 +5,33 @@ namespace Unity3dAzure.AppServices /// /// Helper methods to check and get object properties /// - public class Model { - public static bool HasProperty(object obj, string propertyName) + public class Model + { + public static bool HasProperty (object obj, string propertyName) { - return GetProperty(obj, propertyName) != null; + return GetProperty (obj, propertyName) != null; } - public static PropertyInfo GetProperty(object obj, string propertyName) + public static PropertyInfo GetProperty (object obj, string propertyName) { #if NETFX_CORE - return obj.GetType().GetTypeInfo().GetDeclaredProperty(propertyName); // workaround for GetProperty on Windows + return obj.GetType().GetTypeInfo().GetDeclaredProperty(propertyName); // GetProperty for UWP #else - return obj.GetType().GetProperty(propertyName); + return obj.GetType ().GetProperty (propertyName); + #endif + } + + public static bool HasField (object obj, string fieldName) + { + return GetField (obj, fieldName) != null; + } + + public static FieldInfo GetField (object obj, string fieldName) + { + #if NETFX_CORE + return obj.GetType().GetTypeInfo().GetDeclaredField(fieldName); // GetField for UWP + #else + return obj.GetType ().GetField (fieldName); #endif } } diff --git a/http/IRestResponse.cs b/http/IRestResponse.cs new file mode 100644 index 0000000..d7da69d --- /dev/null +++ b/http/IRestResponse.cs @@ -0,0 +1,21 @@ +using System; +using System.Net; + +namespace Unity3dAzure.AppServices +{ + public interface IRestResponse + { + bool IsError { get; } + + string ErrorMessage { get; } + + string Url { get; } + + HttpStatusCode StatusCode { get; } + + string Content { get; } + + T Data { get; } + } +} + diff --git a/http/Method.cs b/http/Method.cs new file mode 100644 index 0000000..b2d5e6f --- /dev/null +++ b/http/Method.cs @@ -0,0 +1,13 @@ +using UnityEngine; +using System.Collections; + +namespace Unity3dAzure.AppServices +{ + public enum Method + { + GET, + POST, + PATCH, + DELETE + } +} \ No newline at end of file diff --git a/http/RestRequest.cs b/http/RestRequest.cs new file mode 100644 index 0000000..c7af339 --- /dev/null +++ b/http/RestRequest.cs @@ -0,0 +1,172 @@ +using UnityEngine; +using UnityEngine.Networking; +using System; +using System.Net; + +namespace Unity3dAzure.AppServices +{ + public abstract class RestRequest : IDisposable + { + public UnityWebRequest request { get; private set; } + + public RestRequest (string url, Method method) + { + request = new UnityWebRequest (url, method.ToString ()); + request.downloadHandler = new DownloadHandlerBuffer (); + } + + public void AddHeader (string key, string value) + { + request.SetRequestHeader (key, value); + } + + public void AddBody (byte[] bytes, string contentType) + { + if (request.uploadHandler != null) { + Debug.LogWarning ("Request body can only be set once"); + return; + } + request.uploadHandler = new UploadHandlerRaw (bytes); + request.uploadHandler.contentType = contentType; + } + + public virtual void AddBody (T data, string contentType = "application/json; charset=utf-8") + { + string jsonString = JsonUtility.ToJson (data); + byte[] bytes = DataHelper.ToUTF8Bytes (jsonString); + this.AddBody (bytes, contentType); + } + + #region Response and json object parsing + + private RestResult GetRestResult() + { + HttpStatusCode statusCode = (HttpStatusCode)Enum.Parse(typeof(HttpStatusCode), request.responseCode.ToString()); + RestResult result = new RestResult(statusCode); + + if (result.IsError) + { + result.ErrorMessage = "Response failed with status: " + statusCode.ToString(); + return result; + } + + if (string.IsNullOrEmpty(request.downloadHandler.text)) + { + result.IsError = true; + result.ErrorMessage = "Response has empty body"; + return result; + } + + return result; + } + + /// + /// Shared method to return response result whether an object or array of objects + /// + private RestResult TryParseJsonArray () + { + RestResult result = GetRestResult(); + // try parse an array of objects + try + { + result.AnArrayOfObjects = JsonHelper.FromJsonArray(request.downloadHandler.text); + } + catch (Exception e) + { + result.IsError = true; + result.ErrorMessage = "Failed to parse an array of objects of type: " + typeof(T).ToString() + " Exception message: " + e.Message; + } + return result; + } + + private RestResult TryParseJson() + { + RestResult result = GetRestResult(); + // try parse an object + try + { + result.AnObject = JsonUtility.FromJson(request.downloadHandler.text); + } + catch (Exception e) + { + result.IsError = true; + result.ErrorMessage = "Failed to parse object of type: " + typeof(T).ToString() + " Exception message: " + e.Message; + } + return result; + } + + /// + /// Parses object with T data = JsonUtil.FromJson, then callback RestResponse + /// + public void ParseJson (Action> callback = null) + { + RestResult result = TryParseJson (); + + if (result.IsError) { + Debug.LogWarning ("Response error status:" + result.StatusCode + " code:" + request.responseCode + " error:" + result.ErrorMessage + " request url:" + request.url); + callback (new RestResponse (result.ErrorMessage, result.StatusCode, request.url, request.downloadHandler.text)); + } else { + callback (new RestResponse (result.StatusCode, request.url, request.downloadHandler.text, result.AnObject)); + } + this.Dispose(); + } + + /// + /// Parses array of objects with T[] data = JsonHelper.GetJsonArray, then callback RestResponse + /// + public void ParseJsonArray (Action> callback = null) + { + RestResult result = TryParseJsonArray (); + + if (result.IsError) { + Debug.LogWarning ("Response error status:" + result.StatusCode + " code:" + request.responseCode + " error:" + result.ErrorMessage + " request url:" + request.url); + callback (new RestResponse (result.ErrorMessage, result.StatusCode, request.url, request.downloadHandler.text)); + } else { + callback (new RestResponse (result.StatusCode, request.url, request.downloadHandler.text, result.AnArrayOfObjects)); + } + this.Dispose(); + } + + // *WSA + private RestResult TryParseJsonNestedArray(string namedArray) where N : INestedResults, new() + { + RestResult result = GetRestResult(); + // try parse an object + try + { + result.AnObject = JsonHelper.FromJsonNestedArray(request.downloadHandler.text, namedArray); //JsonUtility.FromJson(request.downloadHandler.text); + } + catch (Exception e) + { + result.IsError = true; + result.ErrorMessage = "Failed to parse object of type: " + typeof(N).ToString() + " Exception message: " + e.Message; + } + return result; + } + + // Work-around for nested array + public void ParseJsonNestedArray(string namedArray, Action> callback = null) where N : INestedResults, new() + { + RestResult result = TryParseJsonNestedArray(namedArray); + + if (result.IsError) + { + Debug.LogWarning("Response error status:" + result.StatusCode + " code:" + request.responseCode + " error:" + result.ErrorMessage + " request url:" + request.url); + callback(new RestResponse(result.ErrorMessage, result.StatusCode, request.url, request.downloadHandler.text)); + } + else + { + callback(new RestResponse(result.StatusCode, request.url, request.downloadHandler.text, result.AnObject)); + } + this.Dispose(); + } + + public void Dispose() + { + request.Dispose(); // request completed, clean-up resources + } + + #endregion + + } +} diff --git a/http/RestResponse.cs b/http/RestResponse.cs new file mode 100644 index 0000000..989532b --- /dev/null +++ b/http/RestResponse.cs @@ -0,0 +1,88 @@ +using System.Net; + +namespace Unity3dAzure.AppServices +{ + public abstract class Response + { + public bool IsError { get; set; } + + public string ErrorMessage { get; set; } + + public string Url { get; set; } + + public HttpStatusCode StatusCode { get; set; } + + public string Content { get; set; } + + protected Response (HttpStatusCode statusCode) + { + this.StatusCode = statusCode; + this.IsError = !((int)statusCode >= 200 && (int)statusCode < 300); + } + + // success + protected Response (HttpStatusCode statusCode, string url, string text) + { + this.IsError = false; + this.Url = url; + this.ErrorMessage = null; + this.StatusCode = statusCode; + this.Content = text; + } + + // failure + protected Response (string error, HttpStatusCode statusCode, string url, string text) + { + this.IsError = true; + this.Url = url; + this.ErrorMessage = error; + this.StatusCode = statusCode; + this.Content = text; + } + } + + public sealed class RestResponse : Response + { + // success + public RestResponse (HttpStatusCode statusCode, string url, string text) : base (statusCode, url, text) + { + } + + // failure + public RestResponse (string error, HttpStatusCode statusCode, string url, string text) : base (error, statusCode, url, text) + { + } + } + + public sealed class RestResponse : Response, IRestResponse + { + public T Data { get; set; } + + // success + public RestResponse (HttpStatusCode statusCode, string url, string text, T data) : base (statusCode, url, text) + { + this.Data = data; + } + + // failure + public RestResponse (string error, HttpStatusCode statusCode, string url, string text) : base (error, statusCode, url, text) + { + } + } + + /// + /// Parsed JSON result could either be an object or an array of objects + /// + internal sealed class RestResult : Response + { + public T AnObject { get; set; } + + public T[] AnArrayOfObjects { get; set; } + + public RestResult (HttpStatusCode statusCode) : base (statusCode) + { + } + } + +} + diff --git a/http/ZumoRequest.cs b/http/ZumoRequest.cs index ca57400..6409817 100644 --- a/http/ZumoRequest.cs +++ b/http/ZumoRequest.cs @@ -1,38 +1,33 @@ using UnityEngine; -using System.Collections; -using RestSharp; -using System; namespace Unity3dAzure.AppServices { - [CLSCompliant(false)] - public class ZumoRequest : RestRequest - { - public ZumoRequest(MobileServiceClient client, string uri, Method httpMethod) : base(uri, httpMethod) - { - if (client.IsAppService()) - { - // App Service headers - this.AddHeader("ZUMO-API-VERSION", "2.0.0"); - } else { - // Mobile Service headers - this.AddHeader("X-ZUMO-APPLICATION", client.AppKey); - } + public sealed class ZumoRequest : RestRequest + { + public ZumoRequest (MobileServiceClient client, string url, Method httpMethod) : base (url, httpMethod) + { + this.AddHeader ("ZUMO-API-VERSION", "2.0.0"); + this.AddHeader ("Accept", "application/json"); + this.AddHeader ("Content-Type", "application/json; charset=utf-8"); - this.AddHeader("Accept", "application/json"); - this.RequestFormat = DataFormat.Json; - if (client.User != null && !string.IsNullOrEmpty(client.User.authenticationToken)) - { - this.AddHeader("X-ZUMO-AUTH", client.User.authenticationToken); - Debug.Log("Auth UserId:" + client.User.user.userId); - } - } + if (client.User != null && !string.IsNullOrEmpty (client.User.authenticationToken)) { + this.AddHeader ("X-ZUMO-AUTH", client.User.authenticationToken); + Debug.Log ("Auth UserId:" + client.User.user.userId); + } + } - public void AddBodyAccessToken(string token) - { - AccessToken accessToken = new AccessToken(token); - this.AddBody(accessToken); - } + public void AddBodyAccessToken (string token) + { + AccessToken accessToken = new AccessToken (token); + this.AddBody (accessToken); + } - } + public override void AddBody (T data, string contentType = "application/json; charset=utf-8") + { + string jsonString = JsonHelper.ToJsonExcludingSystemProperties (data); // strip App Services System Properties + byte[] bytes = DataHelper.ToUTF8Bytes (jsonString); + this.AddBody (bytes, contentType); + } + + } } diff --git a/model/AccessToken.cs b/model/AccessToken.cs index d74aea6..411e5d6 100644 --- a/model/AccessToken.cs +++ b/model/AccessToken.cs @@ -1,18 +1,21 @@ -namespace Unity3dAzure.AppServices +using System; + +namespace Unity3dAzure.AppServices { - public class AccessToken - { - public string access_token { get; set; } - + [Serializable] + public class AccessToken + { + public string access_token; + /// /// Facebook, Google, AAD access_token request /// - public AccessToken(string accessTokenValue) + public AccessToken (string accessTokenValue) { access_token = accessTokenValue; } - /// Needed only for Serialization (Fixes error: AccessToken cannot be serialized. Consider marking it with the DataContractAttribute attribute, and marking all of its members you want serialized with the DataMemberAttribute attribute) - public AccessToken() {} - } + // Must have a public parameterless contructor in order to use it as paramter 'T' in the generic type or method RestRequest.AddBody + //public AccessToken() {} + } } \ No newline at end of file diff --git a/model/MobileServiceUser.cs b/model/MobileServiceUser.cs index c025765..a473009 100644 --- a/model/MobileServiceUser.cs +++ b/model/MobileServiceUser.cs @@ -1,8 +1,11 @@ -namespace Unity3dAzure.AppServices +using System; + +namespace Unity3dAzure.AppServices { - public class MobileServiceUser - { - public string authenticationToken { get; set; } - public User user { get; set; } - } + [Serializable] + public class MobileServiceUser + { + public string authenticationToken; + public User user; + } } \ No newline at end of file diff --git a/model/ResponseError.cs b/model/ResponseError.cs index d630991..f0ba15b 100644 --- a/model/ResponseError.cs +++ b/model/ResponseError.cs @@ -1,8 +1,10 @@ -namespace Unity3dAzure.AppServices +using System; + +namespace Unity3dAzure.AppServices { - public class ResponseError - { - public int code { get; set; } - public string error { get; set; } - } + [Serializable] + public class ResponseError + { + public string error; + } } \ No newline at end of file diff --git a/model/User.cs b/model/User.cs index 61e107a..479c714 100644 --- a/model/User.cs +++ b/model/User.cs @@ -1,7 +1,10 @@ -namespace Unity3dAzure.AppServices +using System; + +namespace Unity3dAzure.AppServices { - public class User - { - public string userId { get; set; } - } + [Serializable] + public class User + { + public string userId; + } } \ No newline at end of file diff --git a/model/base/DataModel.cs b/model/base/DataModel.cs index 920a4bf..7f9b82e 100644 --- a/model/base/DataModel.cs +++ b/model/base/DataModel.cs @@ -1,37 +1,81 @@ using System; -using System.ComponentModel; -using Pathfinding.Serialization.JsonFx; +using UnityEngine; namespace Unity3dAzure.AppServices { [Serializable] - [CLSCompliant(false)] public class DataModel : IDataModel - { - public string id { get; set; } + { + [SerializeField] + public string id; - // system properties - [JsonIgnore] public DateTime createdAt { get; private set; } - [JsonIgnore] public DateTime updatedAt { get; private set; } - [JsonIgnore] public string version { get; private set; } - [JsonIgnore] public bool deleted { get; private set; } + public string GetId () + { + return id; + } + + public void SetId (string id) + { + this.id = id; + } + + // System Properties (read only) + [SerializeField] + private string createdAt; + private DateTime? _createdAt; + + public DateTime? CreatedAt () + { + if (_createdAt == null) { + _createdAt = Convert.ToDateTime (createdAt); + } + return _createdAt; + } + + [SerializeField] + private string updatedAt; + private DateTime? _updatedAt; + + public DateTime? UpdatedAt () + { + if (_updatedAt == null) { + _updatedAt = Convert.ToDateTime (updatedAt); + } + return _updatedAt; + } + + [SerializeField] + private string version; + + public string Version () + { + return version; + } + + [SerializeField] + private bool deleted; - // `$inlinecount=allpages` property - [EditorBrowsable(EditorBrowsableState.Never)] - [JsonIgnore] public uint ROW_NUMBER { get; private set; } - public uint GetIndex() + public bool Deleted () { - return ROW_NUMBER; + return deleted; } - public string GetId() - { - return id; - } + [SerializeField] + private string ROW_NUMBER; + + public uint RowNumber () + { + return Convert.ToUInt32 (ROW_NUMBER, 10); + } public override string ToString () { - return string.Format ("id:{0}, createdAt:{1}, updatedAt:{2}, version:{3}, deleted:{4}, index:{5}", id, createdAt, updatedAt, version, deleted, GetIndex()); + return JsonUtility.ToJson (this); + } + + public string ToJSON () + { + return JsonHelper.ToJsonExcludingSystemProperties (this); } - } + } } \ No newline at end of file diff --git a/model/base/IDataModel.cs b/model/base/IDataModel.cs index 370ca6b..dbd9c02 100644 --- a/model/base/IDataModel.cs +++ b/model/base/IDataModel.cs @@ -1,7 +1,7 @@ namespace Unity3dAzure.AppServices { - public interface IDataModel - { - string GetId(); - } + public interface IDataModel + { + string GetId (); + } } \ No newline at end of file diff --git a/model/nested/INestedResults.cs b/model/nested/INestedResults.cs index d6223ac..413f49b 100644 --- a/model/nested/INestedResults.cs +++ b/model/nested/INestedResults.cs @@ -1,9 +1,4 @@ -using System.Collections; -using System.Collections.Generic; -using System; -using RestSharp; - -namespace Unity3dAzure.AppServices +namespace Unity3dAzure.AppServices { /// /// Interface to support table Query with `$inlinecount=allpages` @@ -11,4 +6,15 @@ namespace Unity3dAzure.AppServices public interface INestedResults { } + + public interface INestedResults + { + // work-around for WSA + string GetArrayField(); + string GetCountField(); + + void SetArray(T[] array); + void SetCount(uint count); + + } } \ No newline at end of file diff --git a/model/nested/NestedResults.cs b/model/nested/NestedResults.cs index c8eb813..4db78c1 100644 --- a/model/nested/NestedResults.cs +++ b/model/nested/NestedResults.cs @@ -1,16 +1,35 @@ using System; -using System.Collections.Generic; namespace Unity3dAzure.AppServices { - /// - /// Wrap your data model with this object to call the table Query with `$inlinecount=allpages` param. - /// - [CLSCompliant(false)] - [Serializable] - public class NestedResults : INestedResults - { - public uint count { get; set; } - public List results { get; set; } - } + /// + /// Wrap your data model with this object to call the table Query with `$inlinecount=allpages` param. + /// + [Serializable] + public sealed class NestedResults : INestedResults + { + public uint count; + public T[] results; + + // WSA work-around + public void SetArray(T[] array) + { + this.results = array; + } + + public void SetCount(uint count) + { + this.count = count; + } + + public string GetArrayField() + { + return "results"; + } + + public string GetCountField() + { + return "count"; + } + } } \ No newline at end of file diff --git a/table/IAzureMobileServiceTable.cs b/table/IAzureMobileServiceTable.cs index 676b959..f3b70a7 100644 --- a/table/IAzureMobileServiceTable.cs +++ b/table/IAzureMobileServiceTable.cs @@ -1,55 +1,45 @@ -using System.Collections; -using System.Collections.Generic; -using System; -using RestSharp; +using System; +using System.Collections; namespace Unity3dAzure.AppServices { - /// - /// NB: Interface uses callbacks as Unity does not support async methods right now. - /// An async method would mean we could just return result as success or failure and eliminate need for a callback. - /// - /// List Read() where T : new(); - /// - /// - [CLSCompliant(false)] - public interface IAzureMobileServiceTable - { - /// - /// Create a new item. - /// - void Insert(T item, Action> callback = null) where T : new(); - - /// - /// Get a list of items. - /// - void Read(Action>> callback = null) where T : new(); - - /// - /// Update an item's data using id property. - /// - void Update(T item, Action> callback = null) where T : new(); - - /// - /// Delete an item using id property. - /// - void Delete(string id, Action> callback = null) where T : new(); - - /// - /// Get a list of results using a custom query. - /// More info about custom queries here: - /// https://msdn.microsoft.com/en-us/library/azure/jj677199.aspx - /// - void Query(CustomQuery query, Action>> callback = null) where T : new(); + public interface IAzureMobileServiceTable + { + /// + /// Create a new item. + /// + IEnumerator Insert (T item, Action> callback = null) where T : new(); /// - /// Returns a 'count' and nested list of 'results' (appends `$inlinecount=allpages` parameter to the query) + /// Get a list of items. /// - void Query(CustomQuery query, Action> callback = null) where T : INestedResults, new(); - + IEnumerator Read (Action> callback = null) where T : new(); + + /// + /// Update an item's data using id property. + /// + IEnumerator Update (T item, Action> callback = null) where T : new(); + + /// + /// Delete an item using id property. + /// + IEnumerator Delete (string id, Action> callback = null) where T : new(); + + /// + /// Get a list of results using a custom query. + /// More info about custom queries here: + /// https://msdn.microsoft.com/en-us/library/azure/jj677199.aspx + /// + IEnumerator Query (CustomQuery query, Action> callback = null) where T : new(); + + /// + /// Returns a 'count' and nested list of 'results' (appends `$inlinecount=allpages` parameter to the query) + /// + IEnumerator Query(CustomQuery query, Action>> callback = null) where T : new(); + /// /// Get an item's data using id property. /// - void Lookup(string id, Action> callback = null) where T : new(); - } + IEnumerator Lookup (string id, Action> callback = null) where T : new(); + } } \ No newline at end of file diff --git a/table/MobileServiceTable.cs b/table/MobileServiceTable.cs index 188bcd7..c5232e2 100644 --- a/table/MobileServiceTable.cs +++ b/table/MobileServiceTable.cs @@ -1,111 +1,121 @@ using UnityEngine; using System.Collections; -using RestSharp; -using System.Collections.Generic; using System; namespace Unity3dAzure.AppServices { - [CLSCompliant(false)] - public class MobileServiceTable : IAzureMobileServiceTable - { - private MobileServiceClient _client; - private string _name; + public class MobileServiceTable : IAzureMobileServiceTable + { + private MobileServiceClient _client; + private string _name; - public const string URI_TABLES = "tables/"; - - public MobileServiceTable(string tableName, MobileServiceClient client) - { - _client = client; - _name = tableName; // NB: The table name could be infered from the Table's DataModel using typeof(E).Name; but passing Table name as a string allows for the case when the Classname is not the same as the Table name. - } - - public override string ToString() - { - return _name; - } - - public void Insert(T item, Action> callback = null) where T : new() - { - string uri = URI_TABLES + _name; - ZumoRequest request = new ZumoRequest(_client, uri, Method.POST); - Debug.Log( "Insert Request: " + uri ); - request.AddBody(item); - _client.ExecuteAsync(request, callback); - } - - public void Read(Action>> callback = null) where T : new() - { - string uri = URI_TABLES + _name; - ZumoRequest request = new ZumoRequest(_client, uri, Method.GET); - Debug.Log( "Read Request: " + uri ); - _client.ExecuteAsync>(request, callback); - } - - public void Query(CustomQuery query, Action>> callback = null) where T : new() - { - string uri = string.Format("{0}{1}{2}", URI_TABLES, _name, query); - ZumoRequest request = new ZumoRequest(_client, uri, Method.GET); - Debug.Log( "Query Request: " + uri +" Query:"+ query ); - _client.ExecuteAsync> (request, callback); - } + public const string URI_TABLES = "tables/"; - public void Query(CustomQuery query, Action> callback = null) where T : INestedResults, new() + public MobileServiceTable (string tableName, MobileServiceClient client) { - string queryResults = query.ToString (); - string q = queryResults.Length > 0 ? "&" : "?"; - queryResults += string.Format("{0}$inlinecount=allpages", q); - string uri = string.Format("{0}{1}{2}", URI_TABLES, _name, queryResults); - ZumoRequest request = new ZumoRequest(_client, uri, Method.GET); - Debug.Log( "Query Request: " + uri +" Query with inlinecount:"+ queryResults ); - _client.ExecuteAsync (request, callback); + _client = client; + _name = tableName; // NB: The table name could be infered from the Table's DataModel using typeof(E).Name; but passing Table name as a string allows for the case when the Classname is not the same as the Table name. } - - public void Update(T item, Action> callback = null) where T : new() - { - string id = null; - // Check if model uses the 'IDataModel' Interface to get id property, otherwise try Refelection (using 'Model' helper). - IDataModel model = item as IDataModel; - if (model != null) - { - id = model.GetId(); - } - else if ( Model.HasProperty(item, "id") ) - { - var x = Model.GetProperty(item, "id"); - id = x.GetValue(item, null) as string; - } - else - { - Debug.LogError("Unable to get 'id' data model property"); - } - if (string.IsNullOrEmpty(id)) - { - Debug.LogError("Error 'id' value is missing"); - return; - } - string uri = URI_TABLES + _name + "/" + id; - ZumoRequest request = new ZumoRequest(_client, uri, Method.PATCH); - Debug.Log("Update Request Uri: " + uri); - request.AddBody(item); - _client.ExecuteAsync(request, callback); - } - - public void Delete(string id, Action> callback = null) where T : new() + + public override string ToString () + { + return _name; + } + + public IEnumerator Insert (T item, Action> callback = null) where T : new() + { + string url = string.Format ("{0}/{1}{2}", _client.AppUrl, URI_TABLES, _name); + ZumoRequest request = new ZumoRequest (_client, url, Method.POST); + Debug.Log ("Insert Request: " + url); + request.AddBody (item); + yield return request.request.Send (); + request.ParseJson (callback); + } + + public IEnumerator Read (Action> callback = null) where T : new() + { + string url = string.Format ("{0}/{1}{2}", _client.AppUrl, URI_TABLES, _name); + ZumoRequest request = new ZumoRequest (_client, url, Method.GET); + Debug.Log ("Read Request: " + url); + yield return request.request.Send (); + request.ParseJsonArray (callback); + } + + public IEnumerator Query (CustomQuery query, Action> callback = null) where T : new() + { + string url = string.Format ("{0}/{1}{2}{3}", _client.AppUrl, URI_TABLES, _name, query); + ZumoRequest request = new ZumoRequest (_client, url, Method.GET); + Debug.Log ("Query Request: " + url + " Query:" + query); + yield return request.request.Send (); + request.ParseJsonArray (callback); + } + /* + public IEnumerator Query (CustomQuery query, Action> callback = null) where N : INestedResults where T : new() { - string uri = URI_TABLES + _name + "/" + id; - ZumoRequest request = new ZumoRequest(_client, uri, Method.DELETE); - Debug.Log( "Delete Request Uri: " + uri ); - _client.ExecuteAsync(request, callback); - } - - public void Lookup(string id, Action> callback = null) where T : new() + string queryString = query.ToString (); + string q = queryString.Length > 0 ? "&" : "?"; + queryString += string.Format ("{0}$inlinecount=allpages", q); + string url = string.Format ("{0}/{1}{2}{3}", _client.AppUrl, URI_TABLES, _name, queryString); + Debug.Log ("Query Request: " + url + " Paginated Query:" + query); + ZumoRequest request = new ZumoRequest (_client, url, Method.GET); + yield return request.request.Send (); + request.TryParseJsonNestedArray ("results", callback); + } + */ + public IEnumerator Query(CustomQuery query, Action>> callback = null) where T : new() { - string uri = URI_TABLES + _name + "/" + id; - ZumoRequest request = new ZumoRequest(_client, uri, Method.GET); - Debug.Log( "Lookup Request Uri: " + uri ); - _client.ExecuteAsync(request, callback); + string queryString = query.ToString(); + string q = queryString.Length > 0 ? "&" : "?"; + queryString += string.Format("{0}$inlinecount=allpages", q); + string url = string.Format("{0}/{1}{2}{3}", _client.AppUrl, URI_TABLES, _name, queryString); + Debug.Log("Query Request: " + url + " Paginated Query:" + query); + ZumoRequest request = new ZumoRequest(_client, url, Method.GET); + yield return request.request.Send(); + request.ParseJsonNestedArray>("results", callback); } - } + public IEnumerator Update (T item, Action> callback = null) where T : new() + { + string id = null; + // Check if model uses the 'IDataModel' Interface to get id property, otherwise try Refelection (using 'Model' helper). + IDataModel model = item as IDataModel; + if (model != null) { + id = model.GetId (); + } else if (Model.HasField (item, "id")) { + var x = Model.GetField (item, "id"); + id = x.GetValue (item) as string; + } else { + Debug.LogError ("Unable to get 'id' data model property"); + } + if (string.IsNullOrEmpty (id)) { + Debug.LogError ("Error 'id' value is missing"); + yield return null; + } + string url = string.Format ("{0}/{1}{2}/{3}", _client.AppUrl, URI_TABLES, _name, id); + ZumoRequest request = new ZumoRequest (_client, url, Method.PATCH); + request.AddBody (item); + Debug.Log ("Update Request Url: " + url + " patch:" + item); + yield return request.request.Send (); + request.ParseJson (callback); + } + + public IEnumerator Delete (string id, Action> callback = null) where T : new() + { + string url = string.Format ("{0}/{1}{2}/{3}", _client.AppUrl, URI_TABLES, _name, id); + ZumoRequest request = new ZumoRequest (_client, url, Method.DELETE); + Debug.Log ("Delete Request Url: " + url); + yield return request.request.Send (); + request.ParseJson (callback); + } + + public IEnumerator Lookup (string id, Action> callback = null) where T : new() + { + string url = string.Format ("{0}/{1}{2}/{3}", _client.AppUrl, URI_TABLES, _name, id); + ZumoRequest request = new ZumoRequest (_client, url, Method.GET); + Debug.Log ("Lookup Request Url: " + url); + yield return request.request.Send (); + request.ParseJson (callback); + } + + } } \ No newline at end of file diff --git a/table/query/CustomQuery.cs b/table/query/CustomQuery.cs index a70ad24..e1bf8cd 100644 --- a/table/query/CustomQuery.cs +++ b/table/query/CustomQuery.cs @@ -20,7 +20,6 @@ public enum MobileServiceSystemProperty deleted = 0x8 } - [CLSCompliant (false)] public class CustomQuery { // query option parameters defined by the Open Data Protocol (OData)