From 9081ce181b0a60d3002498e87f5a669968651e8a Mon Sep 17 00:00:00 2001 From: David Douglas Date: Mon, 21 Nov 2016 14:41:54 +0000 Subject: [PATCH 1/5] :construction: UnityWebRequest and JsonUtil --- client/IAzureMobileServiceClient.cs | 33 ++++---- client/MobileServiceClient.cs | 122 ++++++++++------------------ helpers/DataHelper.cs | 19 +++++ helpers/JsonHelper.cs | 35 ++++++++ helpers/Model.cs | 16 +++- http/IRestResponse.cs | 21 +++++ http/Method.cs | 12 +++ http/RestRequest.cs | 70 ++++++++++++++++ http/RestResponse.cs | 95 ++++++++++++++++++++++ http/ZumoRequest.cs | 28 +++---- model/AccessToken.cs | 11 ++- model/MobileServiceUser.cs | 9 +- model/ResponseError.cs | 14 ++-- model/User.cs | 7 +- model/base/DataModel.cs | 75 ++++++++++++----- model/nested/INestedResults.cs | 7 +- model/nested/NestedResults.cs | 6 +- table/IAzureMobileServiceTable.cs | 28 ++----- table/MobileServiceTable.cs | 101 ++++++++++++----------- table/query/CustomQuery.cs | 1 - 20 files changed, 483 insertions(+), 227 deletions(-) create mode 100644 helpers/DataHelper.cs create mode 100644 helpers/JsonHelper.cs create mode 100644 http/IRestResponse.cs create mode 100644 http/Method.cs create mode 100644 http/RestRequest.cs create mode 100644 http/RestResponse.cs diff --git a/client/IAzureMobileServiceClient.cs b/client/IAzureMobileServiceClient.cs index bcceb4a..d109789 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); - + 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 /// - void InvokeApi(string apiName, Action> callback = null) where T : new(); + 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(); + /// + /// Invokes a custom API for HTTP Methods: GET, POST, PUT, PATCH, DELETE + /// + 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(); + /// + /// Invokes a custom API with body object + /// + 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..08a7e23 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,58 +13,32 @@ namespace Unity3dAzure.AppServices { - [CLSCompliant(false)] - public class MobileServiceClient : RestClient, IAzureMobileServiceClient + public class MobileServiceClient : IAzureMobileServiceClient { public string AppUrl { get; private set; } - public string AppKey { get; private 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. + /// Creates a new RestClient using Azure App Service's Application Url /// - public MobileServiceClient(string appUrl, string appKey) : base(appUrl) + public MobileServiceClient(string appUrl) { - AppUrl = appUrl; - AppKey = appKey; + 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 - } - - /// - /// 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 +#if !NETFX_CORE || UNITY_ANDROID Debug.Log("ServerCertificateValidation"); ServicePointManager.ServerCertificateValidationCallback = RemoteCertificateValidationCallback; - #endif +#endif } - /// - /// Using factory method forces app url to be changed from 'http' to 'https' url - /// - public static MobileServiceClient Create(string appUrl) - { - return new MobileServiceClient (ForceHttps (appUrl)); - } - public override string ToString() { - return this.BaseUrl; + return this.AppUrl; } public MobileServiceTable GetTable(string tableName) where E : class @@ -76,14 +49,15 @@ public MobileServiceTable GetTable(string tableName) where E : class /// /// Client-directed single sign on (POST with access token) /// - public void Login(MobileServiceAuthenticationProvider provider, string token, Action> callback = null) + public IEnumerator 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); + 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); - this.ExecuteAsync(request, callback); + yield return request.request.Send(); + request.ParseData(callback); } /// @@ -99,51 +73,45 @@ public void Login(MobileServiceAuthenticationProvider provider) /// /// GET custom API /// - public void InvokeApi(string apiName, Action> callback = null) where T : new() + 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 IEnumerator InvokeApi(string apiName, Method httpMethod, Action> callback = null) where T : new() { - InvokeApi (apiName, Method.GET, 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.ParseData(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() - { - 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); - } - - /// - /// Invokes custom API with body - /// - public void 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); - } - - /// - /// When you copy the URL is is 'http' by default, but its preferable to use 'https' - /// - private static string ForceHttps(string appUrl) - { - return Regex.Replace(appUrl, "(?m)http://", "https://"); - } + /// + /// Invokes custom API with body + /// + public IEnumerator InvokeApi(string apiName, Method httpMethod, T body, Action> callback = null) where T : new() + { + string url = string.Format("{0}/{1}{2}", AppUrl, URI_API, apiName); + Debug.Log(httpMethod.ToString() + " custom API Request Uri: " + url); + ZumoRequest request = new ZumoRequest(this, url, httpMethod); + request.AddBody(body); + yield return request.request.Send(); + request.ParseData(callback); + } /// - /// Mobile Service uses an AppKey, but App Service does not. + /// When you copy the URL is is 'http' by default, but it needs to be 'https' /// - public bool IsAppService() + private static string HttpsUri(string appUrl) { - return String.IsNullOrEmpty(AppKey); + return Regex.Replace(appUrl, "(?m)http://", "https://").TrimEnd('/'); } - #if !NETFX_CORE || UNITY_ANDROID +#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 @@ -156,7 +124,7 @@ private bool RemoteCertificateValidationCallback(System.Object sender, X509Certi return false; } } - #endif +#endif } } diff --git a/helpers/DataHelper.cs b/helpers/DataHelper.cs new file mode 100644 index 0000000..e872caf --- /dev/null +++ b/helpers/DataHelper.cs @@ -0,0 +1,19 @@ +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..f9b2a18 --- /dev/null +++ b/helpers/JsonHelper.cs @@ -0,0 +1,35 @@ +#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; + +namespace Unity3dAzure.AppServices +{ + public class JsonHelper { + /// + /// Workaround for json array described on https://forum.unity3d.com/threads/how-to-load-an-array-with-jsonutility.375735/ + /// + public static T[] GetJsonArray(string json) + { + string newJson = "{\"array\":" + json + "}"; + Wrapper wrapper = JsonUtility.FromJson> (newJson); + return wrapper.array; + } + + [Serializable] + private class Wrapper + { + public T[] array; + } + + /// + /// 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..22e375a 100644 --- a/helpers/Model.cs +++ b/helpers/Model.cs @@ -14,10 +14,24 @@ public static bool HasProperty(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); #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 + } } } \ No newline at end of file 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..b26f1d3 --- /dev/null +++ b/http/Method.cs @@ -0,0 +1,12 @@ +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..601ea7f --- /dev/null +++ b/http/RestRequest.cs @@ -0,0 +1,70 @@ +using UnityEngine; +using System.Collections; +using UnityEngine.Networking; +using System; +using System.Net; + +namespace Unity3dAzure.AppServices +{ + public abstract class RestRequest { + + 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 + public void ParseData(Action> callback = null) { + Debug.Log ("parse data response:" + request.url); + int code = Convert.ToInt32 (request.responseCode); + HttpStatusCode statusCode = (HttpStatusCode)Enum.Parse(typeof (HttpStatusCode), request.responseCode.ToString()); + string text = request.downloadHandler.text; + // TODO: add error handling + Debug.Log ("HttpStatusCode:" + statusCode + " code:" + code + " request url:" + request.url); + if (statusCode == HttpStatusCode.OK) { + Debug.Log ("Status OK"); + } + T data = JsonUtility.FromJson(text); + callback( new RestResponse (request.url, statusCode, text, data) ); + request.Dispose (); + } + + public void ParseDataArray(Action> callback = null) { + Debug.Log ("parse data array response:" + request.url); + int code = Convert.ToInt32 (request.responseCode); + HttpStatusCode statusCode = (HttpStatusCode)Enum.Parse(typeof (HttpStatusCode), request.responseCode.ToString()); + string text = request.downloadHandler.text; + // TODO: add error handling + Debug.Log ("HttpStatusCode:" + statusCode + " code:" + code + " request url:" + request.url); + if (statusCode == HttpStatusCode.OK) { + Debug.Log ("Status OK"); + } + T[] data = JsonHelper.GetJsonArray(request.downloadHandler.text); + callback ( new RestResponse (request.url, statusCode, text, data) ); + request.Dispose (); + } + #endregion + + } +} diff --git a/http/RestResponse.cs b/http/RestResponse.cs new file mode 100644 index 0000000..35a4d83 --- /dev/null +++ b/http/RestResponse.cs @@ -0,0 +1,95 @@ +using System; +using System.Net; + +namespace Unity3dAzure.AppServices +{ + public abstract class Response + { + private bool isError; + public bool IsError { + get { + return this.isError; + } + } + + private string errorMessage; + public string ErrorMessage { + get { + return this.errorMessage; + } + } + + private string url; + public string Url { + get { + return this.url; + } + } + + private HttpStatusCode statusCode; + public HttpStatusCode StatusCode { + get { + return this.statusCode; + } + } + + private string content; + public string Content { + get { + return this.content; + } + } + + // success + protected Response (string url, HttpStatusCode statusCode, string text) + { + this.isError = false; + this.url = url; + this.errorMessage = null; + this.statusCode = statusCode; + this.content = text; + } + + // failure + protected Response (string error, string url, HttpStatusCode statusCode, 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 (string url, HttpStatusCode statusCode, string text) : base(url, statusCode, text){} + + // failure + public RestResponse (string error, string url, HttpStatusCode statusCode, string text) : base(error, url, statusCode, text){} + } + + public sealed class RestResponse : Response, IRestResponse + { + private T data; + public T Data { + get { + return this.data; + } + } + + // success + public RestResponse (string url, HttpStatusCode statusCode, string text, T data) : base(url, statusCode, text) + { + this.data = data; + } + + // failure + public RestResponse (string error, string url, HttpStatusCode statusCode, string text) : base(error, url, statusCode, text) + { + } + } + +} + diff --git a/http/ZumoRequest.cs b/http/ZumoRequest.cs index ca57400..d4df304 100644 --- a/http/ZumoRequest.cs +++ b/http/ZumoRequest.cs @@ -1,26 +1,16 @@ using UnityEngine; -using System.Collections; -using RestSharp; using System; namespace Unity3dAzure.AppServices { - [CLSCompliant(false)] - public class ZumoRequest : RestRequest + public sealed class ZumoRequest : RestRequest { - public ZumoRequest(MobileServiceClient client, string uri, Method httpMethod) : base(uri, httpMethod) + public ZumoRequest(MobileServiceClient client, string url, Method httpMethod) : base(url, 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); - } - + this.AddHeader("ZUMO-API-VERSION", "2.0.0"); this.AddHeader("Accept", "application/json"); - this.RequestFormat = DataFormat.Json; + this.AddHeader("Content-Type", "application/json; charset=utf-8"); + if (client.User != null && !string.IsNullOrEmpty(client.User.authenticationToken)) { this.AddHeader("X-ZUMO-AUTH", client.User.authenticationToken); @@ -31,8 +21,14 @@ public ZumoRequest(MobileServiceClient client, string uri, Method httpMethod) : public void AddBodyAccessToken(string token) { AccessToken accessToken = new AccessToken(token); - this.AddBody(accessToken); + 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..ca0abad 100644 --- a/model/AccessToken.cs +++ b/model/AccessToken.cs @@ -1,8 +1,11 @@ -namespace Unity3dAzure.AppServices +using System; + +namespace Unity3dAzure.AppServices { + [Serializable] public class AccessToken { - public string access_token { get; set; } + public string access_token; /// /// Facebook, Google, AAD access_token request @@ -12,7 +15,7 @@ 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..ba5cb03 100644 --- a/model/MobileServiceUser.cs +++ b/model/MobileServiceUser.cs @@ -1,8 +1,11 @@ -namespace Unity3dAzure.AppServices +using System; + +namespace Unity3dAzure.AppServices { + [Serializable] public class MobileServiceUser { - public string authenticationToken { get; set; } - public User user { get; set; } + 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..609c189 100644 --- a/model/User.cs +++ b/model/User.cs @@ -1,7 +1,10 @@ -namespace Unity3dAzure.AppServices +using System; + +namespace Unity3dAzure.AppServices { + [Serializable] public class User { - public string userId { get; set; } + public string userId; } } \ No newline at end of file diff --git a/model/base/DataModel.cs b/model/base/DataModel.cs index 920a4bf..d99f25d 100644 --- a/model/base/DataModel.cs +++ b/model/base/DataModel.cs @@ -1,37 +1,70 @@ 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; } - - // 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; } - - // `$inlinecount=allpages` property - [EditorBrowsable(EditorBrowsableState.Never)] - [JsonIgnore] public uint ROW_NUMBER { get; private set; } - public uint GetIndex() + [SerializeField] + public string id; + 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 ROW_NUMBER; + return version; } - public string GetId() - { - return id; - } + [SerializeField] + private bool deleted; + public bool Deleted() + { + return deleted; + } + + [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/nested/INestedResults.cs b/model/nested/INestedResults.cs index d6223ac..8bff8dc 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` diff --git a/model/nested/NestedResults.cs b/model/nested/NestedResults.cs index c8eb813..e8b2950 100644 --- a/model/nested/NestedResults.cs +++ b/model/nested/NestedResults.cs @@ -1,16 +1,14 @@ 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; } + public uint count; + public T[] results; } } \ No newline at end of file diff --git a/table/IAzureMobileServiceTable.cs b/table/IAzureMobileServiceTable.cs index 676b959..f01715c 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(); + IEnumerator Insert(T item, Action> callback = null) where T : new(); /// /// Get a list of items. /// - void Read(Action>> callback = null) where T : new(); + IEnumerator 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(); + IEnumerator 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(); + 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 /// - void Query(CustomQuery query, Action>> callback = null) where T : new(); + 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) /// - void Query(CustomQuery query, Action> callback = null) where T : INestedResults, new(); + IEnumerator Query(CustomQuery query, Action> callback = null) where T : INestedResults, 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..ca52a3b 100644 --- a/table/MobileServiceTable.cs +++ b/table/MobileServiceTable.cs @@ -1,13 +1,10 @@ using UnityEngine; using System.Collections; -using RestSharp; -using System.Collections.Generic; using System; namespace Unity3dAzure.AppServices { - [CLSCompliant(false)] - public class MobileServiceTable : IAzureMobileServiceTable + public class MobileServiceTable : IAzureMobileServiceTable { private MobileServiceClient _client; private string _name; @@ -25,43 +22,46 @@ public override string ToString() return _name; } - public void Insert(T item, Action> callback = null) where T : new() + public IEnumerator 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 ); + 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); - _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); + yield return request.request.Send(); + request.ParseData(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.ParseDataArray(callback); + } - public void Query(CustomQuery query, Action>> callback = null) where T : new() + public IEnumerator 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); + 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.ParseDataArray (callback); } - public void Query(CustomQuery query, Action> callback = null) where T : INestedResults, new() + public IEnumerator Query(CustomQuery query, Action> callback = null) where T : INestedResults, new() { - 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); + 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.ParseData(callback); } - public void Update(T item, Action> callback = null) where T : new() + 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). @@ -70,10 +70,10 @@ public override string ToString() { id = model.GetId(); } - else if ( Model.HasProperty(item, "id") ) + else if ( Model.HasField(item, "id") ) { - var x = Model.GetProperty(item, "id"); - id = x.GetValue(item, null) as string; + var x = Model.GetField(item, "id"); + id = x.GetValue(item) as string; } else { @@ -82,29 +82,32 @@ public override string ToString() if (string.IsNullOrEmpty(id)) { Debug.LogError("Error 'id' value is missing"); - return; + yield return null; } - string uri = URI_TABLES + _name + "/" + id; - ZumoRequest request = new ZumoRequest(_client, uri, Method.PATCH); - Debug.Log("Update Request Uri: " + uri); + 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); - _client.ExecuteAsync(request, callback); + Debug.Log("Update Request Url: " + url + " patch:" + item); + yield return request.request.Send(); + request.ParseData(callback); } - public void Delete(string id, Action> callback = null) where T : new() + public IEnumerator Delete(string id, Action> callback = null) 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); + 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.ParseData(callback); } - public void Lookup(string id, Action> callback = null) where T : new() + public IEnumerator Lookup(string id, 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 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.ParseData(callback); } } 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) From 6e0c42864ebf2975b1c9b7e74ada85536d0090aa Mon Sep 17 00:00:00 2001 From: David Douglas Date: Tue, 22 Nov 2016 13:36:08 +0000 Subject: [PATCH 2/5] :package: UnityWebRequest and documentation update --- README.md | 65 +++--- .../MobileServiceAuthenticationProvider.cs | 16 +- client/IAzureMobileServiceClient.cs | 50 ++-- client/MobileServiceClient.cs | 213 +++++++++--------- helpers/DataHelper.cs | 11 +- helpers/JsonHelper.cs | 11 +- helpers/Model.cs | 19 +- http/Method.cs | 11 +- http/RestRequest.cs | 111 ++++++--- http/RestResponse.cs | 95 ++++---- http/ZumoRequest.cs | 44 ++-- model/AccessToken.cs | 14 +- model/MobileServiceUser.cs | 6 +- model/User.cs | 6 +- model/base/DataModel.cs | 37 +-- model/base/IDataModel.cs | 8 +- table/IAzureMobileServiceTable.cs | 70 +++--- table/MobileServiceTable.cs | 185 ++++++++------- 18 files changed, 511 insertions(+), 461 deletions(-) diff --git a/README.md b/README.md index 727e076..742c8dd 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 [JsonUtil](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,27 +69,29 @@ 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. - -## 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 +## Known issues +There is an issue with using [Patch on Android using UnityWebRequest with 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. [Dimitris-Ilias Gkanatsios](https://twitter.com/dgkanatsios) who wrote an [AzureServicesForUnity](https://github.com/dgkanatsios/AzureServicesForUnity) library has documented a possible [workaround for using PATCH on Android](http://stackoverflow.com/questions/39023937/trying-to-use-patch-on-a-unitywebrequest-on-android-and-getting-unsupported-pro) using Easy APIs. + Questions or tweet #Azure #GameDev [@deadlyfingers](https://twitter.com/deadlyfingers) 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 d109789..6d0de2b 100644 --- a/client/IAzureMobileServiceClient.cs +++ b/client/IAzureMobileServiceClient.cs @@ -3,34 +3,34 @@ namespace Unity3dAzure.AppServices { - public interface IAzureMobileServiceClient - { - MobileServiceTable GetTable(string tableName) where E : class; + 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); + /// + /// 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); + /// + /// 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 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 - /// - IEnumerator InvokeApi(string apiName, Method httpMethod, Action> callback = null) where T : new(); + /// + /// Invokes a custom API for HTTP Methods: GET, POST, PUT, PATCH, DELETE + /// + IEnumerator InvokeApi (string apiName, Method httpMethod, Action> callback = null) where T : new(); - /// - /// Invokes a custom API with body object - /// - IEnumerator InvokeApi(string apiName, Method httpMethod, T body, Action> callback = null) where T : new(); - } + /// + /// Invokes a custom API with body object + /// + 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 08a7e23..4d9b942 100644 --- a/client/MobileServiceClient.cs +++ b/client/MobileServiceClient.cs @@ -13,118 +13,115 @@ namespace Unity3dAzure.AppServices { - public class MobileServiceClient : IAzureMobileServiceClient - { - public string AppUrl { get; private set; } - - public MobileServiceUser User { get; set; } - - public const string URI_API = "api/"; - - /// - /// Creates a new RestClient using Azure App Service's Application Url - /// - public MobileServiceClient(string 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.AppUrl; - } - - public MobileServiceTable GetTable(string tableName) where E : class - { - return new MobileServiceTable(tableName, this); - } - - /// - /// 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.ParseData(callback); - } - - /// - /// TODO: Service login (using GET via webview) - /// - /* + public class MobileServiceClient : IAzureMobileServiceClient + { + public string AppUrl { get; private set; } + + public MobileServiceUser User { get; set; } + + public const string URI_API = "api/"; + + /// + /// Creates a new RestClient using Azure App Service's Application Url + /// + public MobileServiceClient (string 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.AppUrl; + } + + public MobileServiceTable GetTable (string tableName) where E : class + { + return new MobileServiceTable (tableName, this); + } + + /// + /// 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.ParseData (callback); + } + + /// + /// TODO: Service login (using GET via webview) + /// + /* public void Login(MobileServiceAuthenticationProvider provider) { Debug.Log("TODO"); } //*/ - /// - /// 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 IEnumerator InvokeApi(string apiName, Method httpMethod, Action> callback = null) where T : new() - { - 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.ParseData(callback); - } - - /// - /// Invokes custom API with body - /// - public IEnumerator InvokeApi(string apiName, Method httpMethod, T body, Action> callback = null) where T : new() - { - string url = string.Format("{0}/{1}{2}", AppUrl, URI_API, apiName); - Debug.Log(httpMethod.ToString() + " custom API Request Uri: " + url); - ZumoRequest request = new ZumoRequest(this, url, httpMethod); - request.AddBody(body); - yield return request.request.Send(); - request.ParseData(callback); - } - - /// - /// When you copy the URL is is 'http' by default, but it needs to be 'https' - /// - private static string HttpsUri(string appUrl) - { - return Regex.Replace(appUrl, "(?m)http://", "https://").TrimEnd('/'); - } - -#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 - - } + /// + /// 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 IEnumerator InvokeApi (string apiName, Method httpMethod, Action> callback = null) where T : new() + { + 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.ParseData (callback); + } + + /// + /// Invokes custom API with body + /// + public IEnumerator InvokeApi (string apiName, Method httpMethod, T body, Action> callback = null) where T : new() + { + 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.ParseData (callback); + } + + /// + /// 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 HttpsUri (string appUrl) + { + return Regex.Replace (appUrl, "(?si)^http://", "https://").TrimEnd ('/'); + } + + #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 index e872caf..d546cc6 100644 --- a/helpers/DataHelper.cs +++ b/helpers/DataHelper.cs @@ -3,16 +3,17 @@ namespace Unity3dAzure.AppServices { - public class DataHelper { + public class DataHelper + { - public static byte[] ToUTF8Bytes(string utf8String) + public static byte[] ToUTF8Bytes (string utf8String) { - return Encoding.UTF8.GetBytes( utf8String ); + return Encoding.UTF8.GetBytes (utf8String); } - public static string ToUTF8String(byte[] bytes) + public static string ToUTF8String (byte[] bytes) { - return Encoding.UTF8.GetString( bytes ); + return Encoding.UTF8.GetString (bytes); } } diff --git a/helpers/JsonHelper.cs b/helpers/JsonHelper.cs index f9b2a18..03b2294 100644 --- a/helpers/JsonHelper.cs +++ b/helpers/JsonHelper.cs @@ -5,11 +5,12 @@ namespace Unity3dAzure.AppServices { - public class JsonHelper { + public class JsonHelper + { /// /// Workaround for json array described on https://forum.unity3d.com/threads/how-to-load-an-array-with-jsonutility.375735/ /// - public static T[] GetJsonArray(string json) + public static T[] GetJsonArray (string json) { string newJson = "{\"array\":" + json + "}"; Wrapper wrapper = JsonUtility.FromJson> (newJson); @@ -25,10 +26,10 @@ private class Wrapper /// /// 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) + 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]*\\\",)?", ""); + 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]*\\\",)?", ""); } } diff --git a/helpers/Model.cs b/helpers/Model.cs index 22e375a..eef4235 100644 --- a/helpers/Model.cs +++ b/helpers/Model.cs @@ -5,32 +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); // GetProperty for UWP #else - return obj.GetType().GetProperty(propertyName); + return obj.GetType ().GetProperty (propertyName); #endif } - public static bool HasField(object obj, string fieldName) + public static bool HasField (object obj, string fieldName) { - return GetField(obj, fieldName) != null; + return GetField (obj, fieldName) != null; } - public static FieldInfo GetField(object obj, string fieldName) + 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); + return obj.GetType ().GetField (fieldName); #endif } } diff --git a/http/Method.cs b/http/Method.cs index b26f1d3..b2d5e6f 100644 --- a/http/Method.cs +++ b/http/Method.cs @@ -3,10 +3,11 @@ namespace Unity3dAzure.AppServices { - public enum Method { - GET, - POST, - PATCH, + public enum Method + { + GET, + POST, + PATCH, DELETE - }; + } } \ No newline at end of file diff --git a/http/RestRequest.cs b/http/RestRequest.cs index 601ea7f..c182bf4 100644 --- a/http/RestRequest.cs +++ b/http/RestRequest.cs @@ -6,20 +6,23 @@ namespace Unity3dAzure.AppServices { - public abstract class RestRequest { - + public abstract class RestRequest + { public UnityWebRequest request { get; private set; } - public RestRequest(string url, Method method) { - request = new UnityWebRequest (url, method.ToString()); - request.downloadHandler = new DownloadHandlerBuffer(); + public RestRequest (string url, Method method) + { + request = new UnityWebRequest (url, method.ToString ()); + request.downloadHandler = new DownloadHandlerBuffer (); } - public void AddHeader(string key, string value) { + public void AddHeader (string key, string value) + { request.SetRequestHeader (key, value); } - public void AddBody(byte[] bytes, string contentType) { + public void AddBody (byte[] bytes, string contentType) + { if (request.uploadHandler != null) { Debug.LogWarning ("Request body can only be set once"); return; @@ -28,42 +31,88 @@ public void AddBody(byte[] bytes, string contentType) { request.uploadHandler.contentType = contentType; } - public virtual void AddBody(T data, string contentType="application/json; charset=utf-8") { + public virtual void AddBody (T data, string contentType = "application/json; charset=utf-8") + { string jsonString = JsonUtility.ToJson (data); - byte[] bytes = DataHelper.ToUTF8Bytes( jsonString ); + byte[] bytes = DataHelper.ToUTF8Bytes (jsonString); this.AddBody (bytes, contentType); } #region Response and json object parsing - public void ParseData(Action> callback = null) { - Debug.Log ("parse data response:" + request.url); - int code = Convert.ToInt32 (request.responseCode); - HttpStatusCode statusCode = (HttpStatusCode)Enum.Parse(typeof (HttpStatusCode), request.responseCode.ToString()); - string text = request.downloadHandler.text; - // TODO: add error handling - Debug.Log ("HttpStatusCode:" + statusCode + " code:" + code + " request url:" + request.url); - if (statusCode == HttpStatusCode.OK) { - Debug.Log ("Status OK"); + + /// + /// Shared method to return response result whether an object or array of objects + /// + private RestResult ParseResultAsAnArray (bool isArray) + { + 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; + } + + if (isArray) { + // try parse an array of objects + result.AnArrayOfObjects = JsonHelper.GetJsonArray (request.downloadHandler.text); + if (result.AnArrayOfObjects == null) { + result.IsError = true; + result.ErrorMessage = "Failed to parse an array of objects of type: " + typeof(T).ToString (); + } + } else { + // try parse an object + result.AnObject = JsonUtility.FromJson (request.downloadHandler.text); + if (result.AnObject == null) { + result.IsError = true; + result.ErrorMessage = "Failed to parse object of type: " + typeof(T).ToString (); + } } - T data = JsonUtility.FromJson(text); - callback( new RestResponse (request.url, statusCode, text, data) ); + return result; + } + + /// + /// Parses object with T data = JsonUtil.FromJson, then callback RestResponse + /// + public void ParseData (Action> callback = null) + { + RestResult result = ParseResultAsAnArray (false); + + 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)); + } + + // all done, clean-up request.Dispose (); } - public void ParseDataArray(Action> callback = null) { - Debug.Log ("parse data array response:" + request.url); - int code = Convert.ToInt32 (request.responseCode); - HttpStatusCode statusCode = (HttpStatusCode)Enum.Parse(typeof (HttpStatusCode), request.responseCode.ToString()); - string text = request.downloadHandler.text; - // TODO: add error handling - Debug.Log ("HttpStatusCode:" + statusCode + " code:" + code + " request url:" + request.url); - if (statusCode == HttpStatusCode.OK) { - Debug.Log ("Status OK"); + /// + /// Parses array of objects with T[] data = JsonHelper.GetJsonArray, then callback RestResponse + /// + public void ParseDataArray (Action> callback = null) + { + RestResult result = ParseResultAsAnArray (true); + + 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)); } - T[] data = JsonHelper.GetJsonArray(request.downloadHandler.text); - callback ( new RestResponse (request.url, statusCode, text, data) ); + + // all done, clean-up request.Dispose (); } + #endregion } diff --git a/http/RestResponse.cs b/http/RestResponse.cs index 35a4d83..3bf25b0 100644 --- a/http/RestResponse.cs +++ b/http/RestResponse.cs @@ -5,88 +5,79 @@ namespace Unity3dAzure.AppServices { public abstract class Response { - private bool isError; - public bool IsError { - get { - return this.isError; - } - } + public bool IsError { get; internal set; } - private string errorMessage; - public string ErrorMessage { - get { - return this.errorMessage; - } - } + public string ErrorMessage { get; internal set; } - private string url; - public string Url { - get { - return this.url; - } - } + public string Url { get; internal set; } - private HttpStatusCode statusCode; - public HttpStatusCode StatusCode { - get { - return this.statusCode; - } - } + public HttpStatusCode StatusCode { get; internal set; } + + public string Content { get; internal set; } - private string content; - public string Content { - get { - return this.content; - } + protected Response (HttpStatusCode statusCode) + { + this.StatusCode = statusCode; + this.IsError = !((int)statusCode >= 200 && (int)statusCode < 300); } // success - protected Response (string url, HttpStatusCode statusCode, string text) + protected Response (HttpStatusCode statusCode, string url, string text) { - this.isError = false; - this.url = url; - this.errorMessage = null; - this.statusCode = statusCode; - this.content = text; + this.IsError = false; + this.Url = url; + this.ErrorMessage = null; + this.StatusCode = statusCode; + this.Content = text; } // failure - protected Response (string error, string url, HttpStatusCode statusCode, string text) + 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; + this.IsError = true; + this.Url = url; + this.ErrorMessage = error; + this.StatusCode = statusCode; + this.Content = text; } } public sealed class RestResponse : Response { // success - public RestResponse (string url, HttpStatusCode statusCode, string text) : base(url, statusCode, text){} + public RestResponse (HttpStatusCode statusCode, string url, string text) : base (statusCode, url, text) + { + } // failure - public RestResponse (string error, string url, HttpStatusCode statusCode, string text) : base(error, url, statusCode, text){} + public RestResponse (string error, HttpStatusCode statusCode, string url, string text) : base (error, statusCode, url, text) + { + } } public sealed class RestResponse : Response, IRestResponse { - private T data; - public T Data { - get { - return this.data; - } - } + public T Data { get; internal set; } // success - public RestResponse (string url, HttpStatusCode statusCode, string text, T data) : base(url, statusCode, text) + public RestResponse (HttpStatusCode statusCode, string url, string text, T data) : base (statusCode, url, text) { - this.data = data; + this.Data = data; } // failure - public RestResponse (string error, string url, HttpStatusCode statusCode, string text) : base(error, url, statusCode, text) + public RestResponse (string error, HttpStatusCode statusCode, string url, string text) : base (error, statusCode, url, text) + { + } + } + + internal sealed class RestResult : Response + { + public T AnObject { get; internal set; } + + public T[] AnArrayOfObjects { get; internal set; } + + public RestResult (HttpStatusCode statusCode) : base (statusCode) { } } diff --git a/http/ZumoRequest.cs b/http/ZumoRequest.cs index d4df304..13b8a98 100644 --- a/http/ZumoRequest.cs +++ b/http/ZumoRequest.cs @@ -3,32 +3,32 @@ namespace Unity3dAzure.AppServices { - 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"); + 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"); - 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 ); + 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 ca0abad..411e5d6 100644 --- a/model/AccessToken.cs +++ b/model/AccessToken.cs @@ -3,19 +3,19 @@ namespace Unity3dAzure.AppServices { [Serializable] - public class AccessToken - { + public class AccessToken + { public string access_token; - + /// /// Facebook, Google, AAD access_token request /// - public AccessToken(string accessTokenValue) + public AccessToken (string accessTokenValue) { access_token = accessTokenValue; } - // Must have a public parameterless contructor in order to use it as paramter 'T' in the generic type or method RestRequest.AddBody -// 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 ba5cb03..a473009 100644 --- a/model/MobileServiceUser.cs +++ b/model/MobileServiceUser.cs @@ -3,9 +3,9 @@ namespace Unity3dAzure.AppServices { [Serializable] - public class MobileServiceUser - { + public class MobileServiceUser + { public string authenticationToken; public User user; - } + } } \ No newline at end of file diff --git a/model/User.cs b/model/User.cs index 609c189..479c714 100644 --- a/model/User.cs +++ b/model/User.cs @@ -3,8 +3,8 @@ namespace Unity3dAzure.AppServices { [Serializable] - public class User - { + 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 d99f25d..7f9b82e 100644 --- a/model/base/DataModel.cs +++ b/model/base/DataModel.cs @@ -5,13 +5,17 @@ namespace Unity3dAzure.AppServices { [Serializable] public class DataModel : IDataModel - { + { [SerializeField] public string id; - public string GetId() { + + public string GetId () + { return id; } - public void SetId(string id) { + + public void SetId (string id) + { this.id = id; } @@ -19,9 +23,11 @@ public void SetId(string id) { [SerializeField] private string createdAt; private DateTime? _createdAt; - public DateTime? CreatedAt() { + + public DateTime? CreatedAt () + { if (_createdAt == null) { - _createdAt = Convert.ToDateTime(createdAt); + _createdAt = Convert.ToDateTime (createdAt); } return _createdAt; } @@ -29,32 +35,37 @@ public void SetId(string id) { [SerializeField] private string updatedAt; private DateTime? _updatedAt; - public DateTime? UpdatedAt() { + + public DateTime? UpdatedAt () + { if (_updatedAt == null) { - _updatedAt = Convert.ToDateTime(updatedAt); + _updatedAt = Convert.ToDateTime (updatedAt); } return _updatedAt; } [SerializeField] private string version; - public string Version() + + public string Version () { return version; } [SerializeField] private bool deleted; - public bool Deleted() + + public bool Deleted () { return deleted; } [SerializeField] private string ROW_NUMBER; - public uint RowNumber() + + public uint RowNumber () { - return Convert.ToUInt32(ROW_NUMBER, 10); + return Convert.ToUInt32 (ROW_NUMBER, 10); } public override string ToString () @@ -62,9 +73,9 @@ public override string ToString () return JsonUtility.ToJson (this); } - public string ToJSON() + 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/table/IAzureMobileServiceTable.cs b/table/IAzureMobileServiceTable.cs index f01715c..3ae76cc 100644 --- a/table/IAzureMobileServiceTable.cs +++ b/table/IAzureMobileServiceTable.cs @@ -3,43 +3,43 @@ namespace Unity3dAzure.AppServices { - public interface IAzureMobileServiceTable - { - /// - /// Create a new item. - /// - IEnumerator Insert(T item, Action> callback = null) where T : new(); - - /// - /// Get a list of items. - /// - 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(); + public interface IAzureMobileServiceTable + { + /// + /// Create a new item. + /// + IEnumerator Insert (T item, Action> callback = null) where T : new(); + + /// + /// Get a list of items. + /// + 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 : INestedResults, new(); - - /// - /// Get an item's data using id property. - /// - IEnumerator Lookup(string id, Action> callback = null) where T : new(); - } + IEnumerator Query (CustomQuery query, Action> callback = null) where T : INestedResults, new(); + + /// + /// Get an item's data using id property. + /// + 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 ca52a3b..dc4fb16 100644 --- a/table/MobileServiceTable.cs +++ b/table/MobileServiceTable.cs @@ -5,110 +5,105 @@ namespace Unity3dAzure.AppServices { public class MobileServiceTable : IAzureMobileServiceTable - { - private MobileServiceClient _client; - private string _name; + { + 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 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.ParseData(callback); - } + 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 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.ParseDataArray(callback); + public override string ToString () + { + return _name; } - - 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(); + + 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.ParseData (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.ParseDataArray (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.ParseDataArray (callback); - } + } - public IEnumerator Query(CustomQuery query, Action> callback = null) where T : INestedResults, new() + public IEnumerator Query (CustomQuery query, Action> callback = null) where T : INestedResults, 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.ParseData(callback); + 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.ParseData (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"); + + 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.ParseData(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.ParseData(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.ParseData(callback); - } + } + 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.ParseData (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.ParseData (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.ParseData (callback); + } - } + } } \ No newline at end of file From 23eaaf130e0e72f6a0a299fef46b109bc12a1634 Mon Sep 17 00:00:00 2001 From: David Douglas Date: Tue, 22 Nov 2016 19:09:11 +0000 Subject: [PATCH 3/5] :package: UnityWebRequest and updated documentation --- README.md | 21 ++++++++++++++++----- http/RestResponse.cs | 16 ++++++++-------- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 742c8dd..b570878 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ 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 [JsonUtil](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. +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 AppServices](https://github.com/Unity3dAzure/AppServices/archive/master.zip) @@ -83,6 +83,21 @@ private void OnReadItemsCompleted(IRestResponse response) { } ``` +## 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. + ## Supported platforms Will work on all the platforms [UnityWebRequest](https://docs.unity3d.com/Manual/UnityWebRequest.html) supports including: * Unity Editor and Standalone players @@ -90,8 +105,4 @@ Will work on all the platforms [UnityWebRequest](https://docs.unity3d.com/Manual * Android * Windows -## Known issues -There is an issue with using [Patch on Android using UnityWebRequest with 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. [Dimitris-Ilias Gkanatsios](https://twitter.com/dgkanatsios) who wrote an [AzureServicesForUnity](https://github.com/dgkanatsios/AzureServicesForUnity) library has documented a possible [workaround for using PATCH on Android](http://stackoverflow.com/questions/39023937/trying-to-use-patch-on-a-unitywebrequest-on-android-and-getting-unsupported-pro) using Easy APIs. - Questions or tweet #Azure #GameDev [@deadlyfingers](https://twitter.com/deadlyfingers) diff --git a/http/RestResponse.cs b/http/RestResponse.cs index 3bf25b0..baac14f 100644 --- a/http/RestResponse.cs +++ b/http/RestResponse.cs @@ -5,15 +5,15 @@ namespace Unity3dAzure.AppServices { public abstract class Response { - public bool IsError { get; internal set; } + public bool IsError { get; set; } - public string ErrorMessage { get; internal set; } + public string ErrorMessage { get; set; } - public string Url { get; internal set; } + public string Url { get; set; } - public HttpStatusCode StatusCode { get; internal set; } + public HttpStatusCode StatusCode { get; set; } - public string Content { get; internal set; } + public string Content { get; set; } protected Response (HttpStatusCode statusCode) { @@ -57,7 +57,7 @@ public RestResponse (string error, HttpStatusCode statusCode, string url, string public sealed class RestResponse : Response, IRestResponse { - public T Data { get; internal set; } + public T Data { get; set; } // success public RestResponse (HttpStatusCode statusCode, string url, string text, T data) : base (statusCode, url, text) @@ -73,9 +73,9 @@ public RestResponse (string error, HttpStatusCode statusCode, string url, string internal sealed class RestResult : Response { - public T AnObject { get; internal set; } + public T AnObject { get; set; } - public T[] AnArrayOfObjects { get; internal set; } + public T[] AnArrayOfObjects { get; set; } public RestResult (HttpStatusCode statusCode) : base (statusCode) { From 0b0c1716efbbe8953be845b42254ed81e541e483 Mon Sep 17 00:00:00 2001 From: David Douglas Date: Tue, 22 Nov 2016 20:48:56 +0000 Subject: [PATCH 4/5] :package: UnityWebRequest and updated AppServices documentation --- README.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index b570878..387b2ef 100644 --- a/README.md +++ b/README.md @@ -85,16 +85,16 @@ private void OnReadItemsCompleted(IRestResponse response) { ## 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')); - ``` + 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. From c052f6192152b4210e54047eacfe5f3bff9c402e Mon Sep 17 00:00:00 2001 From: David Douglas Date: Mon, 28 Nov 2016 09:53:14 +0000 Subject: [PATCH 5/5] :construction: UWP and JsonUtility work-around for parsing arrays in WSA --- client/MobileServiceClient.cs | 6 +- helpers/JsonHelper.cs | 121 ++++++++++++++++++----- http/RestRequest.cs | 153 ++++++++++++++++++++---------- http/RestResponse.cs | 6 +- http/ZumoRequest.cs | 1 - model/nested/INestedResults.cs | 11 +++ model/nested/NestedResults.cs | 39 ++++++-- table/IAzureMobileServiceTable.cs | 16 ++-- table/MobileServiceTable.cs | 34 ++++--- 9 files changed, 278 insertions(+), 109 deletions(-) diff --git a/client/MobileServiceClient.cs b/client/MobileServiceClient.cs index 4d9b942..7bdf401 100644 --- a/client/MobileServiceClient.cs +++ b/client/MobileServiceClient.cs @@ -57,7 +57,7 @@ public IEnumerator Login (MobileServiceAuthenticationProvider provider, string t ZumoRequest request = new ZumoRequest (this, url, Method.POST); request.AddBodyAccessToken (token); yield return request.request.Send (); - request.ParseData (callback); + request.ParseJson (callback); } /// @@ -87,7 +87,7 @@ public void Login(MobileServiceAuthenticationProvider provider) Debug.Log (httpMethod.ToString () + " custom API Request Url: " + url); ZumoRequest request = new ZumoRequest (this, url, httpMethod); yield return request.request.Send (); - request.ParseData (callback); + request.ParseJson (callback); } /// @@ -100,7 +100,7 @@ public void Login(MobileServiceAuthenticationProvider provider) ZumoRequest request = new ZumoRequest (this, url, httpMethod); request.AddBody (body); yield return request.request.Send (); - request.ParseData (callback); + request.ParseJson (callback); } /// diff --git a/helpers/JsonHelper.cs b/helpers/JsonHelper.cs index 03b2294..4f3d564 100644 --- a/helpers/JsonHelper.cs +++ b/helpers/JsonHelper.cs @@ -2,35 +2,106 @@ 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 { - public class JsonHelper - { - /// - /// Workaround for json array described on https://forum.unity3d.com/threads/how-to-load-an-array-with-jsonutility.375735/ - /// - public static T[] GetJsonArray (string json) - { - string newJson = "{\"array\":" + json + "}"; - Wrapper wrapper = JsonUtility.FromJson> (newJson); - return wrapper.array; - } + /// + /// 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; + } - [Serializable] - private 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; + } - /// - /// 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]*\\\",)?", ""); - } - } + 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/http/RestRequest.cs b/http/RestRequest.cs index c182bf4..c7af339 100644 --- a/http/RestRequest.cs +++ b/http/RestRequest.cs @@ -1,12 +1,11 @@ using UnityEngine; -using System.Collections; using UnityEngine.Networking; using System; using System.Net; namespace Unity3dAzure.AppServices { - public abstract class RestRequest + public abstract class RestRequest : IDisposable { public UnityWebRequest request { get; private set; } @@ -40,49 +39,68 @@ public void AddBody (byte[] bytes, string 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 ParseResultAsAnArray (bool isArray) + private RestResult TryParseJsonArray () { - 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; - } - - if (isArray) { - // try parse an array of objects - result.AnArrayOfObjects = JsonHelper.GetJsonArray (request.downloadHandler.text); - if (result.AnArrayOfObjects == null) { - result.IsError = true; - result.ErrorMessage = "Failed to parse an array of objects of type: " + typeof(T).ToString (); - } - } else { - // try parse an object - result.AnObject = JsonUtility.FromJson (request.downloadHandler.text); - if (result.AnObject == null) { - result.IsError = true; - result.ErrorMessage = "Failed to parse object of type: " + typeof(T).ToString (); - } - } + 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; } - /// - /// Parses object with T data = JsonUtil.FromJson, then callback RestResponse - /// - public void ParseData (Action> callback = null) + 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 = ParseResultAsAnArray (false); + RestResult result = TryParseJson (); if (result.IsError) { Debug.LogWarning ("Response error status:" + result.StatusCode + " code:" + request.responseCode + " error:" + result.ErrorMessage + " request url:" + request.url); @@ -90,17 +108,15 @@ public void ParseData (Action> callback = null) } else { callback (new RestResponse (result.StatusCode, request.url, request.downloadHandler.text, result.AnObject)); } - - // all done, clean-up - request.Dispose (); + this.Dispose(); } /// /// Parses array of objects with T[] data = JsonHelper.GetJsonArray, then callback RestResponse /// - public void ParseDataArray (Action> callback = null) + public void ParseJsonArray (Action> callback = null) { - RestResult result = ParseResultAsAnArray (true); + RestResult result = TryParseJsonArray (); if (result.IsError) { Debug.LogWarning ("Response error status:" + result.StatusCode + " code:" + request.responseCode + " error:" + result.ErrorMessage + " request url:" + request.url); @@ -108,12 +124,49 @@ public void ParseDataArray (Action> callback = null) } else { callback (new RestResponse (result.StatusCode, request.url, request.downloadHandler.text, result.AnArrayOfObjects)); } - - // all done, clean-up - request.Dispose (); - } - - #endregion - - } + 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 index baac14f..989532b 100644 --- a/http/RestResponse.cs +++ b/http/RestResponse.cs @@ -1,5 +1,4 @@ -using System; -using System.Net; +using System.Net; namespace Unity3dAzure.AppServices { @@ -71,6 +70,9 @@ public RestResponse (string error, HttpStatusCode statusCode, string url, string } } + /// + /// Parsed JSON result could either be an object or an array of objects + /// internal sealed class RestResult : Response { public T AnObject { get; set; } diff --git a/http/ZumoRequest.cs b/http/ZumoRequest.cs index 13b8a98..6409817 100644 --- a/http/ZumoRequest.cs +++ b/http/ZumoRequest.cs @@ -1,5 +1,4 @@ using UnityEngine; -using System; namespace Unity3dAzure.AppServices { diff --git a/model/nested/INestedResults.cs b/model/nested/INestedResults.cs index 8bff8dc..413f49b 100644 --- a/model/nested/INestedResults.cs +++ b/model/nested/INestedResults.cs @@ -6,4 +6,15 @@ 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 e8b2950..4db78c1 100644 --- a/model/nested/NestedResults.cs +++ b/model/nested/NestedResults.cs @@ -2,13 +2,34 @@ namespace Unity3dAzure.AppServices { - /// - /// Wrap your data model with this object to call the table Query with `$inlinecount=allpages` param. - /// - [Serializable] - public class NestedResults : INestedResults - { - public uint count; - public T[] results; - } + /// + /// 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 3ae76cc..f3b70a7 100644 --- a/table/IAzureMobileServiceTable.cs +++ b/table/IAzureMobileServiceTable.cs @@ -32,14 +32,14 @@ public interface IAzureMobileServiceTable /// 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 : INestedResults, 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. - /// - IEnumerator Lookup (string id, Action> callback = null) where T : new(); + /// + /// Get an item's data using id property. + /// + 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 dc4fb16..c5232e2 100644 --- a/table/MobileServiceTable.cs +++ b/table/MobileServiceTable.cs @@ -29,7 +29,7 @@ public override string ToString () Debug.Log ("Insert Request: " + url); request.AddBody (item); yield return request.request.Send (); - request.ParseData (callback); + request.ParseJson (callback); } public IEnumerator Read (Action> callback = null) where T : new() @@ -38,7 +38,7 @@ public override string ToString () ZumoRequest request = new ZumoRequest (_client, url, Method.GET); Debug.Log ("Read Request: " + url); yield return request.request.Send (); - request.ParseDataArray (callback); + request.ParseJsonArray (callback); } public IEnumerator Query (CustomQuery query, Action> callback = null) where T : new() @@ -47,11 +47,11 @@ public override string ToString () ZumoRequest request = new ZumoRequest (_client, url, Method.GET); Debug.Log ("Query Request: " + url + " Query:" + query); yield return request.request.Send (); - request.ParseDataArray (callback); + request.ParseJsonArray (callback); } - - public IEnumerator Query (CustomQuery query, Action> callback = null) where T : INestedResults, new() - { + /* + public IEnumerator Query (CustomQuery query, Action> callback = null) where N : INestedResults where T : new() + { string queryString = query.ToString (); string q = queryString.Length > 0 ? "&" : "?"; queryString += string.Format ("{0}$inlinecount=allpages", q); @@ -59,10 +59,22 @@ public override string ToString () Debug.Log ("Query Request: " + url + " Paginated Query:" + query); ZumoRequest request = new ZumoRequest (_client, url, Method.GET); yield return request.request.Send (); - request.ParseData (callback); + request.TryParseJsonNestedArray ("results", callback); } + */ + public IEnumerator Query(CustomQuery query, 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.ParseJsonNestedArray>("results", callback); + } - public IEnumerator Update (T item, Action> callback = null) where T : new() + 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). @@ -84,7 +96,7 @@ public override string ToString () request.AddBody (item); Debug.Log ("Update Request Url: " + url + " patch:" + item); yield return request.request.Send (); - request.ParseData (callback); + request.ParseJson (callback); } public IEnumerator Delete (string id, Action> callback = null) where T : new() @@ -93,7 +105,7 @@ public override string ToString () ZumoRequest request = new ZumoRequest (_client, url, Method.DELETE); Debug.Log ("Delete Request Url: " + url); yield return request.request.Send (); - request.ParseData (callback); + request.ParseJson (callback); } public IEnumerator Lookup (string id, Action> callback = null) where T : new() @@ -102,7 +114,7 @@ public override string ToString () ZumoRequest request = new ZumoRequest (_client, url, Method.GET); Debug.Log ("Lookup Request Url: " + url); yield return request.request.Send (); - request.ParseData (callback); + request.ParseJson (callback); } }